Wine-NSPA – Yabridge-NSPA

This page documents the yabridge fork aligned to Wine-NSPA’s RT scheduling, priority-inheritance, and userspace-sync model.

Table of Contents

  1. Overview
  2. Why a separate fork exists
  3. Process shape
  4. Editor embedding
  5. RT ownership and priority mapping
  6. Per-callback transport
  7. Current format coverage
  8. Startup and lifecycle hardening
  9. Relationship to Wine-NSPA
  10. References

1. Overview

Yabridge-NSPA is a yabridge fork for native Linux DAWs hosting Windows VST2, VST3, and CLAP plugins through Wine-NSPA. It keeps yabridge’s basic split between a native Linux plugin library and a Winelib yabridge-host, but it changes the hot path to match Wine-NSPA’s RT rules instead of treating Wine as generic userspace.

The load-bearing changes are:

Yabridge-NSPA: native/Linux control plane + PI audio rendezvous + Wine-owned RT mapping same bridge model as yabridge, but with a different hot-path contract DAW process / plugin-lib side native Linux plugin library loaded by the DAW control/editor traffic still uses the established socket path audio callback publishes one request per block into shared memory DAW thread then sleeps on the reply cond with cross-process PI native half owns the DAW-facing plugin ABI `AudioControlShm` creator = plugin-lib, peer = wine-host request lock + cond, reply lock + cond no cross-process mutex spans plugin `process()` direct envelope is default when layout version matches fixed-shape metadata stays in shared memory oversized or irregular shapes fall back safely audio hot path is narrow on purpose wine-host / plugin side `yabridge-host.exe` + Windows plugin module audio workers call `set_thread_time_critical()` dispatch/control loops call `set_thread_realtime_idle()` plugin init / `LoadLibrary` brackets use `ScopedRealtimeIdleBoost` Wine owns the Win32 -> Linux RT mapping Wine-NSPA substrate used by the bridge vendored `rtpi.h`, Win32 priority mapping, atomic X11 embed, wineserver pre-warm, pidfd teardown, and Wine-side sync correctness fixes the bridge reuses those rules instead of reproducing them locally Current contract control/editor traffic stays on the older socket path the audio callback path is the one that moves onto PI rendezvous and fixed-layout envelopes

The fork does not replace yabridge’s overall model. It changes which primitive owns the timing-critical boundary between the DAW and the Wine-host process.


2. Why a separate fork exists

Upstream yabridge targets stock Wine plus generic Linux scheduling and IPC. Wine-NSPA changes the assumptions under that bridge:

Concern Generic yabridge shape Yabridge-NSPA shape
Audio-thread promotion direct Linux scheduler calls Win32 priority APIs so Wine-NSPA’s mapping and process-class rules apply
Cross-process callback wait unix socket send/recv and ordinary wakeup pi_mutex + pi_cond rendezvous with cross-process PI
Shared sync ABI external or system-provided primitive choice vendored Wine-NSPA rtpi.h so both halves use one pi_mutex_t / pi_cond_t layout
Audio metadata path serialized request/reply payload every block fixed-layout direct fields for the hot metadata, with bounded fallback
Host startup and teardown generic Wine spawn + polling watchdog wineserver pre-warm, pidfd exit detection, PID-tagged cleanup

This is why the fork is code, not just packaging or environment defaults. Wine-NSPA-specific rules live on both sides of the bridge.


3. Process shape

Yabridge-NSPA still has the same two major halves:

The important change is what happens on the callback boundary.

That keeps the hard RT path narrow without rewriting the rest of yabridge’s control-plane logic.


4. Editor embedding

Yabridge-NSPA still keeps a wrapper X11 window as the immediate foreign parent for the plugin editor, but the Wine window handoff beneath that wrapper changed substantially.

The current path is:

  1. reparent the wrapper window under the host’s X11 parent
  2. send WM_X11DRV_NSPA_EMBED_WINDOW to the Wine editor HWND
  3. let Wine perform the reparent + embedded-state flip internally
  4. resize both the wrapper and the embedded Wine X11 child from the host side

The older SubstructureRedirect handler path and related synthetic X11 translation logic are gone from the normal embed flow.

Yabridge editor embed path host X11 parent DAW-owned editor parent wrapper is reparented here first wrapper window keeps host-side focus and size ownership foreign parent passed to the Wine embed message Wine editor HWND / X11 child embedded by `WM_X11DRV_NSPA_EMBED_WINDOW` Wine tracks embedded mode internally What changed the wrapper still exists as the immediate foreign parent, but the old SubstructureRedirect path is replaced by one Wine-side atomic embed primitive

