Commit Graph
1381 Commits
Author SHA1 Message Date
MaxsTechReview 4ff4be2371 fix: keep a slightly early vsync tick for frame pacing (#1405)
Gamescope paces from the presentation timestamp. The Choreographer tick
is often still slightly ahead of the render thread, and that tick was
thrown away, so the draw time was reported instead.

A tick within one refresh of the draw is now kept, whether it is
slightly ahead or slightly behind. A missing tick, or one more than a
refresh away, still falls back to the draw time.
2026-10-23 17:00:32 -04:00
Kurt Himebauch 61482ffee7 perf(ci): reuse verified build inputs
## Summary

Follow-up to #1412, focused on repeat work observed in completed CI
logs.

- Give APK compilation and Android tests the same build-label and
API-key inputs. Previously `generateReleaseBuildConfig` changed between
invocations and caused Kotlin/Java recompilation before tests.
- Continue executing the Android test suite on every CI run: disable
reuse of the test task's results in CI while retaining compilation
caching. Keep the existing heap and process isolation settings.
- Cache the four remote checksum-pinned model/AAR downloads used by
add-on packs. Hash-check every cache hit, atomically replace corrupt
entries only after a verified download, and verify the destination
bytes. Packs are still regenerated from current prepared components and
their checksums remain part of the APK.
- Bound download time and retry connection resets as well as HTTP
errors; keep checksum mismatches fatal.
- Include the pack recipes in runtime source freshness checks.
- Restrict PR report updates to the workflow bot's own marker comment,
addressing the minor extraction review finding from #1412.

## Evidence and validation

The previous successful run explicitly recorded changed
`BuildConfig.java` as the reason for repeated Kotlin/Java compilation
between assembly and tests. The final #1412 normal run spent 38 seconds
building/downloading packs, 5m59s building the APK, and 3m03s in the
Android test step. A forced run also hit a connection reset while
fetching pack inputs and passed on retry.

Focused tests cover cold/warm input reuse, corrupt-cache repair,
changed-pin invalidation, rejected download bytes, actual local HTTP
connection failure/retry, identical build metadata and bot-only comment
selection. Actionlint passed. The clean Linux full suite passed 1,149
tests (18 skipped) before the final Gradle test-execution policy block.
Host, APK/ABI and Android checks are green on the final head, including
the ordinary PR run against the updated main branch and both controlled
branch attempts below.

Validation will compare cold and warm runs on the same commit, confirm
cache-hit hash verification, confirm app compilation remains up-to-date
in the test invocation, and confirm the Android suite actually executes
on both runs. Source-tree, preload, APK/ABI, all-test, signing and
publishing gates remain.

Existing Gradle, CMake and Cargo caching remains. Native compilation was
tens of seconds in the inspected baseline; no speculative native cache
changes or unmeasured speed claim are included. Android test execution
itself was about 2m30s; this PR removes avoidable setup/recompilation
rather than weakening coverage or changing concurrency.

## Follow-up correction

The first CI run failed two artwork-key tests that assumed the
compiled-in API key was empty. Keeping build inputs consistent exposed
that assumption. Tests now check the configured build key and its
trimmed fallback after clearing a user key; existing pure-function
empty-key and precedence coverage remains. No key values are included in
assertion messages. The failed run confirmed BuildConfig generation and
Kotlin/Java compilation were UP-TO-DATE in the test invocation;
subsequent full runs passed. The merged #1412 main run40007656633 passed
signing, pack publishing, preview announcement and catalog updates.

## Gradle cache audit fixes

Completed-run logs showed setup-gradle was read-only on PRs.
Same-repository PRs and branch runs can now persist their own cache
entries; fork PRs remain read-only. GitHub cache ref isolation is
retained. See [Gradle cache
guidance](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md)
and [GitHub cache
scopes](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching).
PR caches consume additional shared quota; no claim of reuse between
different PR refs is made.

COMMIT_TIME now comes from SOURCE_COMMIT, matching the provenance field
and avoiding timestamp changes from temporary PR merge commits. The
source tree and pack validation remain unchanged. This enables a
meaningful same-commit warm check. Per-commit generated Java metadata
may still cause cross-commit recompilation; moving metadata into
packaged data is a separate candidate, not part of this patch. Head
9835c4e3 passed full CI and the same-commit warm rerun below.

## Confirmed final results

Both controlled runs use exactly
9835c4e3e1 and workflow_dispatch, avoiding
PR merge-tree changes between attempts.

| Measurement | First attempt | Warm attempt | Reduction |
| --- | ---: | ---: | ---: |
| Request to required gate green | 13m09s | 7m11s | 5m58s (45%) |
| Gradle APK build step | 6m11s | 2m08s | 4m03s (66%) |
| Android test step | 2m57s | 2m08s | 49s |
| Add-on pack build step | 40s | 31s | 9s |

[First
attempt](https://github.com/Droid-Deck/DroidDeck/actions/runs/40015059221/attempts/1)
and [warm
attempt](https://github.com/Droid-Deck/DroidDeck/actions/runs/40015059221/attempts/2)
both passed. Initial queue delay was 3 seconds in each. This is one
observed pair, not a guaranteed latency or isolated attribution of every
timing difference to cache changes. The first attempt already had some
dependency/build cache reuse.

Warm logs confirm the exact head's Gradle-home cache restored; release
Kotlin and Java compilation were FROM-CACHE during assembly, then
UP-TO-DATE during tests. Assembly reported 22 tasks FROM-CACHE. The
Android test executor actually ran; test results were neither cached nor
skipped. All four remote pack input hits were hash-verified; packs were
regenerated. Runtime inputs and APK native exports/dependencies
verified. The ordinary [PR
workflow](https://github.com/Droid-Deck/DroidDeck/actions/runs/40013127993/attempts/2)
also passed against the updated main merge tree.

A fresh Gradle daemon and "no cached configuration" remain expected on a
new runner: configuration/task-graph persistence is distinct from
build-output caching and currently has no configured encryption key. The
measured Kotlin/Java FROM-CACHE results demonstrate output reuse despite
that message.

This proves same-commit warm reuse. New pushes can still invalidate
compilation through changed source, toolchain, generated provenance or
pack metadata, and cache eviction/ref scope can cause misses.
Cross-commit compilation reuse is not established by this pair. Branch
validation intentionally skips main-only publishing; merged #1412's main
run40007656633 separately passed signing and publication. The user
authorized merging after the final cache-recovery correction passed.

## Final cache-recovery correction

Commit c95e1147 addresses the remaining minor review finding: after a
download passes its SHA check, an unexpected directory at the exact
hash-named cache entry is removed before the verified file is promoted.
Invalid downloads leave that directory untouched. A regression test
covers failed verification, successful repair and subsequent warm reuse.
All five focused download tests passed in a clean Linux export. All
final-head PR checks passed in
[run40018498031](https://github.com/Droid-Deck/DroidDeck/actions/runs/40018498031):
host suite ran 1,164 tests (49 platform skips), APK build and native
validation passed, Android tests executed successfully, and the
source/build validation status is green. Benchmark results above remain
measurements of 9835c4e3.
2026-10-23 16:34:50 -04:00
MaxsTechReview 1deae2fa5b chore: drop the comments added with the Unity TSO fix (#1423)
Follow-up to #1415: removes the code comment and docstring it added. No
behavior change.
2026-10-23 16:32:35 -04:00
XxJSONDeruloxX c95e114766 fix(ci): recover directory cache entries 2026-10-23 16:20:42 -04:00
maxjivi05 7eb4717d4c chore: drop the comments added with the Unity TSO fix 2026-10-23 16:16:10 -04:00
MaxsTechReview 0c26a829ec fix: keep FEX's TSO on for Unity games (#1415)
The default FEX preset (Performance + TSO) is fine for Unity. But when
the Default profile is switched to a preset
that turns TSO off (Performance, Extreme, Extreme-gn, Denuvo), Unity
players break. DroidDeck already keeps full TSO
for .NET games (`coreclr.dll`) whatever the preset; this gives Unity
players the same safety net for TSO.

Tested on a Pixel 10 Pro Fold (PowerVR DXT-48-1536), shared preset
Performance (TSO off):

| Game | Build | TSO off | TSO on |
| --- | --- | --- | --- |
| Megabonk | Unity 2023.2 IL2CPP, native Linux, "Linux x86 (FEX)" |
froze on about half of launches (every thread parked on a futex) | 5 of
5 starts, played run stayed responsive |
| Crab Game | Unity IL2CPP, Windows, Proton arm64 | 9 of 10 starts; 1
access violation in `UnityPlayer.dll` (read of a garbage pointer) | 10
of 10 starts |

Measured cost of TSO on Megabonk: none (+2% fps with TSO on, within
noise).

What changes:
- `droiddeck-game-env`: `unity_memory_ordering` sets `FEX_TSOENABLED=1`
when `UnityPlayer.dll`, `UnityPlayer.so` or
`GameAssembly.so` sits next to the executable (or one folder below). A
FEX preset chosen for the game still wins
(fixes sit between the shared preset and the per-game entry, as for
.NET).
- `droiddeck-fex`: the FEX tool now applies the environment half of
these game-file fixes, so native Linux Unity builds
  get it too. Launch arguments some fixes add stay Proton-only.
- Docs paragraph in `docs/development/game-environment.md`; tests in
`test_game_environment.py` and
  `test_droiddeck_fex.py`.

Device check of the change itself: with the session's
`droiddeck-game-env` patched, shared TSO off and no per-game
entry, Crab Game started with `FEX_TSOENABLED=1`.

Related to #1386, but not a fix for its Risk of Rain 2 crash: that game
is Unity Mono, which with the default preset
already has TSO on; the report points at multiblock instead (as the .NET
fix turns off). Both games tested here are
IL2CPP, so this PR doesn't change multiblock. #1386's other points
(`steam_appid.txt`, `device.txt`) aren't covered.

Tests (WSL, on main `16bedf36` with the patch): `test_game_environment`
+ `test_droiddeck_fex` 119 OK. Full `tools/tests`: 1114 tests, one
failure,
`test_appimage_run…test_a_failed_program_shows_the_log_it_wrote`, which
fails the same way without the patch.
2026-10-23 16:15:59 -04:00
MaxsTechReview 9010d275ab fix(steam): the last change to a game's compatibility tool wins (#1347)
Refs #1248

## Problem
Users report that changing a game's compatibility tool in Steam's game
properties doesn't stick: it goes back to DroidDeck's default. #1248 was
closed by #1250, but its reporter still saw it after clearing the game's
DroidDeck profile.

A game's tool can be chosen in two places, DroidDeck's per-game Proton
(`games.json`, written by `ProtonDefault.requestGame`) and Steam.
`plan_mapping` treats every `games.json` entry as an override that
always wins, so the live helper (`droiddeck-steam-compat`, on
`CompatManager.NotifyStateChanged`) sets it back within a second, and
`steam-compatibility` does again at every session start. A `null` entry
("use the default", also written by a profile reset) is forced as the
default, so resetting the profile made it worse.

Reproduced with the real `plan_mapping` (game 220, Steam set to
`droiddeck-fex`):

| `games.json` entry | before |
|---|---|
| none | kept |
| `null` (profile reset / "Default") | reverted to default |
| a DroidDeck Proton | reverted to it |

## Change
Whichever side changed a game's tool last wins:
- A DroidDeck choice is applied once (as now). Once applied, a different
pick in Steam stands:
- a pick of one of DroidDeck's Protons becomes the DroidDeck choice too
(`games.json` updated, so the app shows it);
- any other pick (FEX, a Steam Linux Runtime, clearing the forced tool)
removes the game from `games.json`.
- `null` is applied once and then dropped, so the game follows the
default like one DroidDeck never managed.
- What was applied is kept in `state.json` as `games_applied`. Choices
an earlier build already applied count as applied (when `state.json` is
from an earlier build), so upgrading respects the Steam picks users
already made.
- `games.json` is rewritten only if the app didn't replace it after it
was read, so a newer app choice is never overwritten.
- An x86-only Valve Proton picked in Steam is still replaced
(`unrunnable`), unchanged.

Both paths share the logic: `register_default` (session start, reads
config.vdf; with the client stopped, an app missing from the mapping
counts as cleared) and the live helper's `reconcile`.

## Tests
- `test_steam_compat_live.py` + `test_game_environment.py`: 109 pass
locally (103 existing + 6 new). The 6 new tests fail on the old scripts.
- Steam pick after an app choice is kept live, and a new app choice
applies again
  - Steam pick of a DroidDeck Proton becomes the app's choice
  - "use the default" applies once, then Steam picks stand
  - upgrade: choices an earlier build applied give way to Steam picks
  - an app choice written during a sync is not overwritten
  - session start keeps a Steam pick from config.vdf

Not device-tested.

## Side note: `test_authorship` writes into the global git hooks
`InstalledHooksTest.setUp` runs
`tools/release/install_authorship_hooks.py` in a temp repo, but the
installer reads `core.hooksPath` from the **global** config. On a
machine with a global hooks path, running the suite replaced the user's
global `commit-msg`/`prepare-commit-msg`/`pre-push` with hooks pointing
into the deleted temp repo (every later commit failed), and the saved
originals were deleted with the temp repo. The test should isolate
`HOME`/`GIT_CONFIG_GLOBAL`. Not changed in this PR.
2026-10-23 16:13:28 -04:00
MaxsTechReview 92ef850999 fix: Steam starts on ROMs that refuse the Android ID read (#1421)
On a Redmi K70 (Android 14), tapping Steam drops straight back to the
main screen. The session thread dies while writing the guest network
link:

```
java.lang.IllegalArgumentException: Illegal char: - at offset 8
at android.provider.Settings$Secure.getStringForUser(Settings.java:6486)
at com.droiddeck.launcher.runtime.LinuxNetworkLinkComponent.describe(LinuxNetworkLinkComponent.kt:281)
```

The guest MAC is seeded from `ANDROID_ID`. That ROM throws from the
read, which had no guard. A failed read now uses the same fallback seed
as a missing ID. Devices that can read it keep the MAC they have now.

- `SeededMacTest`: stable MAC from an ID, and a read that throws the
ROM's exception falls back instead of crashing.
- REDMAGIC NP06J: the guest MAC is the same before and after the change
(`16:4e:5b:53:e9:ef`).

## Credits
Reported by lc666 on Discord.
2026-10-23 16:08:53 -04:00
MaxsTechReview 64b94f7cdb fix: the app follows automatic landscape rotation again unless an orientation was picked (#1420)
Fixes #1348, fixes #1372, fixes #1388.

Since 0.6.0 the app ignores the sensor and stays in one landscape side,
even with auto-rotate on. #1287 changed the default orientation from
sensor landscape to fixed landscape, so everyone who never picked an
orientation in settings got locked.

The default is sensor landscape again. A picked orientation is still
saved and used as before.

| | Before | After |
| --- | --- | --- |
| No orientation picked | `SCREEN_ORIENTATION_LANDSCAPE`, no flip |
`SCREEN_ORIENTATION_SENSOR_LANDSCAPE`, follows the sensor |
| Orientation picked in settings | that orientation | that orientation |

Tested on REDMAGIC NP06J (Android 16) with no saved orientation:
`dumpsys activity` reports the requested orientation, and the app flips
with the device.
2026-10-23 16:08:02 -04:00
maxjivi05 06946fe8ae chore: drop comments 2026-10-23 16:01:41 -04:00
MaxsTechReview 36dc4014b7 fix: x86 Linux games under FEX keep LD_PRELOAD libraries given by name (#1419)
## Summary

`droiddeck-fex` kept an `LD_PRELOAD` entry only if `os.path.exists()`
found it. A bare soname that the loader resolves through
`LD_LIBRARY_PATH` doesn't exist relative to the working directory, so it
was dropped before the x86 game started. MelonLoader's bootstrap in
Drova is one example.

Names without a `/` are now passed through. Paths that don't exist and
arm64 libraries are still dropped, as before.

## Proof (REDMAGIC tablet, Stardew Valley on x86 Linux (FEX), launch
options `LD_PRELOAD=libz.so.1 %command%`)

| | before | after |
|---|---|---|
| `LD_PRELOAD` in the FEX game process | missing | `libz.so.1` |
| `libz.so` mappings in the game process | 4 (the arm64 host's) | 10
(the x86 one loaded too) |
| Game reaches its title screen | yes | yes |

## Tests

`tools/tests/test_droiddeck_fex.py` now has a bare soname in the
guest-environment case. It fails on main and passes here; all 51 tests
pass.

## Credits

Reported with the fix idea on the DroidDeck Discord by whatsupbros.
2026-10-23 16:00:27 -04:00
maxjivi05 3e85e4a614 chore: drop comments 2026-10-23 15:51:06 -04:00
maxjivi05 a4abb797a7 Merge remote-tracking branch 'origin/main' into fix/steam-compat-pick-wins 2026-10-23 15:51:00 -04:00
maxjivi05 b6ca673d6e fix: Steam starts on ROMs that refuse the Android ID read 2026-10-23 15:50:16 -04:00
maxjivi05 6378c3f3d2 fix: the app follows automatic landscape rotation again unless an orientation was picked 2026-10-23 15:48:03 -04:00
maxjivi05 f909c7a69c fix: x86 Linux games under FEX keep LD_PRELOAD libraries given by name
droiddeck-fex dropped every LD_PRELOAD entry that was not an existing path, so a bare soname the loader finds through LD_LIBRARY_PATH (MelonLoader's bootstrap in Drova, for one) never reached the game. Names without a slash are now kept; missing paths and arm64 libraries are still dropped.
2026-10-23 15:44:35 -04:00
Kurt Himebauch 2d59f03e7b fix: preserve GPU stats while pinning clocks (#1416)
Enabling GPU clock pinning froze Thor's kernel utilization counters,
leaving the HUD stuck at an arbitrary value (10–11% in the reproduced
sessions). KGSL_PROP_PWRCTRL disables both the governor and its
utilization sampling.

Use a dedicated KGSL context with a maximum-power constraint, renewed by
a small NOP submission every 25 ms. Keep devfreq running, bound the
queue to one submission, and release the context, mapped command buffer
and GPU allocation on stop or failure. Preserve recovery of legacy
global pins and report native setup failures instead of claiming
success.

Validation:
- AYN Thor: reproduced frozen counters with the old pin. The replacement
native implementation, run as a standalone ARM64 harness alongside the
installed Steam/Bulletstorm session, kept 680 MHz while utilization
changed from 17% in Steam to about 4% loading and 44% at the animated
title. A final-source smoke run also returned changing 60–66% samples at
680 MHz.
- Native lifecycle regression passed on Linux and as an ARM64 executable
on Thor: setup failure cleanup, renewal, bounded queue, runtime failure,
restart, and type-3/type-7 NOP selection for older/newer Adreno cores.
- Android native build and Kotlin compilation passed locally (NDK
27.0.12077973).
- CI build, Android tests, native regressions, and signing passed.
Installed the release-signed PR APK on Thor with app data preserved.
With GPU pin enabled, the normal Steam/Bulletstorm session held 680 MHz
while the HUD changed from 3% loading to 53% in the animated game scene
at 60 FPS; raw gpubusy samples also continued changing. This checks
utilization reporting, not long-term gameplay stability.

The earlier session exit was recorded by Android as LOW_MEMORY; this PR
addresses the utilization freeze.
2026-10-23 15:41:12 -04:00
XxJSONDeruloxX 9835c4e3e1 ci: persist PR caches and stabilize build time 2026-10-23 15:39:22 -04:00
XxJSONDeruloxX a7d6279ee3 test: check artwork key normalization 2026-10-23 15:22:12 -04:00
XxJSONDeruloxX 5a806b35f1 test: support configured artwork key 2026-10-23 15:22:01 -04:00
MaxsTechReview 8f0bd51fd8 Merge pull request #1401 from Droid-Deck/perf/ntsync-fast-paths
## Summary

droiddeck-ntsync (the userspace stand-in for the Linux ntsync driver,
Wine sync = ntsync) was slower than droiddeck-esync on every cheap
operation. On the tablet a `SetEvent` cost 0.94 µs against esync's 0.21
µs, and a wait on a signaled event 0.45 µs against 0.08 µs. Four things
made it slow:

- **Signal masking:** every operation blocked and restored the signal
mask, so each call made 2 `rt_sigprocmask` syscalls, and a blocking wait
made 4. The masking itself is needed: if Wine's suspend signal stopped a
thread while it held an object lock, a GC thread that suspended it and
then touched the same object would deadlock.
- **Queueing for objects that were already free:** a wait linked itself
onto every object and unlinked again even when the first object could
simply be taken.
- **Wakes nobody needed:** a futex wake syscall went out for every
handoff, even when the waiter was still spinning and would have seen the
change anyway.
- **One shared counter:** every call took and dropped a reference on a
single process-wide device counter, so threads working on unrelated
objects bounced one cache line between cores.

### Changes

- **Signals are deferred instead of masked:** handlers installed by
Wine's `ntdll.so` run behind a small trampoline. If such a signal
arrives inside a critical section (a per-thread depth count), it is
saved with its `siginfo` and sent again with `rt_tgsigqueueinfo`
(falling back to `tgkill`) as soon as the section ends.
  - `sigaction` queries still return Wine's own handler.
- Synchronous signals (SEGV, BUS, ILL, FPE, TRAP, SYS) are never
wrapped, as before.
  - Handlers installed by anything other than `ntdll.so` are left alone.
- **Wait-any fast path:** an object that is signaled and has nobody
queued on it is taken directly, in index order. A wait whose timeout has
already passed returns `ETIMEDOUT` without building a queue.
- **Short spin before sleeping:** a waiter spins for up to 4 µs, bounded
by time rather than by iteration count, before it sleeps on the futex.
- **Wake only sleeping waiters:** the waiter publishes a `sleeping`
flag, and the waker only calls `FUTEX_WAKE` when it is set. Both sides
use seq_cst ordering, so a wake can't be lost. The queue layout changed,
so `NS_VERSION` is now 2.
- **Per-thread device reference:** a thread takes one reference on the
device the first time it uses it and keeps it until it exits or moves to
another device, instead of two atomics on the shared counter per call.
- **More than four signals in one section:** a fifth distinct signal is
held pending with the thread's signal mask, then released once the
section ends. Its handler never runs while an object lock is held.
- **Replay:** deferred signals are replayed with every signal blocked,
so each one is sent exactly once. A held signal is released at the same
signal-nesting level that held it. Handlers installed with
`SA_RESETHAND` are not wrapped, so a one-shot handler keeps its kernel
behaviour.
- **Waits from a handler during another wait's sleep:** they get their
own queue and, for a second device, a reference for that call only. The
outer wait keeps its queue and can't pick up the inner wait's result.

### ntsync becomes the default

- **New installs:** start on ntsync.
- **Existing installs:** move to ntsync once, on the first start of this
version, whatever sync they were on before (a one-time `ntsyncDefaultV1`
marker). Anything picked after that is kept, including esync, fsync and
wineserver.
- **Wording:** the ntsync switch in Performance no longer says
"experimental" or "for testing", in all 33 languages.
- **Unchanged:** per-game sync choices on a game's settings page still
win over the app-wide choice. Proton builds without ntsync still fall
back to droiddeck-esync, then droiddeck-fsync.
- **Tests:** `SyncBackendTest` covers a fresh install, each earlier
choice moving to ntsync once, and a later choice surviving the next
start. All 2,160 unit tests pass.

## Performance (REDMAGIC tablet)

Windows microbenchmark (`syncbench.exe`, x86-64 under FEX), µs per
operation:

| | esync | ntsync before | ntsync after |
|---|---|---|---|
| SetEvent | 0.57 / 0.70 | 0.51 / 0.65 | 0.38 / 0.35 |
| wait on signaled event | 0.13 / 0.13 | 0.44 / 0.43 | 0.27 / 0.20 |
| wait, timeout 0 | 0.27 / 0.25 | 0.52 / 0.51 | 0.19 / 0.18 |
| semaphore release + wait | 0.72 / 0.68 | 0.66 / 0.63 | 0.12 / 0.16 |
| mutex wait + release | 0.77 / 0.73 | 0.66 / 0.64 | 0.12 / 0.12 |
| wait on 8 objects | 0.10 / 0.09 | 0.59 / 0.57 | 0.15 / 0.15 |
| alertable wait | 0.06 / 0.05 | 0.43 / 0.42 | 0.07 / 0.07 |
| event ping-pong, 2 threads | 25.6 / 24.5 | 24.4 / 29.3 | 1.97 / 1.66 |
| 4 threads on one semaphore | 9.4 / 4.6 | 20.3 / 19.1 | 7.3 / 7.6 |

The backend each run actually used was checked from its file
descriptors:
- **esync:** wineserver held eventfds.
- **ntsync:** wineserver held the `droiddeck-ntsync` device and its
timerfd objects.

Native arm64 benchmark against the device directly, µs (before / after):
- set: 0.31–0.36 → 0.06–0.08
- signaled wait: 0.37–0.45 → 0.04–0.08
- semaphore pair: 0.56–0.58 → 0.06–0.07
- mutex pair: 0.56–0.61 → 0.04
- ping-pong: 15–30 → 1.7–2.5

Removing the shared device counter on its own takes 4 threads working on
their own objects from 0.21 to 0.04 µs per operation (x86-64 host).

Games, default Steam launch, The Forest and PEAK at the same scene each
run, 20 s pacing. Columns are esync, ntsync before and ntsync after;
each cell gives two runs.

| | esync | ntsync before | ntsync after |
|---|---|---|---|
| The Forest Low, fps | 64.5 / 61.9 | 70.1 / 74.8 | 72.1 / 72.1 |
| The Forest Low, CPU | 489 / 511% | 434 / 419% | 436 / 426% |
| The Forest High, fps | 58.3 / 57.8 | 72.0 / 69.8 | 72.5 / 69.9 |
| The Forest High, CPU | 506 / 504% | 417 / 429% | 425 / 435% |
| PEAK, fps | 50.4 / 48.6 | — | 47.0 |
| RDR2, launch to signed in / game process | — | — | 29.2 / 42.0 s and
28.3 / 41.1 s |

Two more games, esync against ntsync after, three runs each, default
settings:

| | esync | ntsync after |
|---|---|---|
| Among Us, menu fps (59 cap) | 59.0 / 58.9 / 59.3 | 59.0 / 59.0 / 58.7
|
| Among Us, launch to main menu | 6.65 / 6.84 / 6.85 s | 6.39 / 6.52 /
6.64 s |
| Among Us, CPU | 123 / 132 / 133% | 133 / 133 / 124% |
| Mortal Kombat X attract fight, fps | 56.7 / 54.1 / 56.3 | 61.1 / 56.9
/ 56.2 |
| Mortal Kombat X, 1% low | 20.0 / 30.3 / 30.3 | 30.3 / 30.3 / 30.3 |
| Mortal Kombat X, launch to window | 7.75 / 7.79 / 7.75 s | 7.81 / 7.71
/ 7.63 s |

Mortal Kombat X's CPU moves between 140% and 275% in both modes because
the attract mode changes fighters and stage, so it isn't compared.

The Forest's 1% low was 41.7 fps in every run. PEAK is GPU-bound and
stays in run-to-run noise. Its second ntsync run stopped at the title
menu because the rig's menu input didn't register; the game was still
running at 87 fps, and the same rig miss happened on esync before. RDR2
signs in and reaches the game on the new code.

## Tests

- **`tools/tests/test_ntsync_preload.py` (new):** builds the preload and
checks the emulation against the semantics documented for the kernel
driver:
  - semaphore counts and `EOVERFLOW`;
- mutex owners, recursion, `EPERM`, kill/abandon with `EOWNERDEAD` on
read and on the next wait;
  - auto and manual events, set/reset/pulse previous state;
- wait-any index order, duplicates, the alert object, `EINVAL` limits,
timeouts;
  - wait-all atomicity, duplicates and alert;
- wakes across threads and across processes (inherited fds and fds sent
with `SCM_RIGHTS`);
- 20,000-round ping-pong and 4-thread semaphore stress with infinite
timeouts;
- two signal cases. In one, a handler that re-enters the same object is
fired 2,000 times at a busy thread. In the other, a handler parks the
thread while another thread uses the same object, as Wine's suspend does
under a GC.
- eight real-time signals fired in bursts at a thread busy on one
object, each handler touching that object, so more signals arrive inside
a section than can be deferred;
- a handler that waits on the same device and on a second device while
its thread sleeps in another wait;
- a one-shot (`SA_RESETHAND`) handler installed and fired 300 times at a
busy thread, every one of which must run.
- **The signal tests catch a broken implementation:** with signal
protection removed, the program hangs and the test fails. Before the
last fix, the burst test hung and the outer wait returned the nested
wait's result.
- **The suite passes:** 8 of 8 runs natively on arm64 under proot on the
tablet, and 40 of 40 on the host.
- **On the final build:** The Forest Low ran at 77.7 fps, PEAK at 47.5
fps, and Among Us and two Mortal Kombat X runs played through.
- **The new paths stay out of the way in games:** a build that logged
them saw about 1,400 Wine signals land during ntsync sleeps over two
Mortal Kombat X runs and one The Forest run. None of those handlers
waited on ntsync and no signal was held back, so games run the same code
as before the last fix. The Forest Low ran at 71.6 fps and PEAK at 47.2
fps on the final build.
- **Existing preload tests still pass:** fsync, openat2, usr2 (also with
`BL_SYNC=1`), tracer, dircache, noexec, binfmt, faultreport and esync
packs.

## Credits

- [Linux](https://github.com/torvalds/linux)
`Documentation/userspace-api/ntsync.rst` (GPL-2.0): the object, wait and
error semantics the new test checks. Also added to the README credits.
2026-10-23 15:10:28 -04:00
XxJSONDeruloxX 6cc289f7d3 test: avoid GPU pin scheduling races 2026-10-23 15:06:27 -04:00
XxJSONDeruloxX 9c66a1ac0a fix: select NOP packets for older Adreno cores 2026-10-23 15:04:50 -04:00
XxJSONDeruloxX 556bd96d5a ci: reuse build metadata and verified pack downloads 2026-10-23 15:01:04 -04:00
XxJSONDeruloxX 4d86839910 fix: preserve GPU stats while pinning clocks 2026-10-23 15:00:11 -04:00
Kurt Himebauch 3d9d90d832 ci: consolidate verified runtime components
## Summary

Consolidate runtime preparation into a component registry and reusable
recipes, and reuse approved preload binaries with independent main-build
provenance checks.

- Pin the source tree once. Components, Xbox and Python/native tests
then run independently against that exact tree; APK compilation consumes
verified component artifacts.
- Centralize component inputs, outputs, tool requirements and build
commands under `tools/components`. Cache identities derive from that
registry. Install required Linux tools in one preflight and pin the same
Android NDK across workers.
- Keep small preload input/output manifests in Git. Store the 1.48 MB
binary snapshot in an immutable content-addressed release, rather than
committing executable blobs.
- New or PR-modified manifests must match binaries in an authenticated
successful main APK artifact and its producing source/recipe. Changed
inputs rebuild; missing or temporarily unavailable artifacts rebuild;
corrupt artifacts fail.
- Record the source tree, recipe identities and output hashes in the
components-to-APK handoff. Reject mismatched inventories, traversal and
symlinks.
- Extract component and release shell bodies out of workflow YAML.
Clarify the trusted validation job label. Preserve legacy
workflow/status and gate names consumed by branch rules and trusted main
workflows.

```mermaid
flowchart LR
  Source[Pin source tree] --> Components[Prepare verified components]
  Source --> Xbox[Xbox ARM64 backend]
  Source --> Tests[Python and native tests]
  Components --> APK[APK and Android tests]
  Xbox --> APK
  APK --> Gate[Required aggregate gate]
  Tests --> Gate
  Gate --> Publish[Main only signing and publishing]
```

## Validation

The initial preload implementation passed both [PR
CI](https://github.com/Droid-Deck/DroidDeck/actions/runs/39999620253)
and a [forced source
rebuild](https://github.com/Droid-Deck/DroidDeck/actions/runs/39999657706).
Snapshot restore took 3 seconds and skipped the ARM64 compile; the
forced run compiled both groups. These runs precede the final pipeline
consolidation.

Local clean Linux validation passed 1,142 tests (17 skipped) on the
consolidation commit; the follow-up x86 recipe check passed all 15
focused snapshot tests. Actionlint and focused preparation, NDK,
source-pinning and gate tests passed. Final head d32b1a13 passed [PR
reuse
validation](https://github.com/Droid-Deck/DroidDeck/actions/runs/40004459466)
and a [forced source
rebuild](https://github.com/Droid-Deck/DroidDeck/actions/runs/40004459597);
both full runs are green. PR push-to-green was 19m00s, including 57s
initial queue, a cold Xbox job of 5m48s (compile 5m30s), 50s component
preparation (snapshot restore 8s), APK build 5m59s and Android tests
3m03s. The forced run passed on attempt 2 after a connection reset in
the unchanged pack fetch on attempt 1; rerun APK build 5m24s and tests
2m57s. Both runs verified the prepared artifact handoff and APK/ABI
contents. This is correctness evidence, not a controlled warm-cache
speed benchmark. All existing source checks, regression tests, APK/ABI
checks and signing/publication gates are retained. `workflow_dispatch`
with `rebuild_preloads=true` exercises actual source compilation.

## Trust and scope

The initial snapshot covers 14 core ARM64/x86 preload binaries. It
originates from successful main run
[39992155294](https://github.com/Droid-Deck/DroidDeck/actions/runs/39992155294),
commit `b76f13f7acc0af0cc00369dde61fb88e40529e6d`, APK artifact
`12520502054`; its ZIP SHA-256 is
`b7e800a1884a516ec52b050b7f904f9881427bca4579f660b909fd18f15e49ff`.

This is approved-build reuse, not a signed SLSA attestation or a claim
of reproducible compilation. The pinned-main manifest is the approval
anchor. New manifests require the original main artifact to remain
available for independent verification; otherwise builds fall back to
source. Compiler/system package updates within the same Ubuntu runner
label require a deliberate refresh or forced rebuild. Other components
retain their existing release pins or caches behind registered recipes.

The within-run artifact handoff verifies source/output consistency; it
is not independent provenance for arbitrary artifacts. Main-only
publishing cannot be exercised by a branch dispatch. No controlled
end-to-end speed claim is made before final runs complete. Leave this PR
open for review.
2026-10-23 14:58:10 -04:00
maxjivi05 9438aac0db fix: droiddeck-ntsync leaves one-shot handlers alone and replays deferred signals exactly once
- Handlers installed with SA_RESETHAND are no longer wrapped, so a deferred one-shot signal can't fall through to the default action.
- Deferred signals are replayed with every signal blocked, so a signal landing mid-replay can't send an entry twice.
- Taking a deferral slot and holding a signal are safe when one signal interrupts another, and a held signal is released only at the nesting level that held it, so a handler's return can't block it again.
2026-10-23 14:55:33 -04:00
coffeemeist3r 5d1e70cd20 Keep FEX's TSO on for Unity games
A FEX preset that turns TSO off (Performance, Extreme, Extreme-gn, Denuvo)
breaks Unity players under FEX:
- Megabonk (Unity 2023.2, IL2CPP, native Linux build, "Linux x86 (FEX)")
  froze on about half of its launches, every thread parked on a futex.
  With TSO on: 5 of 5 starts, and a played run stayed responsive.
- Crab Game (Windows IL2CPP build under Proton arm64) crashed on 1 of 5
  starts with an access violation in UnityPlayer.dll on a garbage pointer.
  With TSO on: 5 of 5 starts.

A Unity player (UnityPlayer.dll, UnityPlayer.so or GameAssembly.so beside
the executable) now keeps FEX_TSOENABLED=1 over the shared preset, the way
.NET games already keep full TSO. The FEX tool applies the environment half
of the game-file fixes too, so native Linux builds get it as well. A FEX
preset chosen for the game itself still wins.

Related: #1386 (its Mono crash happens with TSO already on)
2026-10-23 13:38:59 -05:00
XxJSONDeruloxX d32b1a13fd ci: verify x86 container build recipe 2026-10-23 14:34:44 -04:00
XxJSONDeruloxX d7bf0b274f ci: consolidate verified component preparation 2026-10-23 14:31:13 -04:00
XxJSONDeruloxX d111b2b3dd ci: reuse preload snapshots verified against main builds 2026-10-23 13:59:04 -04:00
maxjivi05 293e84a64c feat: droiddeck-ntsync is the default Wine sync
- New installs start on ntsync.
- Existing installs move to ntsync once on the first start of this version, whatever sync they used before. A choice made after that is kept.
- The ntsync switch is no longer labelled experimental or for testing, in every language.
2026-10-23 13:57:38 -04:00
Kurt Himebauch 16bedf3635 fix: keep the UBWC hint consistent after custom guest environment 2026-10-23 13:54:01 -04:00
Kurt Himebauch e06b942e67 fix: warn about UBWC restart only after a hint is active 2026-10-23 13:54:01 -04:00
Kurt Himebauch 2e1156038e test: cover the inclusive A740 blob 700 boundary 2026-10-23 13:54:01 -04:00
Kurt HimebauchandVabian124 dccd5a7c69 fix: match the A740 UBWC hint on driver 512.676.76
Co-authored-by: Vabian124 <82999916+Vabian124@users.noreply.github.com>
2026-10-23 13:54:01 -04:00
Kurt Himebauch fc768a3c45 feat: add an A740 UBWC compatibility override 2026-10-23 13:54:01 -04:00
XxJSONDeruloxX a4447039b9 ci: install native Vulkan and ARM64 C headers for tests 2026-10-23 13:48:55 -04:00
XxJSONDeruloxX 8355c1495b ci: bound apt phases and retry the official Ubuntu mirror 2026-10-23 13:48:55 -04:00
XxJSONDeruloxX fcf4c09c50 test: verify dependency installation fails closed 2026-10-23 13:48:55 -04:00
XxJSONDeruloxX 74d5daaa5d ci: parallelize regression checks and isolate component caches 2026-10-23 13:48:55 -04:00
0x1Nawaf 7c51e6e904 fix: draw the gamescope cursor only when that option is on
The extra plane costs frames on a phone, so compositing stays off unless Performance asks for it.
2026-10-23 13:30:42 -04:00
Kurt Himebauch b76f13f7ac fix: keep the shader pre-caching choice until a session confirms it, and read it from its own VDF section 2026-10-23 13:05:04 -04:00
Kurt Himebauch f88227ed74 feat: toggle Steam shader pre-caching from DroidDeck's Steam settings 2026-10-23 13:05:04 -04:00
Kurt Himebauch f683ca1dea fix: show the paused card already settled, skip it on auto resume, and cancel manual pauses on a quick return 2026-10-23 12:28:01 -04:00
Kurt Himebauch 6b6d7d2ac7 feat: lift paused frame into a grey card with a glass resume button 2026-10-23 12:28:01 -04:00
maxjivi05 77dd04ec8c fix: droiddeck-ntsync keeps protection when more than four signals arrive and keeps waits intact when a handler waits again
- A fifth distinct signal inside a critical section is now held pending with the thread's signal mask and released once the section ends, instead of running its handler while an object lock is held.
- A wait from a signal handler during another wait's sleep uses its own queue and, for a second device, a reference for that call only, so the outer wait keeps its queue and can't return the inner wait's result.
2026-10-23 12:15:43 -04:00
ovurrsl 70eec3cc32 fix: a half-built steamrt shim is removed, and its launcher accepts --directory DIR 2026-10-23 12:03:41 -04:00
ovurrsl 2eb18f5540 fix: document which Steam client builds Valve's arm64 manifests carry 2026-10-23 12:03:41 -04:00
ovurrsl 7aeb7779bb fix: fall back to Valve's stable Steam client when the beta one keeps failing
The native arm64 client follows steamdeck_publicbeta, so a Valve beta
regression reaches every user at once. If the repaired beta client is still
started twice without being shown, or the beta manifest carries no arm64
client, droiddeck-steam-install now installs steamdeck_stable, keeps it, and
logs why (listing the manifest's blocks in the second case). The session asks
the installer for the channel, so -clientbeta and package/beta stay in sync.
BL_STEAM_CHANNEL or ~/.config/droiddeck/steam-channel (beta|stable) pins it.

Valve's 2026-10-15 beta moved the x86 client into a Steam Runtime container.
In case the arm64 client follows, its session now exports
STEAM_STEAMRT_RECURSION_GUARD=1 and, only if steamrtarm64 carries pv-runtime
or a steam-runtime-check-requirements probe, starts it through the same
steamrt shim the x86-64 client uses (now droiddeck-steamrt-shim) and logs it.
2026-10-23 12:03:41 -04:00