From 45591fda149b8605dfd4b565fc6a0880edf3fc54 Mon Sep 17 00:00:00 2001
From: Joshua Tam <297250+joshuatam@users.noreply.github.com>
Date: Mon, 31 Aug 2026 23:54:06 +0800
Subject: [PATCH 1/2] ntsync: runtime kernel/userspace backend detection
instead of compile-time #ifdef
Replace the hardcoded __ANDROID__ userspace-ntsync selection with runtime
detection: wineserver opens /dev/ntsync and probes it with a real
NTSYNC_IOC_CREATE_EVENT ioctl (catches SELinux/seccomp-blocked nodes);
if the device is missing or unusable it falls back to the userspace
libntsync_android shm backend. PROTON_NO_NTSYNC=1 still disables both.
- add android/ntsync_android/ntsync_kernel_abi.h: frozen kernel uapi
ioctl numbers, pairs with ntsync_user.h (whose structs clash with
ntsync_tmp.h, so both cannot be included together)
- server/inproc_sync.c: runtime get_inproc_device_fd() with kernel
probe + userspace fallback; all call sites dispatch on ntsync_userspace
- dlls/ntdll/unix/sync.c: compile both linux_* and userspace_* paths,
dispatch per call; client learns backend via init_first_thread reply
- server/thread.c, process.c, ntdll/unix/server.c, unix_private.h:
matching runtime branches; ntsync_sweep_dead gated on userspace active
(no shm region auto-init in kernel mode)
- README: document runtime backend selection order
Verified: full 56-patch train applies on pristine HEAD; Android arm64ec
builds of server/wineserver and dlls/ntdll/ntdll.so link with zero new
warnings; non-Android path passes -fsyntax-only -Werror.
---
android/ntsync_android/README.md | 56 ++++--
android/ntsync_android/ntsync_kernel_abi.h | 52 +++++
.../common/dlls_ntdll_unix_server_c.patch | 10 +-
.../common/dlls_ntdll_unix_sync_c.patch | 186 ++++++++++++++----
.../dlls_ntdll_unix_unix_private_h.patch | 17 +-
.../patches/common/server_inproc_sync_c.patch | 169 ++++++++--------
android/patches/common/server_process_c.patch | 13 +-
android/patches/common/server_thread_c.patch | 45 +++--
8 files changed, 380 insertions(+), 168 deletions(-)
create mode 100644 android/ntsync_android/ntsync_kernel_abi.h
diff --git a/android/ntsync_android/README.md b/android/ntsync_android/README.md
index d77addbeaa48..7f982039f388 100644
--- a/android/ntsync_android/README.md
+++ b/android/ntsync_android/README.md
@@ -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
@@ -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`,
@@ -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).
diff --git a/android/ntsync_android/ntsync_kernel_abi.h b/android/ntsync_android/ntsync_kernel_abi.h
new file mode 100644
index 000000000000..13a582869ddc
--- /dev/null
+++ b/android/ntsync_android/ntsync_kernel_abi.h
@@ -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 .
+ *
+ * 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
+
+#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 */
diff --git a/android/patches/common/dlls_ntdll_unix_server_c.patch b/android/patches/common/dlls_ntdll_unix_server_c.patch
index 003359d11b50..2693db024840 100644
--- a/android/patches/common/dlls_ntdll_unix_server_c.patch
+++ b/android/patches/common/dlls_ntdll_unix_server_c.patch
@@ -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 @@
@@ -30,7 +30,7 @@ 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 );
}
@@ -38,7 +38,11 @@ index 112157b0dd7..d74c6a32b27 100644
+ /* 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)
diff --git a/android/patches/common/dlls_ntdll_unix_sync_c.patch b/android/patches/common/dlls_ntdll_unix_sync_c.patch
index f012795725b3..2b1e5280383a 100644
--- a/android/patches/common/dlls_ntdll_unix_sync_c.patch
+++ b/android/patches/common/dlls_ntdll_unix_sync_c.patch
@@ -1,38 +1,43 @@
diff --git a/dlls/ntdll/unix/sync.c b/dlls/ntdll/unix/sync.c
-index 67ab945..d4c6657 100644
+index 67ab945cef8..8e79186810f 100644
--- a/dlls/ntdll/unix/sync.c
+++ b/dlls/ntdll/unix/sync.c
-@@ -61,7 +61,9 @@
+@@ -61,7 +61,14 @@
# include
#endif
+#ifndef __ANDROID__
# include "ntsync_tmp.h"
++#else
++/* Kernel /dev/ntsync ioctl ABI + userspace ntsync API: both are compiled
++ * in and selected at runtime (kernel ntsync if the server passed us a real
++ * device fd, userspace if it reported NTSYNC_ANDROID_USED_BY_SERVER). */
++# include "../../../android/ntsync_android/ntsync_kernel_abi.h"
+#endif
#include "ntstatus.h"
#define WIN32_NO_STATUS
-@@ -74,6 +76,10 @@
-
- #include "fsync.h"
+@@ -78,6 +85,11 @@ WINE_DEFAULT_DEBUG_CHANNEL(sync);
+ HANDLE keyed_event = 0;
+ int inproc_device_fd = -1;
+#ifdef __ANDROID__
-+#include "../../../android/ntsync_android/ntsync_user.h"
++/* 1 when the server reported userspace ntsync (NTSYNC_ANDROID_USED_BY_SERVER);
++ * inproc_device_fd is a dummy 0 then, not a /dev/ntsync fd */
++int ntsync_userspace = 0;
+#endif
-+
- WINE_DEFAULT_DEBUG_CHANNEL(sync);
- HANDLE keyed_event = 0;
-@@ -451,6 +457,146 @@ static NTSTATUS linux_wait_objs( int device, DWORD count, const int *objs, WAIT_
- return errno_to_status( errno );
- }
+ static const char *debugstr_timeout( const LARGE_INTEGER *timeout )
+ {
+@@ -312,8 +324,151 @@ static unsigned int validate_open_object_attributes( const OBJECT_ATTRIBUTES *at
-+#elif defined(__ANDROID__)
-+
+ #ifdef NTSYNC_IOC_EVENT_READ
+
++#ifdef __ANDROID__
+/* Userspace ntsync (libntsync_android): same semantics as the kernel ioctls,
+ * but objects are uint32 handles in a shared-memory region instead of fds. */
+
-+static NTSTATUS linux_release_semaphore_obj( int obj, ULONG count, ULONG *prev_count )
++static NTSTATUS userspace_release_semaphore_obj( int obj, ULONG count, ULONG *prev_count )
+{
+ uint32_t prev = count;
+ int ret = ntsync_sem_release( obj, &prev );
@@ -42,7 +47,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_query_semaphore_obj( int obj, SEMAPHORE_BASIC_INFORMATION *info )
++static NTSTATUS userspace_query_semaphore_obj( int obj, SEMAPHORE_BASIC_INFORMATION *info )
+{
+ struct ntsync_sem_args args = {0};
+ int ret = ntsync_sem_read( obj, &args );
@@ -52,7 +57,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_set_event_obj( int obj, LONG *prev_state )
++static NTSTATUS userspace_set_event_obj( int obj, LONG *prev_state )
+{
+ uint32_t prev;
+ int ret = ntsync_event_set( obj, &prev );
@@ -61,7 +66,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_reset_event_obj( int obj, LONG *prev_state )
++static NTSTATUS userspace_reset_event_obj( int obj, LONG *prev_state )
+{
+ uint32_t prev;
+ int ret = ntsync_event_reset( obj, &prev );
@@ -70,7 +75,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_pulse_event_obj( int obj, LONG *prev_state )
++static NTSTATUS userspace_pulse_event_obj( int obj, LONG *prev_state )
+{
+ uint32_t prev;
+ int ret = ntsync_event_pulse( obj, &prev );
@@ -79,7 +84,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_query_event_obj( int obj, EVENT_BASIC_INFORMATION *info )
++static NTSTATUS userspace_query_event_obj( int obj, EVENT_BASIC_INFORMATION *info )
+{
+ struct ntsync_event_args args = {0};
+ int ret = ntsync_event_read( obj, &args );
@@ -89,7 +94,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_release_mutex_obj( int obj, LONG *prev_count )
++static NTSTATUS userspace_release_mutex_obj( int obj, LONG *prev_count )
+{
+ struct ntsync_mutex_args args = {.owner = GetCurrentThreadId()};
+ int ret = ntsync_mutex_unlock( obj, &args );
@@ -100,7 +105,7 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_query_mutex_obj( int obj, MUTANT_BASIC_INFORMATION *info )
++static NTSTATUS userspace_query_mutex_obj( int obj, MUTANT_BASIC_INFORMATION *info )
+{
+ struct ntsync_mutex_args args = {0};
+ int ret = ntsync_mutex_read( obj, &args );
@@ -118,8 +123,8 @@ index 67ab945..d4c6657 100644
+ return STATUS_SUCCESS;
+}
+
-+static NTSTATUS linux_wait_objs( int device, DWORD count, const int *objs, WAIT_TYPE type,
-+ int alert_fd, const LARGE_INTEGER *timeout )
++static NTSTATUS userspace_wait_objs( DWORD count, const int *objs, WAIT_TYPE type,
++ int alert_fd, const LARGE_INTEGER *timeout )
+{
+ struct ntsync_wait_args args = {0};
+ struct timespec now;
@@ -166,31 +171,136 @@ index 67ab945..d4c6657 100644
+ if (ret == -ETIMEDOUT) return STATUS_TIMEOUT;
+ return errno_to_status( -ret );
+}
++#endif /* __ANDROID__ */
+
- #else /* NTSYNC_IOC_EVENT_READ */
-
static NTSTATUS linux_release_semaphore_obj( int obj, ULONG count, ULONG *prev_count )
-@@ -636,7 +782,12 @@ static void release_inproc_sync( struct inproc_sync *sync )
+ {
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_release_semaphore_obj( obj, count, prev_count );
++#endif
+ if (ioctl( obj, NTSYNC_IOC_SEM_RELEASE, &count ) < 0)
+ {
+ if (errno == EOVERFLOW) return STATUS_SEMAPHORE_LIMIT_EXCEEDED;
+@@ -325,6 +480,9 @@ static NTSTATUS linux_release_semaphore_obj( int obj, ULONG count, ULONG *prev_c
+
+ static NTSTATUS linux_query_semaphore_obj( int obj, SEMAPHORE_BASIC_INFORMATION *info )
+ {
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_query_semaphore_obj( obj, info );
++#endif
+ struct ntsync_sem_args args = {0};
+ if (ioctl( obj, NTSYNC_IOC_SEM_READ, &args ) < 0) return errno_to_status( errno );
+ info->CurrentCount = args.count;
+@@ -334,7 +492,10 @@ static NTSTATUS linux_query_semaphore_obj( int obj, SEMAPHORE_BASIC_INFORMATION
+
+ static NTSTATUS linux_set_event_obj( int obj, LONG *prev_state )
+ {
+- __u32 prev;
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_set_event_obj( obj, prev_state );
++#endif
++ uint32_t prev;
+ if (ioctl( obj, NTSYNC_IOC_EVENT_SET, &prev ) < 0) return errno_to_status( errno );
+ if (prev_state) *prev_state = prev;
+ return STATUS_SUCCESS;
+@@ -342,7 +503,10 @@ static NTSTATUS linux_set_event_obj( int obj, LONG *prev_state )
+
+ static NTSTATUS linux_reset_event_obj( int obj, LONG *prev_state )
+ {
+- __u32 prev;
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_reset_event_obj( obj, prev_state );
++#endif
++ uint32_t prev;
+ if (ioctl( obj, NTSYNC_IOC_EVENT_RESET, &prev ) < 0) return errno_to_status( errno );
+ if (prev_state) *prev_state = prev;
+ return STATUS_SUCCESS;
+@@ -350,7 +514,10 @@ static NTSTATUS linux_reset_event_obj( int obj, LONG *prev_state )
+
+ static NTSTATUS linux_pulse_event_obj( int obj, LONG *prev_state )
+ {
+- __u32 prev;
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_pulse_event_obj( obj, prev_state );
++#endif
++ uint32_t prev;
+ if (ioctl( obj, NTSYNC_IOC_EVENT_PULSE, &prev ) < 0) return errno_to_status( errno );
+ if (prev_state) *prev_state = prev;
+ return STATUS_SUCCESS;
+@@ -358,6 +525,9 @@ static NTSTATUS linux_pulse_event_obj( int obj, LONG *prev_state )
+
+ static NTSTATUS linux_query_event_obj( int obj, EVENT_BASIC_INFORMATION *info )
+ {
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_query_event_obj( obj, info );
++#endif
+ struct ntsync_event_args args = {0};
+ if (ioctl( obj, NTSYNC_IOC_EVENT_READ, &args ) < 0) return errno_to_status( errno );
+ info->EventType = args.manual ? NotificationEvent : SynchronizationEvent;
+@@ -367,6 +537,9 @@ static NTSTATUS linux_query_event_obj( int obj, EVENT_BASIC_INFORMATION *info )
+
+ static NTSTATUS linux_release_mutex_obj( int obj, LONG *prev_count )
+ {
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_release_mutex_obj( obj, prev_count );
++#endif
+ struct ntsync_mutex_args args = {.owner = GetCurrentThreadId()};
+ if (ioctl( obj, NTSYNC_IOC_MUTEX_UNLOCK, &args ) < 0)
+ {
+@@ -380,6 +553,9 @@ static NTSTATUS linux_release_mutex_obj( int obj, LONG *prev_count )
+
+ static NTSTATUS linux_query_mutex_obj( int obj, MUTANT_BASIC_INFORMATION *info )
+ {
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_query_mutex_obj( obj, info );
++#endif
+ struct ntsync_mutex_args args = {0};
+ if (ioctl( obj, NTSYNC_IOC_MUTEX_READ, &args ) < 0)
+ {
+@@ -401,6 +577,9 @@ static NTSTATUS linux_query_mutex_obj( int obj, MUTANT_BASIC_INFORMATION *info )
+ static NTSTATUS linux_wait_objs( int device, DWORD count, const int *objs, WAIT_TYPE type,
+ int alert_fd, const LARGE_INTEGER *timeout )
+ {
++#ifdef __ANDROID__
++ if (ntsync_userspace) return userspace_wait_objs( count, objs, type, alert_fd, timeout );
++#endif
+ struct ntsync_wait_args args = {0};
+ unsigned long request;
+ struct timespec now;
+@@ -408,7 +587,7 @@ static NTSTATUS linux_wait_objs( int device, DWORD count, const int *objs, WAIT_
+
+ if (!timeout || timeout->QuadPart == TIMEOUT_INFINITE)
+ {
+- args.timeout = ~(__u64)0;
++ args.timeout = ~(uint64_t)0;
+ }
+ else if (timeout->QuadPart <= 0)
+ {
+@@ -636,7 +815,12 @@ static void release_inproc_sync( struct inproc_sync *sync )
LONG ref = InterlockedDecrement( &sync->refcount );
assert( ref >= 0 );
+#ifdef __ANDROID__
+ /* userspace ntsync objects are owned by the server; nothing to close */
-+ if (!ref) (void)fd;
++ if (!ref && !ntsync_userspace) close( fd );
+#else
if (!ref) close( fd );
+#endif
}
static struct inproc_sync *get_cached_inproc_sync( HANDLE handle )
-@@ -677,8 +828,14 @@ static NTSTATUS get_server_inproc_sync( HANDLE handle, struct inproc_sync *sync
+@@ -677,8 +861,18 @@ static NTSTATUS get_server_inproc_sync( HANDLE handle, struct inproc_sync *sync
{
obj_handle_t fd_handle;
sync->refcount = 1;
+#ifdef __ANDROID__
+ /* userspace ntsync object handle is passed in fsync_shm_idx */
-+ sync->fd = reply->fsync_shm_idx;
-+ (void)fd_handle;
++ if (ntsync_userspace) sync->fd = reply->fsync_shm_idx;
++ else
++ {
++ sync->fd = wine_server_receive_fd( &fd_handle );
++ assert( wine_server_ptr_handle(fd_handle) == handle );
++ }
+#else
sync->fd = wine_server_receive_fd( &fd_handle );
assert( wine_server_ptr_handle(fd_handle) == handle );
@@ -198,24 +308,18 @@ index 67ab945..d4c6657 100644
sync->access = reply->access;
sync->type = reply->type;
sync->closed = 0;
-@@ -908,11 +1065,16 @@ int get_inproc_alert_fd(void)
+@@ -908,6 +1102,10 @@ int get_inproc_alert_fd(void)
if (!server_call_unlocked( req ))
{
if (do_fsync()) data->alert_fd = fd = reply->fsync_shm_idx;
+#ifdef __ANDROID__
+ /* userspace ntsync alert event handle; valid in every process */
-+ else data->alert_fd = fd = reply->fsync_shm_idx;
-+#else
++ else if (ntsync_userspace) data->alert_fd = fd = reply->fsync_shm_idx;
++#endif
else
{
data->alert_fd = fd = wine_server_receive_fd( &token );
- assert( token == reply->handle );
- }
-+#endif
- }
- }
- SERVER_END_REQ;
-@@ -2426,6 +2588,19 @@ NTSTATUS WINAPI NtSignalAndWaitForSingleObject( HANDLE signal, HANDLE wait,
+@@ -2426,6 +2624,19 @@ NTSTATUS WINAPI NtSignalAndWaitForSingleObject( HANDLE signal, HANDLE wait,
*/
NTSTATUS WINAPI NtYieldExecution(void)
{
diff --git a/android/patches/common/dlls_ntdll_unix_unix_private_h.patch b/android/patches/common/dlls_ntdll_unix_unix_private_h.patch
index 9b4fd2ccfde8..c1ef87bac0e4 100644
--- a/android/patches/common/dlls_ntdll_unix_unix_private_h.patch
+++ b/android/patches/common/dlls_ntdll_unix_unix_private_h.patch
@@ -1,8 +1,19 @@
diff --git a/dlls/ntdll/unix/unix_private.h b/dlls/ntdll/unix/unix_private.h
-index 2f649ca..8700313 100644
+index 2f649ca399e..830499d63d3 100644
--- a/dlls/ntdll/unix/unix_private.h
+++ b/dlls/ntdll/unix/unix_private.h
-@@ -232,6 +232,7 @@ extern NTSTATUS exec_wineloader( char **argv, int socketfd, const struct pe_imag
+@@ -203,6 +203,10 @@ extern BOOL terminate_process_running;
+ extern LONG terminate_process_exit_code;
+ extern HANDLE keyed_event;
+ extern int inproc_device_fd;
++#ifdef __ANDROID__
++/* 1 when the server reported userspace ntsync instead of a /dev/ntsync fd */
++extern int ntsync_userspace;
++#endif
+ extern timeout_t server_start_time;
+ extern sigset_t server_block_set;
+ extern pthread_mutex_t fd_cache_mutex;
+@@ -232,6 +236,7 @@ extern NTSTATUS exec_wineloader( char **argv, int socketfd, const struct pe_imag
extern NTSTATUS load_builtin( const struct pe_image_info *image_info, UNICODE_STRING *nt_name,
ANSI_STRING *exp_name, USHORT machine, SECTION_IMAGE_INFORMATION *info,
void **module, SIZE_T *size, ULONG_PTR limit_low, ULONG_PTR limit_high, off_t offset );
@@ -10,7 +21,7 @@ index 2f649ca..8700313 100644
extern BOOL is_builtin_path( const UNICODE_STRING *path, WORD *machine );
extern NTSTATUS load_main_exe( UNICODE_STRING *nt_name, USHORT load_machine, void **module );
extern NTSTATUS load_start_exe( UNICODE_STRING *nt_name, void **module );
-@@ -724,4 +725,6 @@ static inline int is_gdt_sel( WORD sel )
+@@ -724,4 +729,6 @@ static inline int is_gdt_sel( WORD sel )
#endif /* defined(__i386__) || defined(__x86_64__) */
diff --git a/android/patches/common/server_inproc_sync_c.patch b/android/patches/common/server_inproc_sync_c.patch
index 3034786a47b9..6921e1e6c9fc 100644
--- a/android/patches/common/server_inproc_sync_c.patch
+++ b/android/patches/common/server_inproc_sync_c.patch
@@ -1,51 +1,32 @@
diff --git a/server/inproc_sync.c b/server/inproc_sync.c
-index a7020ef8fcd..db00cd8e919 100644
+index a7020ef8fcd..c8691d1cd49 100644
--- a/server/inproc_sync.c
+++ b/server/inproc_sync.c
-@@ -36,10 +36,18 @@
+@@ -36,7 +36,14 @@
#include "fsync.h"
-+#ifdef __ANDROID__
-+#include
-+#include "../android/ntsync_android/ntsync_user.h"
-+#endif
-+
+#ifndef __ANDROID__
#include "ntsync_tmp.h"
++#else
++#include
++/* Kernel /dev/ntsync ioctl ABI + userspace ntsync API: both are compiled
++ * in and selected at runtime (kernel ntsync if usable, userspace else). */
++#include "../android/ntsync_android/ntsync_kernel_abi.h"
+#endif
--#ifdef NTSYNC_IOC_EVENT_READ
-+#if defined(NTSYNC_IOC_EVENT_READ) || defined(__ANDROID__)
+ #ifdef NTSYNC_IOC_EVENT_READ
-+#ifndef __ANDROID__
- #include
- #include
+@@ -45,6 +52,36 @@
#include
-@@ -64,6 +72,49 @@ int get_inproc_device_fd(void)
- }
- return fd;
- }
-+#else /* __ANDROID__ */
-+int get_inproc_device_fd(void)
+ #include
+
++#ifdef __ANDROID__
++static int ntsync_userspace; /* 1 when using libntsync_android instead of /dev/ntsync */
++
++int ntsync_userspace_active(void)
+{
-+ static int fd = -2;
-+ if (fd == -2)
-+ {
-+ if (getenv( "PROTON_NO_NTSYNC" ) && atoi(getenv( "PROTON_NO_NTSYNC" )))
-+ fd = -1;
-+ else if (!ntsync_init( NULL ))
-+ {
-+ /* userspace ntsync needs no device fd; objects live in a
-+ * shared-memory region every process attaches to */
-+ fd = 0;
-+ do_fsync_cached = 0;
-+ fprintf( stderr, "ntsync: up and running (userspace).\n" );
-+ }
-+ else if (do_fsync()) fd = FSYNC_USED_BY_SERVER;
-+ else fprintf( stderr, "wineserver: using server-side synchronization.\n" );
-+ }
-+ return fd;
++ return ntsync_userspace;
+}
+
+static int create_ntsync_event( const struct ntsync_event_args *args )
@@ -69,58 +50,95 @@ index a7020ef8fcd..db00cd8e919 100644
+ return (int)handle;
+}
+#endif /* __ANDROID__ */
-
- struct inproc_sync
++
+ int get_inproc_device_fd(void)
{
-@@ -124,7 +175,11 @@ struct inproc_sync *create_inproc_internal_sync( int manual, int signaled )
+ static int fd = -2;
+@@ -53,10 +90,39 @@ int get_inproc_device_fd(void)
+ if (getenv( "PROTON_NO_NTSYNC" ) && atoi(getenv( "PROTON_NO_NTSYNC" )))
+ fd = -1;
+ else
++ {
+ fd = open( "/dev/ntsync", O_CLOEXEC | O_RDONLY );
++#ifdef __ANDROID__
++ if (fd >= 0)
++ {
++ /* the device node can exist but be unusable (SELinux policy,
++ * seccomp); probe it with a real object creation */
++ struct ntsync_event_args args = {0};
++ int probe = ioctl( fd, NTSYNC_IOC_CREATE_EVENT, &args );
++ if (probe < 0)
++ {
++ close( fd );
++ fd = -1;
++ }
++ else close( probe );
++ }
++ if (fd < 0 && !ntsync_init( NULL ))
++ {
++ /* userspace ntsync needs no device fd; objects live in a
++ * shared-memory region every process attaches to */
++ fd = 0;
++ ntsync_userspace = 1;
++ }
++#endif
++ }
+ if (fd >= 0)
+ {
+ do_fsync_cached = 0;
++#ifdef __ANDROID__
++ if (ntsync_userspace)
++ fprintf( stderr, "ntsync: no usable /dev/ntsync, using userspace ntsync.\n" );
++ else
++#endif
+ fprintf( stderr, "ntsync: up and running.\n" );
+ }
+ else if (do_fsync()) fd = FSYNC_USED_BY_SERVER;
+@@ -124,6 +190,10 @@ struct inproc_sync *create_inproc_internal_sync( int manual, int signaled )
else
{
event->type = INPROC_SYNC_INTERNAL;
+#ifdef __ANDROID__
-+ event->fd = create_ntsync_event( &args );
-+#else
- event->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_EVENT, &args );
++ if (ntsync_userspace) event->fd = create_ntsync_event( &args );
++ else
+#endif
+ event->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_EVENT, &args );
}
list_init( &event->entry );
-
-@@ -151,7 +206,11 @@ struct inproc_sync *create_inproc_event_sync( int manual, int signaled )
+@@ -151,6 +221,10 @@ struct inproc_sync *create_inproc_event_sync( int manual, int signaled )
else
{
event->type = INPROC_SYNC_EVENT;
+#ifdef __ANDROID__
-+ event->fd = create_ntsync_event( &args );
-+#else
- event->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_EVENT, &args );
++ if (ntsync_userspace) event->fd = create_ntsync_event( &args );
++ else
+#endif
+ event->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_EVENT, &args );
}
list_init( &event->entry );
-
-@@ -178,7 +237,11 @@ struct inproc_sync *create_inproc_mutex_sync( thread_id_t owner, unsigned int co
+@@ -178,6 +252,10 @@ struct inproc_sync *create_inproc_mutex_sync( thread_id_t owner, unsigned int co
else
{
mutex->type = INPROC_SYNC_MUTEX;
+#ifdef __ANDROID__
-+ mutex->fd = create_ntsync_mutex( &args );
-+#else
- mutex->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_MUTEX, &args );
++ if (ntsync_userspace) mutex->fd = create_ntsync_mutex( &args );
++ else
+#endif
+ mutex->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_MUTEX, &args );
}
list_add_tail( &inproc_mutexes, &mutex->entry );
-
-@@ -206,7 +269,11 @@ struct inproc_sync *create_inproc_semaphore_sync( unsigned int initial, unsigned
+@@ -206,6 +284,10 @@ struct inproc_sync *create_inproc_semaphore_sync( unsigned int initial, unsigned
else
{
sem->type = INPROC_SYNC_SEMAPHORE;
+#ifdef __ANDROID__
-+ sem->fd = create_ntsync_sem( &args );
-+#else
- sem->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_SEM, &args );
++ if (ntsync_userspace) sem->fd = create_ntsync_sem( &args );
++ else
+#endif
+ sem->fd = ioctl( get_inproc_device_fd(), NTSYNC_IOC_CREATE_SEM, &args );
}
list_init( &sem->entry );
-
-@@ -228,18 +295,26 @@ static void inproc_sync_dump( struct object *obj, int verbose )
+@@ -228,17 +310,23 @@ static void inproc_sync_dump( struct object *obj, int verbose )
void signal_inproc_sync( struct inproc_sync *sync )
{
@@ -129,10 +147,9 @@ index a7020ef8fcd..db00cd8e919 100644
if (debug_level) fprintf( stderr, "set_inproc_event %d\n", sync->fd );
if (do_fsync()) fsync_set_event( sync->fd );
+#ifdef __ANDROID__
-+ else ntsync_event_set( sync->fd, &count );
-+#else
- else ioctl( sync->fd, NTSYNC_IOC_EVENT_SET, &count );
++ else if (ntsync_userspace) ntsync_event_set( sync->fd, &count );
+#endif
+ else ioctl( sync->fd, NTSYNC_IOC_EVENT_SET, &count );
}
void reset_inproc_sync( struct inproc_sync *sync )
@@ -142,49 +159,43 @@ index a7020ef8fcd..db00cd8e919 100644
if (debug_level) fprintf( stderr, "reset_inproc_event %d\n", sync->fd );
if (do_fsync()) fsync_reset_event( sync->fd );
+#ifdef __ANDROID__
-+ else ntsync_event_reset( sync->fd, &count );
-+#else
- else ioctl( sync->fd, NTSYNC_IOC_EVENT_RESET, &count );
++ else if (ntsync_userspace) ntsync_event_reset( sync->fd, &count );
+#endif
+ else ioctl( sync->fd, NTSYNC_IOC_EVENT_RESET, &count );
}
- static int inproc_sync_signal( struct object *obj, unsigned int access, int signal )
-@@ -262,7 +337,11 @@ static void inproc_sync_destroy( struct object *obj )
+@@ -262,6 +350,9 @@ static void inproc_sync_destroy( struct object *obj )
assert( obj->ops == &inproc_sync_ops );
list_remove( &sync->entry );
if (do_fsync()) fsync_free_shm_idx( sync->fd );
+#ifdef __ANDROID__
-+ else ntsync_close( sync->fd );
-+#else
- else close( sync->fd );
++ else if (ntsync_userspace) ntsync_close( sync->fd );
+#endif
+ else close( sync->fd );
}
- void abandon_inproc_mutexes( thread_id_t tid )
-@@ -277,7 +356,13 @@ void abandon_inproc_mutexes( thread_id_t tid )
+@@ -277,7 +368,13 @@ void abandon_inproc_mutexes( thread_id_t tid )
}
LIST_FOR_EACH_ENTRY( mutex, &inproc_mutexes, struct inproc_sync, entry )
+ {
+#ifdef __ANDROID__
-+ ntsync_mutex_kill( mutex->fd, tid );
-+#else
- ioctl( mutex->fd, NTSYNC_IOC_MUTEX_KILL, &tid );
++ if (ntsync_userspace) ntsync_mutex_kill( mutex->fd, tid );
++ else
+#endif
+ ioctl( mutex->fd, NTSYNC_IOC_MUTEX_KILL, &tid );
+ }
}
static int get_obj_inproc_sync( struct object *obj, int *type )
-@@ -359,7 +444,12 @@ DECL_HANDLER(get_inproc_sync_fd)
+@@ -359,6 +456,10 @@ DECL_HANDLER(get_inproc_sync_fd)
if ((fd = get_obj_inproc_sync( obj, &reply->type )) < 0) set_error( STATUS_NOT_IMPLEMENTED );
else if (do_fsync()) reply->fsync_shm_idx = fsync_grab_shm_idx( fd );
+#ifdef __ANDROID__
+ /* userspace ntsync object handle; valid in every process */
-+ else reply->fsync_shm_idx = fd;
-+#else
- else send_client_fd( current->process, fd, req->handle );
++ else if (ntsync_userspace) reply->fsync_shm_idx = fd;
+#endif
+ else send_client_fd( current->process, fd, req->handle );
release_object( obj );
- }
diff --git a/android/patches/common/server_process_c.patch b/android/patches/common/server_process_c.patch
index 372e7c4239f3..de66a8926168 100644
--- a/android/patches/common/server_process_c.patch
+++ b/android/patches/common/server_process_c.patch
@@ -1,26 +1,29 @@
diff --git a/server/process.c b/server/process.c
-index 287718adc66..3ff0d68c261 100644
+index 287718adc66..0aa6b0c230e 100644
--- a/server/process.c
+++ b/server/process.c
-@@ -67,6 +67,10 @@
+@@ -67,6 +67,12 @@
#include "fsync.h"
+#ifdef __ANDROID__
+#include "../android/ntsync_android/ntsync_user.h"
++/* from inproc_sync.c: 1 when using userspace ntsync instead of /dev/ntsync */
++extern int ntsync_userspace_active(void);
+#endif
+
/* process object */
static struct list process_list = LIST_INIT(process_list);
-@@ -822,6 +826,11 @@ static void process_destroy( struct object *obj )
+@@ -822,6 +828,12 @@ static void process_destroy( struct object *obj )
if (process->token) release_object( process->token );
if (process->sync) release_object( process->sync );
if (do_fsync()) fsync_cleanup_process_shm_indices( process->id );
+#ifdef __ANDROID__
+ /* userspace ntsync has no fd-close-on-death hook; reclaim objects whose
-+ * creator process is gone */
-+ ntsync_sweep_dead();
++ * creator process is gone (only when userspace ntsync is actually in use,
++ * otherwise ntsync_sweep_dead would pointlessly auto-init the shm region) */
++ if (ntsync_userspace_active()) ntsync_sweep_dead();
+#endif
list_remove( &process->rawinput_entry );
free( process->rawinput_devices );
diff --git a/android/patches/common/server_thread_c.patch b/android/patches/common/server_thread_c.patch
index 7a22221c80d5..102bcd3fc4d6 100644
--- a/android/patches/common/server_thread_c.patch
+++ b/android/patches/common/server_thread_c.patch
@@ -1,45 +1,48 @@
diff --git a/server/thread.c b/server/thread.c
-index 5c4a6bd0207..90b2940d387 100644
+index 5c4a6bd0207..396e0407380 100644
--- a/server/thread.c
+++ b/server/thread.c
-@@ -63,6 +63,10 @@
+@@ -63,6 +63,12 @@
#include "fsync.h"
+#ifdef __ANDROID__
+#include "../android/ntsync_android/ntsync_user.h"
++/* from inproc_sync.c: 1 when using userspace ntsync instead of /dev/ntsync */
++extern int ntsync_userspace_active(void);
+#endif
+
/* thread queues */
struct thread_wait
-@@ -1835,6 +1839,14 @@ DECL_HANDLER(init_first_thread)
- {
- reply->inproc_device = FSYNC_USED_BY_SERVER;
+@@ -1837,8 +1843,17 @@ DECL_HANDLER(init_first_thread)
}
-+#ifdef __ANDROID__
-+ /* userspace ntsync needs no device fd; the client attaches to the
-+ * shared-memory region itself */
-+ else if (get_inproc_device_fd() >= 0)
-+ {
-+ reply->inproc_device = NTSYNC_ANDROID_USED_BY_SERVER;
-+ }
-+#endif
else if ((fd = get_inproc_device_fd()) >= 0)
{
- reply->inproc_device = get_process_id( process ) | 1;
-@@ -2479,9 +2491,14 @@ DECL_HANDLER(get_inproc_alert_fd)
+- reply->inproc_device = get_process_id( process ) | 1;
+- send_client_fd( process, fd, reply->inproc_device );
++#ifdef __ANDROID__
++ /* userspace ntsync needs no device fd; the client attaches to the
++ * shared-memory region itself */
++ if (ntsync_userspace_active())
++ reply->inproc_device = NTSYNC_ANDROID_USED_BY_SERVER;
++ else
++#endif
++ {
++ reply->inproc_device = get_process_id( process ) | 1;
++ send_client_fd( process, fd, reply->inproc_device );
++ }
+ }
+ }
+
+@@ -2479,6 +2494,10 @@ DECL_HANDLER(get_inproc_alert_fd)
if ((fd = get_inproc_sync_fd( current->alert_sync )) < 0) set_error( STATUS_INVALID_PARAMETER );
else if (do_fsync()) reply->fsync_shm_idx = fd;
+#ifdef __ANDROID__
+ /* userspace ntsync alert event handle; valid in every process */
-+ else reply->fsync_shm_idx = fd;
-+#else
++ else if (ntsync_userspace_active()) reply->fsync_shm_idx = fd;
++#endif
else
{
reply->handle = get_thread_id( current ) | 1; /* arbitrary token */
- send_client_fd( current->process, fd, reply->handle );
- }
-+#endif
- }
From 787a5049e7edc9272f7da8d11594d2824561e222 Mon Sep 17 00:00:00 2001
From: Joshua Tam <297250+joshuatam@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:28:44 +0800
Subject: [PATCH 2/2] ci: release notes for runtime kernel/userspace ntsync
detection
Release notes now describe runtime backend selection (kernel /dev/ntsync
probe at wineserver startup, userspace ntsync-android fallback,
PROTON_NO_NTSYNC=1 to disable) instead of the old compile-time userspace
selection. ntsync-android library pinned unchanged at 7ce6435; the
accumulated changelog is replaced with just this build's changes.
App-facing descriptions updated to match.
---
.github/workflows/build-proton.yml | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/build-proton.yml b/.github/workflows/build-proton.yml
index 1541c8b4fe5a..bdcda26732c9 100644
--- a/.github/workflows/build-proton.yml
+++ b/.github/workflows/build-proton.yml
@@ -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",
@@ -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",
@@ -331,7 +331,7 @@ 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
@@ -339,15 +339,10 @@ jobs:
- 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