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