This page documents Wine-NSPA’s aggregate-wait kernel primitive and the dispatcher-owned same-thread async-completion path built on it.
io_uring and same-thread completionAggregate-wait is the kernel-side wait primitive that lets the gamma dispatcher block on request traffic, deferred-completion wakeups, and teardown wakeups in one place while keeping receive, CQE drain, and reply signaling on the same RT thread.
That is the architectural role of aggregate-wait plus the dispatcher’s
ring-ownership and same-thread completion work. Gamma already gave Wine-NSPA the correct
request-side priority inheritance story: client threads do
CHANNEL_SEND_PI, the kernel enqueues by priority, and the wineserver
dispatcher runs the handler at the right effective priority. What gamma
lacked was the matching async completion-side wait primitive.
The first async-completion prototype used the wineserver main thread as the CQE drain site. That proved the basic mechanism but broke the more important invariant: the thread that received the request was no longer the thread that completed and replied to it.
The aggregate-wait kernel extension and the accompanying dispatcher restructure fix that. The dispatcher owns all three parts of the async path:
io_uringCHANNEL_REPLYThe same RT thread handles the full lifecycle.
| Layer | Landed change | Why it matters |
|---|---|---|
| Kernel | NTSYNC_IOC_AGGREGATE_WAIT |
One wait covers NTSync objects plus pollable fds |
| Kernel | Channel notify-only support inside aggregate-wait | lets the dispatcher block on the channel without consuming the entry in the aggregate ioctl itself |
| Kernel | post-aggregate-wait PI follow-ons | stable boost propagation for aggregate-waiting dispatchers |
| Userspace | struct nspa_uring_instance per process |
dispatcher-local ring + eventfd + fixed pending pool |
| Userspace | struct nspa_dispatcher_ctx |
single owner for channel fd, shutdown eventfd, and ring lifetime |
| Userspace | aggregate-wait dispatcher loop | same-thread request receive, CQE drain, and reply |
The rejected shape was:
The problem was not that the code path was impossible. The problem was that it was the wrong ownership model for an RT request path:
That shape showed up exactly where expected: real workloads tolerated it structurally, but timing-sensitive application behavior did not.
Patch 1010 adds NTSYNC_IOC_AGGREGATE_WAIT: a heterogeneous wait that combines
NTSync object sources, pollable fd sources, and an optional absolute deadline.
The dispatcher is the first consumer, but the primitive is intentionally general.
struct ntsync_aggregate_source {
__u32 type; /* NTSYNC_AGG_OBJECT | NTSYNC_AGG_FD */
__u32 events; /* FD source: POLLIN / POLLOUT / POLLERR / POLLHUP */
__u64 handle_or_fd; /* ntsync object handle, or unix fd */
};
struct ntsync_aggregate_wait_args {
__u32 nb_sources;
__u32 reserved;
__u64 sources; /* user pointer to struct ntsync_aggregate_source[] */
struct __kernel_timespec deadline; /* CLOCK_MONOTONIC ABSTIME or {0,0} */
__u32 fired_index;
__u32 fired_events;
__u32 flags;
__u32 owner;
};
#define NTSYNC_AGG_OBJECT 0x1
#define NTSYNC_AGG_FD 0x2
#define NTSYNC_AGG_MAX 64
#define NTSYNC_AGG_FLAG_REALTIME 0x1
#define NTSYNC_AGG_TIMEOUT 0xFFFFFFFFu
#define NTSYNC_IOC_AGGREGATE_WAIT _IOWR('N', 0x95, struct ntsync_aggregate_wait_args)
CHANNEL_RECV2 to consume
the actual entry.AGGREGATE_WAIT, CHANNEL_RECV2, and CHANNEL_TRY_RECV2.
The older sticky fallback ladder was retired once that kernel surface became
the project baseline.io_uring and same-thread completionio_uringThis step did not make handlers async by itself. It put the ring and its state in the correct ownership domain first.
The old global-ring direction was abandoned. The landed design keeps one
nspa_uring_instance per gamma channel / per Wine process, stored alongside the
dispatcher context.
struct nspa_dispatcher_ctx {
int channel_fd;
int shutdown_efd;
struct nspa_uring_instance uring;
};
Key properties:
shutdown_efd gives the aggregate-wait path an explicit teardown wakeupThe dispatcher waits on three sources:
The loop is:
{channel, uring eventfd if active, shutdown eventfd}NTSYNC_IOC_AGGREGATE_WAITCHANNEL_RECV2nspa_uring_drain()CHANNEL_REPLYshutdown_efd:
The current dispatcher runtime assumes the full post-1011 wait surface:
NTSYNC_IOC_AGGREGATE_WAITNTSYNC_IOC_CHANNEL_RECV2NTSYNC_IOC_CHANNEL_TRY_RECV2That requirement is deliberate. The older sticky fallback ladder was
useful while aggregate-wait and thread-token receive were landing, but it was
retired once those ioctls became the project baseline. The current production
loop is therefore simpler: one aggregate wait, one RECV2, zero or more
TRY_RECV2 drains, and the same-thread reply path.
| Item | Value |
|---|---|
| Kernel side | Wine-NSPA ntsync overlay with aggregate-wait and burst drain support |
| Wine userspace state | Dispatcher-owned ring and aggregate-wait loop; async CreateFile and burst drain both build on the same base |
| Current wait shape | AGGREGATE_WAIT over channel + uring eventfd + shutdown eventfd |
| Current follow-ons on top of this base | dispatcher-owned async CreateFile; post-reply TRY_RECV2 burst drain |
| Test | Result |
|---|---|
test-aggregate-wait |
9/9 PASS |
| channel-PI propagation sub-test | PASS |
| 1k mixed-concurrency stress | PASS |
| 30k stress + full native ntsync suite | PASS, dmesg clean |
| PE matrix | 32 PASS / 0 FAIL / 0 TIMEOUT, including dispatcher-burst |
| Ableton level 2/3 on the current path | PASS |
| Aggregate-wait in normal production use | PASS |
The follow-up kernel fixes matter here. The first 1010 cut exposed exactly the kind of PI edge that the dispatcher cannot tolerate: an aggregate-waiting dispatcher must still be visible to SEND_PI wake/boost logic and must not be woken before the new boost state is established. The current overlay includes those corrections.
The public decomposition plan still has queued work in front of it, but the aggregate-wait story is no longer purely hypothetical.
Implemented:
Still queued:
So the right interpretation is:
That is a better architectural state than the earlier plan assumed. Future work no longer needs to prove the syscall shape from scratch; it can build on a production consumer.
wine/server/nspa/shmem_channel.c — dispatcher context, aggregate-wait loop, shutdown pathwine/server/nspa/uring.h — per-process nspa_uring_instance public surfacentsync-patches/1010-ntsync-aggregate-wait.patch — aggregate-wait kernel patch