The practical effect is that yabridge no longer needs to recreate Wine’s own embedded-window side effects in userspace just to host a plugin editor.

4.1 What stayed and what changed

Editor-path concern Current behavior
Wrapper window still present as the immediate foreign parent for focus, size, and host-window ownership
Wine child handoff done through WM_X11DRV_NSPA_EMBED_WINDOW instead of a userspace reparent-and-fixup sequence
Win32 pump input-first mouse/keyboard pass, then capped general drain in HostBridge::handle_events()
Host-drag propagation Wine updates embedded WND rect state from host motion
Old SubstructureRedirect path removed from the normal embed flow

The wrapper window remains because yabridge still wants one host-owned X11 surface for focus and size policy. The Wine-side child beneath that wrapper is where the behavior changed.

4.2 Embedded HWND pump discipline

HostBridge::handle_events() is the consumer-side message pump for the editor thread queue. Its current shape is:

That keeps two constraints aligned:

Pump surface Current rule Why
Queue scope whole editor thread queue helper HWNDs below the editor parent still receive real traffic
Mouse pass drain WM_MOUSEFIRST .. WM_MOUSELAST first drag, hover, and resize feedback stay ahead of redraw backlogs
Keyboard pass drain WM_KEYFIRST .. WM_KEYLAST second text and shortcut latency stays low
General drain 20 messages by default bounded return to the native host loop
JUCE flood escape extend the cap to 8192 on WM_USER + 123 JUCE-heavy pages can settle without artificial partial redraw
Yabridge editor-thread pump native host turn wrapper stays host-owned `handle_events()` drains Win32 queue then returns to the main loop input-first mouse range first keyboard range second keeps overlays and menus responsive general queue cap 20 messages by default 8192 after `WM_USER + 123` JUCE-heavy redraw pages still settle Why the split matters input avoids redraw backlogs, but the bridge still guarantees a bounded return to the native host loop

5. RT ownership and priority mapping

The fork removes the old assumption that yabridge should decide Linux FIFO priorities itself.

Instead:

The split is intentional:

Thread class Helper Practical result
Audio callback workers set_thread_time_critical() maps to Wine-NSPA’s top audio band (TIME_CRITICAL)
Dispatch / control / parameter loops set_thread_realtime_idle() stays inside the Win32 realtime class so child RT inheritance works, but below the audio band
LoadLibrary, plugin construction, init brackets ScopedRealtimeIdleBoost child worker threads inherit RT entitlement without running heavyweight init at the audio ceiling

That has two practical effects:

  1. Wine-NSPA, not yabridge, owns the mapping from Win32 priority bands to Linux scheduler state.
  2. Plugin worker threads created during LoadLibrary, static initialization, or plugin initialization inherit an RT-capable parent at the point where the kernel checks entitlement.

The fork also removes two older mechanisms that became redundant once the kernel-side PI handoff was in place:

Removed assumption Replacement
direct sched_setscheduler(..., 5) promotion in wine-host SetThreadPriority() routed through Wine-NSPA
10-second userspace priority resync and per-request new_realtime_priority fields per-callback PI handoff through the shared rendezvous

The point is to let the DAW thread’s effective priority reach the Wine-host worker during the callback, not to mirror scheduler state in userspace on a timer.


// Current pattern: audio workers at the ceiling, control/init work in the
// lowest Win32 RT band, still inside REALTIME_PRIORITY_CLASS.
void audio_worker_entry() {
    yabridge::nspa::set_thread_time_critical();
    run_plugin_audio_loop();
}

void dispatch_loop_entry() {
    yabridge::nspa::set_thread_realtime_idle();
    run_dispatch_and_control_loop();
}

HMODULE module = yabridge::nspa::load_library_rt(dos_path);

6. Per-callback transport

The hot path uses a dedicated AudioControlShm rendezvous region with one request lock/cond pair and one reply lock/cond pair.

The key invariant is that the cross-process lock is not held across the plugin’s callback body.

Audio callback rendezvous: lock only the state transition, never the plugin body DAW audio thread 1. fill request payload 2. lock request side 3. publish `REQUEST_READY` 4. signal request cond 5. unlock request side 6. wait on reply cond request side `req_lock` + `req_cv` state + direct envelope fields bounded payload / fallback metadata reply side `reply_lock` + `reply_cv` reply payload + status wake producer once reply is ready wine-host audio worker 1. wake with PI on request cond 2. copy request into local buffer 3. unlock request side 4. run plugin callback 5. lock reply side 6. publish `REPLY_READY` 7. signal reply cond Load-bearing invariant plugin `process()` / `processReplacing()` runs after the request side is unlocked and before the reply side is locked so no cross-process mutex spans arbitrary plugin code Current direct-envelope contract layout version 9, creator/peer version match required direct envelope active only when both sides agree VST2: `VstTimeInfo`; VST3: `ProcessContext` + event ring + param queues + reply envelope CLAP: transport + event ring + reply envelope fallback remains bounded and transparent when a block shape does not fit

