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.
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.
* 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
* feat(setup): guide wireless pairing from a picture-in-picture window
* feat(setup): share one child-process fix flow between Setup and Play Steam
* fix(setup): read the child-process limit from the settings provider
* fix(setup): re-check the child-process limit while Setup is open
* fix(setup): skip a dead saved pairing and quiet the pairing guide
* fix(setup): handle repeat guide launches and start pairing after the display choice
The Choreographer frame callback that drives the compositor ran on the
app's main thread, so main-thread work delayed the tick before it
reached the compositor thread. It now runs on a display-priority
HandlerThread with its own Choreographer; the session pref vsyncThread
(default true) switches back to the main thread. Each resume posts a new
callback with its own generation, so one left queued from an earlier
resume, on either thread, stops itself instead of ticking twice.
A new 10 s "pacing" log line reports how late the tick reaches the
compositor, how long a new frame waits to be drawn, game frames against
buffers re-attached after an empty attach, empty attaches and distinct
buffers, and presentation feedback shown, replaced before drawing and
not drawn.
* fix: hold Home as Steam on Retroid Nova pads
* fix(input): track Home Guide apart from controller Guide
* fix(input): keep held controls on Home cancel and resume on Home
Installed itch games are compared with the uploads itch lists for them. A game with a newer upload shows an Update label in the Library and an Update button on its page, a replaced build asks first, and a Check for updates button in the itch Library header runs the check on demand.
Game and global component choices were written to desired.tsv only when a
selection changed, so a Proton build installed later (DroidDeck-Proton
11-11.8) had no rows and kept its bundled FEX. Battle.net's d2rfix FEX never
reached it and StarCraft died in ClientSdk.dll's DllMain. The choices are now
republished at session start and on Battle.net's FEX request whenever the
installed tools no longer match what was published.
Remove the Performance mode switch. The session always runs the way
Performance mode did: replaced buffers go back to the game at once
(except while mirroring to glasses), gamescope's WSI layer and winex11
reuse their per-present window checks, and games get eight swapchain
images while uncapped. Steam's Frame Limit in the Quick Access menu is
the only limit.
gamescope 3.16.29-p15 (p14 plus two patches):
- 0119: with the Frame Limit on, gamescope forces FIFO while the WSI
layer hands the driver MAILBOX, so a game with eight images could
queue up to seven frames ahead of the screen. While the limiter forces
FIFO the layer uses its default of 3 images; DXVK and VKD3D-Proton
recreate their swapchain when the limiter changes, other apps are asked
to with VK_SUBOPTIMAL_KHR.
- 0120: -r fixed gamescope's pacing and limiter rate for the whole
session. With GAMESCOPE_NESTED_REFRESH_FOLLOWS_HOST=1 a rate the host
has reported for 300 ms replaces it (a switch from 144 to 120 Hz passes
through 72 Hz for a few frames), and Steam gets the display information
again. The session sets it whenever it passes -r.
Steam keeps its Frame Limit as the client setting
gamescope_app_target_framerate and offers the values gamescope's display
state lists. After a refresh change, a limit the new rate does not offer
(72 at 120 Hz) left the slider on its first value while the game kept the
old limit. The Steam helper watches the offered values and, once they
settle, keeps the limit when it is still offered and moves it to the
highest value otherwise, through Steam's own setting so the slider shows
it. Steam's webpack does not expose its module cache, so the gamescope
service is found by the calls its module makes, and the helper retries
the install every 10 s until Steam is ready.
The session window asks for the fastest mode the peak refresh setting
allows and chose again only when the display changed, so after it had
asked for 60 Hz, raising Android's refresh setting left the display,
gamescope and the limit at 60. It now also chooses again when
peak_refresh_rate or min_refresh_rate changes.
Steam's arm64 streaming_client only decodes through a V4L2 stateful
decoder and libdrm dumb buffers, neither of which exist on Android, so
streams from a PC never showed a picture. The preload now offers it a
/dev/video-dec0 and a libdrm stand-in backed by dma-heap buffers, and
each frame is decoded by the app's MediaCodec (Qualcomm low-latency
mode) over the encoder socket, falling back to the FFmpeg in Steam's
runtime.
* Add itch.io collections to the Library
A Collections button on the Library's itch tab filters the library to one collection of the signed-in account. Its games that are not installed show in the Not installed section.
* Keep itch collections complete across failures, sign-out and empty libraries
A failed later page no longer publishes a partial collection, selecting a collection refreshes it, pagination runs until an empty page, signing out clears collection state, and the itch chip shows for a signed-in account with no owned games.
* Tie itch collection requests to the account that started them
Requests capture the account generation before reading the key and dedupe per generation, sign-out clears collections after the token is removed, and the collections load follows the itch sign-in state.
* Keep paging itch collections past a page of already seen games
* Run the itch collections tests under Robolectric like the other itch tests
* Let the Installed filter apply inside a collection and keep the grid for an empty itch account
* fix(itch): stop paging a collection when a page adds no new games
* Keep the collections icon on the itch collections tab when it opens
---------
Co-authored-by: maxjivi05 <maxjividen@gmail.com>