This page documents the yabridge fork aligned to Wine-NSPA’s RT scheduling, priority-inheritance, and userspace-sync model.
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:
pi_mutex_t + pi_cond_t instead of a
socket-only callback path.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.
Upstream yabridge targets stock Wine plus generic Linux scheduling and IPC. Wine-NSPA changes the assumptions under that bridge:
SetPriorityClass() and
SetThreadPriority(), not direct sched_setscheduler() callsrtpi.h layout on both the native
and Winelib sides| 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.
Yabridge-NSPA still has the same two major halves:
yabridge-host Winelib process that loads the Windows plugin moduleThe important change is what happens on the callback boundary.
AudioControlShm.yabridge-host side.That keeps the hard RT path narrow without rewriting the rest of yabridge’s control-plane logic.
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:
WM_X11DRV_NSPA_EMBED_WINDOW to the Wine editor HWNDThe older SubstructureRedirect handler path and related synthetic X11
translation logic are gone from the normal embed flow.
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.
| 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.
HostBridge::handle_events() is the consumer-side message pump for the editor
thread queue. Its current shape is:
WM_MOUSEFIRST .. WM_MOUSELAST firstWM_KEYFIRST .. WM_KEYLAST secondWM_USER + 123 appears, which is the JUCE flood
heuristic yabridge already carriedThat keeps two constraints aligned:
WM_PAINT / WM_TIMER traffic| 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 |
The fork removes the old assumption that yabridge should decide Linux FIFO priorities itself.
Instead:
yabridge-host sets REALTIME_PRIORITY_CLASS at process scopeset_thread_time_critical()set_thread_realtime_idle()ScopedRealtimeIdleBoostSCHED_OTHERThe 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:
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);
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.
This transport does two different jobs:
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);
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.
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.
Yabridge-NSPA is not part of the Wine tree, but it is intentionally coupled to Wine-NSPA in a few load-bearing places.
rtpi.h because both halves of the bridge must agree
on one pi_mutex_t / pi_cond_t layout. Mixing generic system librtpi with
Wine-NSPA’s header-only implementation is not a safe ABI choice.nspaASIO and winejack.drv solve Wine-native Windows audio APIs;
Yabridge-NSPA solves the Linux-DAW-to-Windows-plugin bridge.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.
yabridge/src/common/rtpi.h – vendored Wine-NSPA rtpi.hyabridge/src/common/pi_sync.h – C++ RAII wrappers for pi_mutex_t / pi_cond_tyabridge/src/common/audio-control-shm.{h,cpp} – shared L2 rendezvous region and direct-struct envelopeyabridge/src/wine-host/nspa_rt.h – Win32 priority helpers and scoped RT load bracketyabridge/src/wine-host/host.cpp – REALTIME_PRIORITY_CLASS process setupyabridge/src/wine-host/editor.{h,cpp} – wrapper-window ownership plus Wine-NSPA atomic embed handoffyabridge/src/wine-host/utils.{h,cpp} – pidfd watchdog registration and DOS-path conversion helpersyabridge/src/plugin/orphan-cleanup.{h,cpp} – PID-tagged endpoint cleanupyabridge/src/plugin/bridges/vst2* – VST2 bridge-side L2 useyabridge/src/plugin/bridges/vst3* – VST3 bridge-side L2 useyabridge/src/plugin/bridges/clap* – CLAP bridge-side L2 useyabridge/src/wine-host/bridges/vst2* – VST2 Wine-host audio worker and callback pathyabridge/src/wine-host/bridges/vst3* – VST3 Wine-host process callback pathyabridge/src/wine-host/bridges/clap* – CLAP Wine-host process callback pathaudio-stack.gen.html – Wine-native JACK / WASAPI / ASIO pathlibrtpi.gen.html – the PI mutex / condvar API Yabridge-NSPA vendorsntsync-userspace.gen.html – Wine-side wait/signal and userspace sync modelnspa-x11-embed-protocol.gen.html – atomic embed primitive used by the Wine-host editor patharchitecture.gen.html – system-level view of the larger Wine-NSPA stack