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).
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.
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).
The Steam helper checked the Frame Limit only when the offered frame
rates changed. With the limit off during a refresh change, turning it
back on left a value the new rate does not offer, and a failed write was
not tried again. It now checks on every state change and every 5 s, and
waits for Steam's settings write so a rejected write is caught and tried
again on the next check.
gamescope 3.16.29-p16 (0120 changed): gamescope took a new host refresh
only from the presentation feedback of a frame it drew, so with nothing
moving on screen it kept the old rate. It now presents once when the
host's wl_output mode changes and keeps presenting while a new rate is
pending, so the 300 ms check runs on a still screen too.
Found by CodeRabbit in the reviews of #1650 and #1663.
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.
A capture format or buffer change now closes the app's decoder so the
next packet reopens it with the new stride and height, and a capture
slot whose remap fails no longer keeps its old mapping.
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.
skate. under ReSkate crashed with an access violation in Skate.exe 12 to
60 seconds after it started at the default feature level 12_2; at 12_1
it loads San Vansterdam and plays. The rule matches Skate.exe with
ReSkate.dll beside it, so it covers ReSkate started from skate.'s own
Steam entry and as a non-Steam game, and a game profile can still set
another level.
Proton sets WINE_CPU_TOPOLOGY from its own default_cpu_limit for games
like Prototype (8 cores) unless PROTON_CPU_TOPOLOGY is set, so the
pinned topology never reached the game the issue was about.
Game cores pinned the game with taskset, but Wine counts the online
CPUs, so a game held to two cores still saw eight processors and sized
its thread pools for them. Prototype freezes at its splash screen that
way. When the session or a game profile limits the game cores, Wine now
gets WINE_CPU_TOPOLOGY for exactly those cores unless the profile sets
its own.
The Crew 2 loads D3DGear64.dll from its folder. Its DXGI Present hook
faults over a thousand times within seconds under Wine and the game
never presents again: black screen while the sound keeps playing.
droiddeck-game-env now disables the DLL when it finds it beside the
game, and DLL overrides from several engine fixes are kept together
instead of the last one replacing the others.
Steam's native QAM sliders now control Android brightness and media
volume, with device changes reflected back in Steam. Holding Android
Back for 700 ms sends a real held QAM button for Steam shortcuts. Single
and double taps keep the existing menu/QAM order and swap setting; the
double-tap window remains 500 ms.
- Discover native brightness through a guest backlight descriptor and
use Android's Modify system settings permission. Retain the latest
pending request until permission is granted or the session stops, with a
notification action for hidden sessions.
- Give DirectAudio and fallback AAudio sinks Android media volume/mute
callbacks, keeping software gain at unity. Recover when initial volume
state is absent or invalid, brightness acknowledgements differ or never
arrive, or the Display bridge is not ready yet.
- Forward other buttons while QAM is held in Steam Deck and Steam
Controller modes. Consume release after a hold and clear pending input
on overlays, focus loss, pause, disconnect, and teardown. The session
menu retains its compact Back choice without an explanatory paragraph.
Validation:
- Local ARM64 release build passed. The latest timing change passed 20
targeted input tests, including release at 600 ms remaining a tap. The
preceding full feature check passed 61 Android input/device-control
tests, with permission cases on API 28 and 33.
- 66 Linux Python/Node/C tests passed for device controls, audio
recovery, Steam compatibility, and file dialogs. Both audio sink modules
rebuilt with Android NDK 27.2.
- Release-signed integration build 14d12cf0 is installed on the unrooted
AYN Thor, carrying the Android/input changes through 749ade4a plus the
existing portrait changes. Device checks confirmed the configured
QAM/menu tap order, Back dismissal, removal of the inline menu
description, and held QAM overlapping X with clean release. Hands-on
testing confirmed this installed build works.
- Earlier Thor testing covered brightness permission and pending
requests, brightness synchronization in both directions, native volume
changes, hardware volume key readback, and both sink backends. Failure
recovery is covered by the automated tests above.
The subsequent queued-notification fix in 4d7ceb23 is covered by the
Linux regression suite and has not replaced the Thor install.
Bluetooth/wired output switching and other device models remain
untested. CI and CodeRabbit monitoring continue against the pushed head.
A tag that matches no longer vouches for the build: FEX and FEXServer must
also load (ld.so --list), otherwise the session reinstalls it and x86
programs use the Steam or system FEX meanwhile.
A failed catalog install is retried after an hour or when the session names
another build, a missing updater falls back to the FEX already here, and the
archive download gives up on a stalled connection.
The Sentry GDExtension was turned off for every Godot game that ships it,
because its crash handler takes the .NET runtime down under FEX. GDScript
games such as Pathogenic call SentrySDK from their scripts, so without the
extension their autoloads fail to parse and the game quits. Only Godot .NET
builds have it turned off now.
The launch view lives under the app's files folder, so games saw an
install path like Z:\data\user\0\com.droiddeck.launcher\files\xbox-runtime
\views\launch-<n>\droiddeck-xbox-<id>. Forza Horizon checks that length and
quits with "Installation path is too long". The view is now mapped to a
free drive letter in the prefix (X: first) for the length of the launch.
Once Blizzard's Agent has removed a game it answers GET /game/<uid> with
an empty object, not installed false, so a finished uninstall read as a
failure and opened Battle.net.
A cancelled install now waits for its Agent job to end before the
uninstall runs, and removes the folder only once the Agent no longer
lists the game or the folder is empty. Uninstall gives up when Battle.net
or a game keeps the prefix busy for a minute, so the page falls back to
opening Battle.net. An uninstall only counts when the Agent reports the
game as not installed.
Battle.net games now install like Epic, Xbox and itch games: Install,
pick a location, and the download runs in the Downloads tab while the
Stores page stays open. droiddeck-battlenet starts Blizzard's own Agent
on its own under a headless gamescope and drives its local API, so no
window opens and nothing waits on the Battle.net client.
- Progress, speed and time left come from the Agent, with pause, resume
(the Agent keeps what it downloaded) and cancel, which uninstalls the
partial game and removes its folder.
- A job only finishes once the Agent has really run it and the game is
playable; the first status after a request can still describe the
previous operation.
- Installed games check Blizzard's version service and show Update when
the build on disk is behind; the update repairs missing files too.
- Battle.net games can be uninstalled from their page through the Agent.
- Games land in the chosen location: internal storage, other storage or
a Games folder.
- The hero keeps its action row clear of the playtime column.
Steam games and Battle.net started from the KDE desktop run on the GPU in gamescope, opened as a KDE window, and Plasma no longer leaves programs with a zero realtime CPU limit that got games killed.
The flow solver starts at flowFinest_, but its images, uniform buffers and descriptor sets were allocated for every pyramid level. Keep the seven-level indices and allocate flow resources only where the solver reads and writes them.
The default preset avoids three unused RGBA16F images, saving 38,707,200 bytes of pixel payload at 2560x1440 without changing the shaders or recorded GPU work. Add native allocation/dispatch regressions and document the resource budgets.