Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions .github/workflows/build-proton.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ jobs:
"type": "Proton",
"versionName": "11.0-2-${ARCH_NAME}",
"versionCode": 1,
"description": "Proton 11.0-2 ${ARCH_NAME} (bionic) — stock Valve + userspace ntsync + fsync + Android fixes. SDK 28 + 16KB pages. Needs a fresh arm64ec container.",
"description": "Proton 11.0-2 ${ARCH_NAME} (bionic) — stock Valve + runtime ntsync (kernel driver, userspace fallback) + fsync + Android fixes. SDK 28 + 16KB pages. Needs a fresh arm64ec container.",
"files": [],
"wine": {
"binPath": "bin",
Expand All @@ -222,7 +222,7 @@ jobs:
"type": "Wine",
"versionName": "11.0-2-${ARCH_NAME}",
"versionCode": 0,
"description": "Proton 11.0-2 ${ARCH_NAME} (bionic) — stock Valve + userspace ntsync + fsync + Android fixes. SDK 28 + 16KB pages. Needs a fresh arm64ec container.",
"description": "Proton 11.0-2 ${ARCH_NAME} (bionic) — stock Valve + runtime ntsync (kernel driver, userspace fallback) + fsync + Android fixes. SDK 28 + 16KB pages. Needs a fresh arm64ec container.",
"files": [],
"wine": {
"binPath": "bin",
Expand Down Expand Up @@ -331,23 +331,18 @@ jobs:
Single SDK 28 build with 16KB page size support (works on Android 9+ and Android 15+).

### Features
- Experimental userspace ntsync (via [ntsync-android](https://github.com/GameNative/ntsync-android), commit [`7ce6435`](https://github.com/GameNative/ntsync-android/commit/7ce6435e5979b1cb5341aa4b299f31e8937fe121)) + fsync, with wineserver-side `ntsync_event_set` support
- Runtime ntsync backend selection: kernel `/dev/ntsync` when present and usable (probed by wineserver at startup), otherwise experimental userspace ntsync via [ntsync-android](https://github.com/GameNative/ntsync-android) (commit [`7ce6435`](https://github.com/GameNative/ntsync-android/commit/7ce6435e5979b1cb5341aa4b299f31e8937fe121)) — plus fsync and wineserver-side `ntsync_event_set` support; `PROTON_NO_NTSYNC=1` disables ntsync entirely
- `WINE_FAST_YIELD` fast-yield hook in `NtYieldExecution`
- FEX integration (stats shm, `SkipThreadAttach` detach guard) via bylaws' arm64ec patches
- FEX unixlib loader: `MemoryWineLoadUnixLibByName` / wow64 / unload support with `$PREFIX/lib/wine` fallback
- `WINEVMEMMAXSIZE` virtual-address cap for FEX environments
- Integrated lsteamclient built into Proton (supports GameNative bionic Steam)
- Stripped, `-g0 -O2` build: smaller tree, faster install (zstd-packed `.wcp`)

### ntsync-android changelog ([`d00ac47`..`7ce6435`](https://github.com/GameNative/ntsync-android/compare/d00ac47491f2a5505d2ea3a2c8533f498a9811b0...7ce6435e5979b1cb5341aa4b299f31e8937fe121))
- **Adaptive spin-before-block (`NTSYNC_SPIN_ITERS`, default off)** — a waiter spins briefly on a read-only acquire check before sleeping on the futex; a signal landing in the window avoids a sleep+wake pair entirely. Per-thread adaptive credit (JVM-style) self-throttles threads with long waits. Measured on device: Persona 5 Royal avoids 25-28% of blocked-wait sleeps during sync bursts (60-76% spin win rate); Dishonored / Lies of P / Hades II correctly self-throttle to ~0.2% CPU. See the README per-game tuning guide.
- **Wait-path trims + sweep-trigger RSS fix** — WAIT_ALL rollback no longer zeroes 1 KB of stack per call; auto-sweep triggers moved off the lock-free signal ops into wait paths only (a dead-process scan can never land on a latency-critical signaler); bounded sweep scans fix a sparse-page RSS spike (664 kB -> 112 kB when a sweep ran).
- **Auto-sweep of dead processes' objects (layout v9)** — wait paths run a rate-limited region-wide sweep (default 30 s, `NTSYNC_SWEEP_INTERVAL_SEC` to tune, `0` to disable), replacing manual `ntsync_sweep_dead()` reliance; the stuck-wait debug dump shows creator pid liveness. Fixes a plausible cause of 60 s `RtlpWaitForCriticalSection ... blocked by 0000` stalls after a Wine process crashes.
- **Wait-path CPU reductions** — wake-latency instrumentation is now debug-gated, `futex_wake` wakes exactly one waiter, and all per-wait heap allocations became fixed stack arrays. Measured vs. baseline: cross-process RPC **-17%**, multi-process contention **-25%**, Wine mixed load **-8%** CPU per op.
- **Lazy region init (layout v8)** — the ~960 KB shm file is no longer bulk-zeroed at init: mapped RSS drops from 976 kB to ~16 kB (sparse pages, negligible flash writeback), growing only as objects/waiters actually arrive.
- **`NTSYNC_NO_SIGUSR1_BLOCK=1` opt-out** — skips the two `pthread_sigmask` syscalls per region-lock acquisition (~13% per-op under multi-process contention). Default stays on; only safe outside Wine.
- **Wine-workload benchmark suite** — 7 benches (shm footprint, signaled fast path, cross-process RPC, mixed load, fork/exec multi-process contention) with a JSON baseline for regression gating.
- **On-device stats (`NTSYNC_DEBUG=1`)** — per-process counters (waits, fast-path hits, sleeps, wake walks, wake latency, spin wins/exhaustion) dumped every 10 s; validated at scale in four games with zero lock contention and zero EAGAIN churn.
### ntsync changes in this build
- **Runtime kernel/userspace backend detection (proton-wine side)** — ntsync is no longer hard-selected at compile time. wineserver probes `/dev/ntsync` at startup with a real `NTSYNC_IOC_CREATE_EVENT` ioctl (catches SELinux/seccomp-blocked nodes): if the kernel driver is present and usable it is used directly; otherwise the build falls back to the userspace ntsync-android shm backend. Backend is chosen once per session and applies to all processes.
- **No shm region on kernel-ntsync devices** — the dead-process sweep is now gated on the userspace backend being active, so the ~960 KB shm file is never initialized when the kernel driver handles ntsync.
- ntsync-android library is unchanged since the last build ([`7ce6435`](https://github.com/GameNative/ntsync-android/commit/7ce6435e5979b1cb5341aa4b299f31e8937fe121)); `PROTON_NO_NTSYNC=1` still disables both backends.

### Android / bionic fixes
- Anonymous PE-image mapping for bionic (vc4 devices); `LC_ALL=C.UTF-8` default; shell32 drive-root path fix
Expand Down
56 changes: 40 additions & 16 deletions android/ntsync_android/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# ntsync_android integration

Userspace replacement of the Linux `ntsync` kernel driver for Android
(no `/dev/ntsync` exists there). The library source lives in a separate
project:
(devices without a usable `/dev/ntsync`). The library source lives in a
separate project:

- Local: `../ntsync-android` (i.e. `$PROJECT_ROOT/../ntsync-android`)
- Upstream: https://github.com/joshuatam/ntsync-android
Expand All @@ -12,6 +12,12 @@ project:
`android/patches/common/*ntsync*` include it via a relative path, like
`android/shm_utils`.

`ntsync_kernel_abi.h` carries the kernel `/dev/ntsync` ioctl request
numbers (frozen uapi, mirroring `include/uapi/linux/ntsync.h`) on top of
the same struct definitions from `ntsync_user.h`. It lets wineserver/ntdll
compile in **both** the kernel-ioctl and the userspace implementation and
pick one at runtime.

## Building the library

The build scripts (`build-scripts/build-step-arm64ec.sh`,
Expand All @@ -22,18 +28,36 @@ step which:
else clones the GitHub repo into `android/ntsync-android/`.
2. Runs its `build-scripts/build-android.sh --build` (Rust/cargo + NDK,
always 16KB-aligned).
3. Copies the matching `libntsync_android.so` for the ABI being built into
`$deps/lib/` and the header into `$deps/include/`.

The resulting `libntsync_android.so` must be shipped alongside Wine
(on `LD_LIBRARY_PATH` / in the Termux prefix libdir, same as
`libandroid-sysvshm.so`).

## Runtime

All Wine processes attach to the same shared-memory region
3. Copies the matching `libntsync_android.a` for the ABI being built into
`$deps/lib/` (static archive; linked directly into ntdll/wineserver).

## Runtime backend selection

wineserver probes for in-process sync support once at startup
(`get_inproc_device_fd()` in `server/inproc_sync.c`):

1. `PROTON_NO_NTSYNC=1` disables ntsync entirely (both kernel and
userspace); wineserver falls back to server-side synchronization.
2. Otherwise it opens `/dev/ntsync` and **probes** it with a real
`NTSYNC_IOC_CREATE_EVENT` ioctl (the node can exist but be unusable,
e.g. SELinux policy or seccomp). If the probe succeeds, the kernel
driver is used, exactly like upstream Proton.
3. If the device is missing or the probe fails, it tries
`ntsync_init()`; on success all processes attach to the shared-memory
region and use the userspace implementation
(`ntsync: no usable /dev/ntsync, using userspace ntsync.`).
4. If that also fails, wineserver falls back to server-side
synchronization.

wineserver tells each client which backend is in use through the
`init_first_thread` reply: a passed device fd (kernel), the
`NTSYNC_ANDROID_USED_BY_SERVER` sentinel (userspace), or nothing. ntdll
compiles in both implementations and dispatches per call at runtime;
there is no per-object mixing — the whole server session uses one backend.

In userspace mode the integer object handles are passed from wineserver to
clients in the `fsync_shm_idx` field of the `get_inproc_sync_fd` /
`get_inproc_alert_fd` replies instead of `SCM_RIGHTS` fd passing. All Wine
processes attach to the same shared-memory region
(`$TMPDIR/ntsync_userspace.shm`); make sure `TMPDIR` is exported (Termux
does this by default). The integer object handles are passed from
wineserver to clients in the `fsync_shm_idx` field of the
`get_inproc_sync_fd` / `get_inproc_alert_fd` replies instead of
`SCM_RIGHTS` fd passing.
does this by default).
52 changes: 52 additions & 0 deletions android/ntsync_android/ntsync_kernel_abi.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Kernel /dev/ntsync ioctl ABI, for runtime kernel-vs-userspace detection.
*
* Copyright (C) 2026 Joshua Tam <297250+joshuatam@users.noreply.github.com>
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, version 3 only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <https://www.gnu.org/licenses/>.
*
* Mirrors the ioctl request numbers from the Linux uapi header
* (include/uapi/linux/ntsync.h); those numbers are frozen kernel ABI.
* The struct layouts come from ntsync_user.h, which mirrors the same uapi
* structs, so this header only adds the ioctl numbers. uint32_t stands in
* for the uapi __u32 (identical size, hence identical ioctl numbers).
*
* Used on Android, where /dev/ntsync may or may not exist and be usable:
* wineserver/ntdll compile in both the kernel-ioctl and the userspace
* implementation and pick one at runtime.
*
* SPDX-License-Identifier: LGPL-3.0-only
*/
#ifndef NTSYNC_KERNEL_ABI_H
#define NTSYNC_KERNEL_ABI_H

#include <sys/ioctl.h>

#include "ntsync_user.h"

#define NTSYNC_IOC_CREATE_SEM _IOW ('N', 0x80, struct ntsync_sem_args)
#define NTSYNC_IOC_SEM_RELEASE _IOWR('N', 0x81, uint32_t)
#define NTSYNC_IOC_WAIT_ANY _IOWR('N', 0x82, struct ntsync_wait_args)
#define NTSYNC_IOC_WAIT_ALL _IOWR('N', 0x83, struct ntsync_wait_args)
#define NTSYNC_IOC_CREATE_MUTEX _IOW ('N', 0x84, struct ntsync_mutex_args)
#define NTSYNC_IOC_MUTEX_UNLOCK _IOWR('N', 0x85, struct ntsync_mutex_args)
#define NTSYNC_IOC_MUTEX_KILL _IOW ('N', 0x86, uint32_t)
#define NTSYNC_IOC_CREATE_EVENT _IOW ('N', 0x87, struct ntsync_event_args)
#define NTSYNC_IOC_EVENT_SET _IOR ('N', 0x88, uint32_t)
#define NTSYNC_IOC_EVENT_RESET _IOR ('N', 0x89, uint32_t)
#define NTSYNC_IOC_EVENT_PULSE _IOR ('N', 0x8a, uint32_t)
#define NTSYNC_IOC_SEM_READ _IOR ('N', 0x8b, struct ntsync_sem_args)
#define NTSYNC_IOC_MUTEX_READ _IOR ('N', 0x8c, struct ntsync_mutex_args)
#define NTSYNC_IOC_EVENT_READ _IOR ('N', 0x8d, struct ntsync_event_args)

#endif /* NTSYNC_KERNEL_ABI_H */
10 changes: 7 additions & 3 deletions android/patches/common/dlls_ntdll_unix_server_c.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/dlls/ntdll/unix/server.c b/dlls/ntdll/unix/server.c
index 112157b0dd7..d74c6a32b27 100644
index 112157b0dd7..ff51533dbc7 100644
--- a/dlls/ntdll/unix/server.c
+++ b/dlls/ntdll/unix/server.c
@@ -83,6 +83,10 @@
Expand Down Expand Up @@ -30,15 +30,19 @@ index 112157b0dd7..d74c6a32b27 100644
}
else if (errno != EEXIST) fatal_perror( "cannot create %s/dosdevices", config_dir );

@@ -1703,6 +1714,13 @@ size_t server_init_process(void)
@@ -1703,6 +1714,17 @@ size_t server_init_process(void)
inproc_device_fd = FSYNC_USED_BY_SERVER;
fsync_init( pid );
}
+#ifdef __ANDROID__
+ /* userspace ntsync: no device fd; attach to the shared region */
+ else if (reply->inproc_device == NTSYNC_ANDROID_USED_BY_SERVER)
+ {
+ if (!ntsync_init( NULL )) inproc_device_fd = 0;
+ if (!ntsync_init( NULL ))
+ {
+ inproc_device_fd = 0;
+ ntsync_userspace = 1;
+ }
+ }
+#endif
else if (reply->inproc_device)
Expand Down
Loading
Loading