This transport does two different jobs:

  1. Wait/ownership: the DAW thread and Wine-host worker hand off one audio callback with cross-process priority inheritance.
  2. Payload carriage: fixed-layout hot metadata can stay in the shared region directly, while oversized or variable cases still have a bounded fallback.

The direct-layout work is what made the remaining serialization cost small enough to stop dominating the bridge:

On a representative ACE VST3 capture with the direct envelope enabled, the remaining bitsery encode/decode surface dropped to roughly 0.10% of yabridge-host CPU, while pi_mutex_lock itself sat at roughly 0.01%.


// Shape only: publish request, release the cross-process lock before plugin
// code, then publish reply under the reply-side lock.
pi_mutex_lock(&layout->req_lock);
write_request(layout, request);
layout->state = RequestReady;
pi_cond_signal(&layout->req_cv);
pi_mutex_unlock(&layout->req_lock);

pi_mutex_lock(&layout->reply_lock);
while (layout->state != ReplyReady)
    pi_cond_wait(&layout->reply_cv, &layout->reply_lock);
read_reply(layout, reply);
pi_mutex_unlock(&layout->reply_lock);

7. Current format coverage

The fork uses one design, but not every plugin API needs the exact same attachment points.

Format Hot callback path What stays on the older control path
VST2 dedicated L2 audio rendezvous for processReplacing() / audio processing, plus a PI mutex on the next-buffer MIDI queue dispatcher/control socket traffic and non-audio host callbacks
VST3 per-instance L2 rendezvous for IAudioProcessor::process() object construction, editor/control traffic, and other infrequent interfaces
CLAP per-instance L2 rendezvous for clap_plugin->process() params/state/control surfaces outside the process callback

Fixed-layout metadata coverage also differs slightly by format:

Format Direct metadata carried in the shared envelope
VST2 VstTimeInfo and the bounded reply path
VST3 ProcessContext, fixed-shape event ring, bounded parameter queues, response-side output events and parameter queues
CLAP clap_event_transport_t, fixed-shape event ring, and response-side output events

The envelope path is the normal path. The explicit runtime gate that used to enable or disable the NSPA L2 transport was removed; the bridge uses the fast path by default and falls back only when a particular callback shape does not fit the bounded region.

Format-specific shared-envelope coverage VST2 dedicated audio rendezvous direct `VstTimeInfo` request fields bounded reply path VST3 per-instance process rendezvous `ProcessContext` fixed event ring + bounded param queues response-side output events and param queues CLAP per-instance process rendezvous `clap_event_transport_t` event ring + response-side out-events Shared rule fixed-layout hot metadata stays in the shared region; oversized or irregular shapes still fall back safely

8. Startup and lifecycle hardening

The yabridge fork also tightens the non-steady-state edges that matter in real plugin-hosting sessions.

Area Final behavior
Module path exposure module loads use DOS-path conversion so plugin-side GetModuleFileNameW() sees a parseable Windows path instead of a raw \\\\?\\unix\\... host path
Plugin load entitlement LoadLibrary and selected plugin-init calls are bracketed with ScopedRealtimeIdleBoost, so child worker threads inherit RT entitlement without pinning heavyweight init work at the audio ceiling
Cold wineserver startup the host side pre-warms wineserver before launching yabridge-host, avoiding a class of cold-spawn failures in hosts that hit the first Wine launch of the session
Host startup failure startup failure no longer aborts the native DAW; the bridge closes sockets and returns an error instead of taking the whole host down
DAW exit detection pidfd-based watchdog wakes on parent exit instead of relying only on a coarse polling timer
Stale artifacts endpoint names carry a PID sentinel, and plugin-lib initialization performs one-shot orphan cleanup in ${XDG_RUNTIME_DIR} and /dev/shm

These are not separate features from the audio transport. They are what make the transport survivable in long DAW sessions, repeated scans, and crash or SIGKILL scenarios.


9. Relationship to Wine-NSPA

Yabridge-NSPA is not part of the Wine tree, but it is intentionally coupled to Wine-NSPA in a few load-bearing places.

The practical view is simple: Wine-NSPA makes the Wine side RT-correct, and Yabridge-NSPA carries those rules across the native/Linux-to-Wine plugin boundary.


10. References

Source