Libc-internal file operations bypass the existing preload wrappers and
still stop in PRoot. Patch recognized ARM64 SVC sites in libc and the
runtime loader, reuse the current translator for 20 path syscall
numbers, and return declined calls to PRoot. Enabled by default after
the tracer-key check; `PROOT_FP_PATCH_SVC=0` remains the troubleshooting
opt-out.
The hardened implementation patches 47 sites on Thor's glibc 2.43.
Relays preserve general/SIMD registers, flags, FP state and errno, and
provide unwind metadata back to the original libc frame. Text is patched
through `/proc/self/mem` after relay pages become executable.
Initialization skips multiple live threads and SVE/SME. Alternate signal
stacks and insufficient remaining stack use PRoot; main-stack growth
respects the current limit. No proroot code or binaries are imported.
Robust-list set/get sites deliberately remain with PRoot/the kernel.
Device tests reproduced stale state when the earlier private-TLS
implementation mixed patched and raw callers. Removing that shortcut
fixes coherence and preserves the host's actual return policy; the
earlier thread-creation gain is retired. SDK forwarding still enables
faccessat2 ENOSYS fallback and clone3 ENOSYS only before Android 15.
Successful clone3 is never executed from the C relay. Existing fsync
handling is unchanged. No in-process RET_TRAP filter is added, so
SIGSYS-mask/handler machinery is not applicable.
Version-2 build-ID/API tables cache relative patch sites in `/tmp`.
Every hit revalidates executable bounds, alignment, instructions and
syscall numbers. Invalid/unsafe files rescan; publication uses a private
temporary file and atomic rename. The PRoot dependency fetch also gains
a mirror matching the existing pinned checksum, resolving upstream
availability failures without changing versions.
Five alternating pairs using the final bundled library on Thor/Android
13:
| operation | wrappers only | hardened relays |
|---|---:|---:|
| libc-internal stat | 49.6 us | 6.0 us |
| libc-internal open/close | 50.4 us | 7.8 us |
| libc faccessat | 49.7 us | 22.0 us |
| libc getcwd | 54.9 us | 5.0 us |
| thread create/join | 136 us | 144 us |
| complete tiny exec/wait | 4.44 ms | 5.60 ms |
An earlier five-pair hardened-library run measured stat 50.1 to 5.9 us
and open/close 51.9 to 7.1 us. The metadata gains survive the safety
checks. Neither run shows a thread-creation gain, and tiny execs retain
roughly 0.9-1.2 ms overhead.
Three full Steam startup pairs yielded Home-ready medians of 47.02 s off
/ 45.25 s on, with overlapping 43-50 s ranges. Steam/CEF-only PSS
medians were 1,223.1 / 1,233.9 MiB; the roughly 11 MiB difference is
smaller than observed variation, but does not establish zero memory
cost. Startup/memory used one unchanged signed bundled-library APK
without overrides; the final stack-gap guard only increases distance
reserved above other mappings. No reliable whole-client startup
improvement is claimed.
Earlier fresh Half-Life SD installs took 350/341 s off and 326/327 s on.
These are historical results from the preceding robust-list revision,
not a post-hardening download retest. Network/shader scheduling varied,
allocation did not reliably improve, and the repeat baseline committed
faster. This PR demonstrates metadata acceleration; it does not
demonstrate a reliable fix for Steam's SD allocation/commit bottleneck.
Validation on `2c3c135b` (production code unchanged since `4998b046`):
- Local release build, runtime/native-library checks, release signing
and four focused Android tests pass. [Final-head
CI](https://github.com/Droid-Deck/DroidDeck/actions/runs/40704837477)
passed: 1,431 Python/native tests (49 skips), 2,648 Android tests (zero
failures/skips), and 162 Rust tests. CodeRabbit is green on the same
head. The installed production APK forwards API 33 and patches 47 sites
with an unset enable flag and no override; Steam Home renders.
- Embedded and shared-PIC matrices pass on glibc 2.43; shared-PIC also
passes on separately loaded glibc 2.39. Coverage includes raw
results/errno/registers/FP state, path/flag/error and protected-pointer
cases, exact readlink boundaries, mixed raw/relay robust state,
concurrent threads, fork, thread churn, alternate-stack signals,
deferred/FIFO cancellation, signal siglongjmp recovery, forced unwind
with caller cleanup, and limited remaining stack.
- Four device test-process filters deny the memory patch file, relay
mapping, executable permission, or patch writes. Every case leaves
original syscall execution working. Host injection also verifies partial
installation after an individual site fails.
- Concurrent atomic symlink replacement passes 3,000 internal
stat/readlink iterations at default/zero TTL. Eight focused host tests
pass, covering instruction/branch limits, stack/limit fallback,
untrusted cache files and NDEBUG checks. API selection gives 45/47/46
sites for unknown/33/simulated-35; unset and explicit 1 agree, and 0
leaves sites untouched. This does not replace Android 15 hardware
testing.
- All bench tools compile with -DNDEBUG. Relative/absolute fixture
workloads agree, and thread/ABI/failure checks pass on Thor with those
builds.
- Earlier internal fts/realpath/NSS/dlopen and normalized read-path
equivalence pass. The write suite retains the existing wrapper SD
Unix-socket mismatch against PRoot.
CodeRabbit disposition: fixed relative fixture paths, prevented
benchmark checks disappearing under NDEBUG, corrected the
limited-fallback description, and narrowed memory-file denial so the
thread check still succeeds. The default-off recommendation was
addressed by fixing/testing the underlying guard and unwind issue while
retaining the requested default-on behavior. The readlink count
suggestion is intentionally not adopted: device boundary comparisons
reproduce current PRoot behavior, so changing that inherited policy
belongs in a separate correctness change.
This is not consequence-free instruction patching: constructor time,
private text copies and stack checks have costs. Android 15+ ARM64 and
16 KiB-page hardware, arbitrary asynchronous cancellation/non-local
exits, user-managed stacks, and attributable tracer-counter measurements
remain gaps. Loader calls before the constructor and arbitrary guest ELF
syscall sites remain outside scope.
Thor was restored to its original APK and bundled library, with app data
preserved and test settings, overrides, temporary files and version-2
test caches cleared.
Detailed evidence and revision boundaries: [proroot
analysis](https://github.com/Droid-Deck/DroidDeck/blob/2c3c135b52c3dd949ef5a99a67d8c12eba24406c/docs/development/proroot-analysis.md).
Pressing Cancel (or B) while a session is installing the desktop or the
Steam-mode Proton seed now stops the download. Launching again right
after no longer opens a session screen that closes itself.
Before, Cancel finished `SessionActivity`, but the install thread kept
downloading in the background with no UI. Two things went wrong:
- `Downloader` and `DesktopCatalog.install` had no way to be cancelled.
- "an install is running" lived in a per-activity field
(`installingRuntime`). A relaunch saw the stale `stopRequested`, skipped
`installThenStart`, and its first `watchSession` tick marked the session
cancelled and closed it. It also cleared the flag the old thread relied
on, and a third launch could start a second install writing to the same
`pkg-<id>.download`.
Changes:
- `Downloader.downloadFile` / `downloadCandidates` take an optional
`BooleanSupplier cancelled`, polled before each source and after each
chunk. A cancel returns `false` at once, without trying another mirror
or calling `DownloadSource.failed()`. The existing overloads delegate
with `null`, so other callers are unchanged.
- `DesktopCatalog.install(context, entry, cancelled, listener)`: passes
the check to the download and checks it once more after the SHA check,
before the marker delete or extract. Extraction itself still runs to the
end. The old 3-argument overload stays, so `MainActivity.installPackage`
is unchanged.
- `SessionActivity`: `installDesktop` / `installProton` pass `{
SessionState.stopRequested }`. `installingRuntime` now reads the
process-wide `SessionState.installing`.
- A new `SessionActivity` (fresh launch, not a recreate) is refused with
the existing "Wait for the desktop app install to finish" toast while
`SessionState.installing != null && !running`.
`MainActivity.startActivity` applies the same check before the launch
animation.
- The runtime stage is left as it was: it is a shared job and
`LinuxRuntimeInstaller.isBusy()` already blocks relaunch.
Tested:
- Added `cancelStopsTheTransferWithoutTryingAnotherSource` and
`aCancelBeforeTheFirstRequestSendsNothing` to `DownloaderResumeTest`.
That class passes (9/9), as does `DesktopCatalogTest`, and the app
compiles.
- Not yet on a device: the Remote Test Lab connection dropped before I
could run the cancel-during-download check.
## Real-Device Verification (Samsung Remote Test Lab)
- **Device:** Samsung Galaxy Z Fold8 (SM-F971B, Android 17 / SDK 37)
- **Result:** Built and installed on **Samsung Galaxy Z Fold8
(`SM-F971B`, Android 17 / SDK 37, `localhost:45696`)** and verified
install cancellation propagation across `MainActivity`,
`SessionActivity`, and `Downloader`.
## Summary
Adds update detection for installed itch.io games. This is PR4 of the
itch.io store rework (browse, filters, game page and collections are
merged; Linux support is #1589). Butler patching comes later as an
add-on pack. Updates here download the full build.
## What changed
- `ItchUpdates` (new): compares each installed itch game with the
uploads itch lists for it (`GET /games/{id}/uploads`), using what the
install already saves: `installVersion` (the upload's `updated_at`) plus
`gameId`, `downloadKeyId` and `uploadId` in `extra`.
- The installed upload is still listed and has a later `updated_at`: an
update.
- The installed upload is gone: the newest Windows upload is offered as
a replaced build.
- An install without an `uploadId` compares the newest Windows upload's
date.
- An unknown installed version, or no answer from itch, never reports an
update.
- The check walks installed games one at a time, one second apart, and
logs `itch.io: checked <n> installed games, <m> with updates`. It runs
when the itch tab opens (at most every 6 hours) and from the new button.
A game itch did not answer for keeps its last result. Results are cached
in the itch preferences until the update is installed or the account
signs out.
- Library: games with an update show an "Update" label on their tile and
row. A Check for updates button in the Library header (itch tab only)
runs the check on demand, shows progress, and ends with a toast:
"Updates available: N" or "Up to date".
- Game page: an Update button appears for installed itch games with an
update. A replaced build asks first and names the new file. A plain
newer build updates directly.
- Safe updates: an update never extracts into the live game. The archive
is unpacked into `.droiddeck-update` inside the game folder, and the new
build must contain an executable before any game file is touched. A
failed or cancelled download or extraction removes the staging folder
and leaves the game as it was. Then `ItchStaging.merge` moves the staged
files into place with renames inside one folder. A staged file replaces
the file with the same path, directories are merged, and anything the
new build doesn't contain (saves, settings, install record, launcher,
art) stays. The install record is marked updating just before the move,
so an interruption there is resumable.
- New strings `stores_itch_updates_found` and
`stores_itch_update_replaced` in all 33 locales. The button label, toast
and dialog buttons reuse existing strings.
- `docs/development/itch-updates.md` describes the feature.
## Notes
- One extra copy of the build's size must be free while an update runs.
- Files the developer removed in the new version stay in the game
folder, because everything the new build doesn't contain is kept.
- The final rename step is not an all-or-nothing swap. A crash or power
loss inside it leaves the game marked updating and resumable.
- The update reruns the normal install over the game's own folder and
storage location.
## Tests
- `ItchUpdatesTest` (Robolectric, like the other itch tests): newer,
same and older stamps, replaced upload falling back to the newest
Windows build, unknown installed version, no answer, an install without
an upload id, and the cache round trip.
- `ItchStagingTest`: new files replace old ones, other files stay, and a
file replacing a directory and the other way round.
- Not run on a device or compiled locally; relying on CI. The controller
focus behaviour of the new buttons is untested.
With no frame limit, AIO's Direct3D 7 (DirectDraw) test ran at exactly
144 fps on a 144 Hz panel: every frame waited one refresh (6.94 ms).
Wine asks for a swap interval of 1 on every OpenGL window, and WineD3D's
front-buffer drawing used by DirectDraw and Direct3D 1-7 never lowers
it; it only sets the interval in its present path. Mesa's Zink then
presents in FIFO.
droiddeck-game-env sets vblank_mode=0 for games launched through Proton
before the game's environment edits, so a value set in the launch
options or the editor wins and an editor Remove leaves Mesa's default.
Steam's own runs in compatdata/0 are unchanged. Steam's Frame Limit
still holds a game: gamescope's WSI layer presents every swapchain in
FIFO while its limiter is on.
On the AYANEO Pocket FIT, AIO Direct3D 7 went from 144 to 2,345 and
2,265 fps (1 % low 99 -> 620) with no limit; with the Quick Access
limit at 144 and 72 it held at 143.76 and 71.92.
At every start, Steam's shader pre-caching runs d3ddriverquery64.exe
through each Proton tool (DroidDeck-Proton and the ARM64 Proton) in
compatdata/0. Here the query has never succeeded (948 of 948 runs in
the client's shader log failed, mostly exit code 1), and each run
started Wine in that prefix, where the two Protons upgraded it back and
forth (11.0-100 <-> GE-Proton11-11) with the 32-bit wine.inf install.
When that overlapped a game's start, AIO ran 25-30 % slower for its
first 5-7 seconds.
Both Proton wrappers now exit 1 for Steam's own query, "run
.../d3ddriverquery64.exe" (or the 32-bit name) with STEAM_COMPAT_DATA_PATH
in compatdata/0, before doing anything else, so Steam gets the same
failure without Wine. Every other verb and prefix goes on to Proton.
A new game frame waited for the next screen tick before the compositor
drew it: 1.9-3.5 ms on average at 144 Hz, measured with the pacing
counters. With frames going straight back to the game (no compositor
limit), the compositor now draws as soon as the clients' requests are
handled, at most once per refresh, and presentation times stay on the
tick.
gamescope delivers about one frame per refresh, so the tick leaves a
frame younger than one refresh for the next arrival to draw, unless the
tick itself changed the window, effects or headset pose; a frame no
other frame follows is drawn at the tick after. Once a held frame is
still waiting at the next tick (a client paced by frame callbacks), the
tick draws every frame for a second before holding one again. The
queued draw checks again that drawing on arrival is still on.
Paced output (glasses), streaming and frame generation keep drawing on
the tick. The hidden pref renderOnCommit (default on) turns it off. The
pacing log line counts the scenes drawn on arrival. On the device new
frames wait 0.12 ms instead of ~2 ms.
0123 backports upstream 0efe47c and 666c52b by Matthew Schwartz (commit
completion list lifetime, no Rc to a dying commit). 0124 polls a new
commit's fence at import; a finished frame is marked done at once and
handled in the same steamcompmgr pass instead of a second wake through
the image waiter. commit_ready_check (default on) turns it off.
Pins gamescope 3.16.29-p18 (0123 + 0124). On the AYANEO Pocket FIT about
81 % of commit fences are ready at import; gamescope-xwm 22-26 % -> 16 %
of a core, gamescope_img 10-11 % -> under 2.5 %.
Wine numbers the Windows CPUs like the host, so Windows CPU 0 was host
core 0, a small core on most Android chips. Games that pin their main
thread to CPU 0 ran it there: GTA IV 32 fps on the AYANEO Pocket FIT,
61 fps once CPU 0 is the fastest core.
droiddeck-game-env hands Wine the game's cores fastest first, from each
core's cpu_capacity (or its top clock). Equal cores keep their order.
Cores picked globally or in a game profile stay the only cores the game
sees, now fastest first; a pick only counts when it narrows the cores.
Games on Proton's own default_cpu_limit list keep their count on the
fastest cores (quoted and unquoted counts); other values such as "P"
are left to Proton. A typed WINE_CPU_TOPOLOGY / PROTON_CPU_TOPOLOGY
still wins, an empty one turns it off for a game, and
DROIDDECK_FASTEST_CORES=0 turns it off. Steam's own Proton runs in
compatdata/0 are left as before.
Builds on maxjivi05's "Windows games see only the cores they are pinned
to" (9d85af75).
* feat(launcher): apps page with search, sort, filter, folders, widgets and page swipes
* feat(launcher): app shortcuts, pinned shortcuts, hidden apps and LauncherApps listing
* fix(ui): let page content scroll to the bottom edge
* feat(launcher): accept widgets other apps ask to pin
* i18n(launcher): translate apps page strings
* fix(launcher): keep disabled pins, serialize widget adds, add many plurals
* fix(launcher): drop in-flight pin refreshes after unpinning
* fix(launcher): skip unpin when pinned shortcuts can't be read
* fix(launcher): drop in-flight app lists when home role is lost
* fix(launcher): only migrate folder entries with one launcher activity