Wine-NSPA – JUCE-NSPA

This page documents the JUCE fork that builds JUCE as a Linux winelib host for Windows VST2, VST3, and CLAP plugins on top of Wine-NSPA.

Table of Contents

  1. Overview
  2. Why a separate fork exists
  3. Build and runtime pivot
  4. Plugin loading and discovery
  5. Dispatch, synchronization, and async load
  6. Host windowing and editor embedding
  7. CLAP GUI ownership
  8. Relationship to Wine-NSPA
  9. Current scope
  10. References

1. Overview

JUCE-NSPA is a JUCE fork that runs JUCE as a Linux binary while still hosting Windows plugin binaries in-process through Wine.

The load-bearing changes are:

JUCE-NSPA: framework-level winelib substrate for Windows plugin hosting framework responsibilities, not app-specific host policy Linux host side JUCE event loop, peer tree, native parent ownership host windows stay native Linux objects editor parents and bounds originate here native shell stays native JUCE-NSPA host substrate winegcc / wineg++ build, Linux winelib process PE module loading through `LoadLibraryW()` VST2 `ms_abi`, VST3 `__stdcall`, CLAP `CLAP_ABI` shim one-shot `CreateThread` load worker + per-instance dispatch `PiMutex` / `PiCond` wrappers + `WineHWNDEmbedComponent` framework-level Win32 ownership lives here Windows plugin side VST2 `.dll`, VST3 PE module, CLAP `.clap` PE DLL Win32 editor HWNDs and child windows plugin callbacks still observe Win32 rules same process, Wine-backed code Editor embed and thread ownership `WineHWNDEmbedComponent` creates a real Wine HWND and selects X11 or Wayland embed X11 reparents the Wine child; Wayland installs a `wl_subsurface` CLAP editor setup runs directly on the JUCE message thread to avoid deadlock with the same Win32 pump that must service `set_parent()` and `show()` framework owns the Linux host <-> Win32/HWND seam Scope Linux host structure stays native, but plugin load, editor HWNDs, and callback ABI still follow Win32 rules inside the same process

JUCE-NSPA is framework infrastructure. It exists so application ports can reuse one host substrate instead of re-solving Win32 load, thread, and embed rules per application.


2. Why a separate fork exists

Upstream JUCE already supports Linux hosts and Windows hosts, but it does not provide the combined shape this stack needs:

That is why this work lives as a fork instead of an application-side patch set.

Concern Generic JUCE host assumption JUCE-NSPA shape
Binary model native Linux host or native Windows host Linux winelib host that still drives Win32 plugin code
Plugin module loading native Linux .so or Windows-native host side Windows PE modules through Wine LoadLibraryW()
Format surface VST2/VST3 plus native-Linux assumptions Windows VST2, VST3, and CLAP only
GUI embedding toolkit-native child windows or native Windows HWNDs Wine HWND-backed editor embedded under a Linux X11 or Wayland peer
Threading model generic async worker or message-thread deferred sync load Win32-registered worker for plugin creation, then per-instance dispatch
Host-side sync generic host mutex / condvar choices PI-aware wrappers where the winelib dispatcher layer needs Wine-NSPA-compatible behavior

The fork therefore provides a framework substrate for Linux applications that want to host Windows plugins through Wine-NSPA without solving the same ABI, thread-registration, and native embed issues independently.


3. Build and runtime pivot

JUCE-NSPA changes JUCE from “build a Linux host for Linux plugins” or “build a Windows host for Windows plugins” into “build a Linux binary that still drives Win32 plugin code in-process.”

The pivot has three parts:

Area Current behavior
Toolchain winelib build via winegcc / wineg++ instead of a native Linux or native Windows host build
ABI surface Windows-facing plugin boundaries use Win32 calling conventions where needed, including VST3 PLUGIN_API = __stdcall, audited VST2 ms_abi call sites, and CLAP CLAP_ABI under winelib x86_64
Runtime assumptions Wine provides the PE loader, Win32 thread state, and HWND/message model inside a Linux process

The fork is intentionally Linux-only and x86_64-only. That is not just build policy. The host shape depends on Wine’s Linux winelib runtime, and the current CLAP/VST ABI shims are specifically authored for the x86_64 Win32 boundary.


4. Plugin loading and discovery

The plugin-loading surface is split by format.

Format Current behavior
VST3 loads single-file Windows PE modules through Wine LoadLibraryW() with Winelib-safe VST3 host fixes
VST2 scans .dll, rejects non-PE candidates, and routes entry points through audited ms_abi call sites
CLAP hosts Windows .clap PE DLLs through a dedicated CLAP format stack; there is no native-Linux .so CLAP path in this fork

4.1 VST3

The VST3 side loads PE plugin modules through Wine’s LoadLibraryW() path. That is paired with:

4.2 VST2

The VST2 side adds the corresponding Windows-module assumptions:

4.3 CLAP

The CLAP side adds a third Windows plugin format to the same winelib host substrate.

The current implementation is split in two:

Important details:

The headless CLAP implementation already covers the load-bearing runtime surfaces:

acceptsMidi() and producesMidi() are derived from the plugin’s note-port shape, not hardcoded.

4.4 CLAP event and transport bridge

The CLAP audio path does more than just “call process.”

At block time:

  1. JUCE MidiBuffer events are translated into CLAP events.
  2. The bridge picks note encoding from the first input port’s preferred dialect:
  3. fillTransportFromPlayHead() builds clap_event_transport_t from JUCE’s AudioPlayHead.
  4. Plugin output events are translated back into JUCE MIDI after process() returns.

Current output translation covers:

Current accepted-but-skipped surfaces include:

Those are omitted because they do not fit JUCE’s MidiBuffer model directly.


5. Dispatch, synchronization, and async load

JUCE-NSPA does not run all plugin work through one global helper thread. Instead, the current host model is per-instance after creation succeeds.

Surface Current behavior
VST3 lifecycle + audio per-instance Win32 dispatcher
VST2 host plumbing per-instance dispatcher plus shared winelib support code
CLAP lifecycle + audio per-instance dispatcher, with CLAP main-thread and audio-thread roles tracked explicitly
Shared sync wrappers vendored PiMutex / PiCond wrappers over Wine-NSPA-compatible rtpi.h
Async plugin creation one-shot Wine CreateThread worker, callback returned to the JUCE message thread

5.1 PI-aware sync where the host needs it

The vendored PiMutex / PiCond wrappers exist for the host-side dispatcher and shared sync surfaces that need Wine-NSPA-compatible priority inheritance. They are not a claim that JUCE’s entire locking surface was replaced.

JUCE’s own POSIX CriticalSection path already uses PTHREAD_PRIO_INHERIT on RT builds. The dedicated PiMutex / PiCond wrappers are for the host’s winelib-facing synchronization layer, including process-shared mode for the cases that need a shared-memory rendezvous shape.

5.2 Async load must use a Win32-registered thread

Under winelib, asynchronous plugin creation cannot use a plain pthread worker.

The reason is architectural:

So JUCE-NSPA changes createPluginInstanceAsync() to:

  1. spawn a one-shot worker with Wine CreateThread
  2. run synchronous createPluginInstance() on that worker
  3. hop the completion callback back to the JUCE message thread via MessageManager::callAsync

That keeps the documented JUCE callback contract intact while moving the actual plugin load off the message thread.

5.3 Per-instance ownership stays isolated

Per-instance dispatch avoids cross-plugin queue coupling. One plugin instance’s lifecycle or audio callback work does not need to share a generic host queue with the next instance just because both live inside the same process.

Async creation on a Win32 worker, then per-instance dispatch JUCE message thread host requests async create does not block UI while module loads completion callback returns here one-shot load worker Wine `CreateThread` valid TEB / SEH / loader state runs `createPluginInstance()` safe place for `LoadLibraryW()` shared host sync layer `PiMutex` / `PiCond` wrappers used where host-facing sync needs Wine-NSPA PI behavior live instance VST2 / VST3 / CLAP owns its own Win32 lane no global plugin queue Why the split exists plugin creation needs Wine-owned thread registration; steady-state callbacks need per-instance ownership instead of one shared host queue

6. Host windowing and editor embedding

Editor hosting uses the same WineHWNDEmbedComponent abstraction on both Linux windowing backends. The component creates a real Wine HWND, then hands that HWND to the Wine-NSPA driver path that matches the current JUCE-NSPA peer.

Host peer Embed path Native object owned by the host
X11 WM_X11DRV_NSPA_EMBED_WINDOW / WM_X11DRV_NSPA_EMBED_DONE foreign X11 parent Window
Wayland WM_WAYLANDDRV_NSPA_EMBED_WINDOW / WM_WAYLANDDRV_NSPA_EMBED_DONE foreign parent wl_surface on the adopted host wl_display

The completion edge is explicit on both backends. Wine posts the matching *_EMBED_DONE message back to the same HWND after the native driver has settled the embed handoff.

The component also owns the Win32 message pump for the embedded thread queue. It does not drain only the top-level editor HWND, because the child windows below that editor carry the actual WM_PAINT, mouse, and keyboard traffic.

The current pump shape is:

That split keeps the JUCE message thread returning to its native host event poll instead of spinning forever on plugin-generated WM_TIMER / WM_PAINT backlogs, while still preserving responsive input under heavy redraw load.

Geometry ownership stays split by layer:

6.1 Current pump contract

The queue pump is a host-side contract, not a best-effort hint.

Pump surface Current rule Why
Queue scope whole thread queue, not only the top-level editor HWND child controls, popups, and helper HWNDs carry real input and paint traffic
Mouse pass drain WM_MOUSEFIRST .. WM_MOUSELAST first pointer capture, hover, and resize feedback do not sit behind redraw backlogs
Keyboard pass drain WM_KEYFIRST .. WM_KEYLAST second text entry and shortcuts stay responsive
General drain bounded to 4 ms per host turn JUCE’s Linux event loop regains control even if the plugin floods WM_PAINT / WM_TIMER
Completion edge observe the backend *_EMBED_DONE message on the same queue embed completion is tied to the same message source as normal editor traffic

6.2 WineHWNDEmbedComponent responsibilities

Responsibility Current behavior
Win32 surface creation creates a real top-level Wine HWND with CreateWindowExW()
Native peer lookup resolves the backing X11 child or Wayland host parent for the JUCE-NSPA peer
Embed handoff sends the X11 or Wayland Wine-NSPA embed request for the HWND
Completion fence handles the backend *_EMBED_DONE message if the host wants a deterministic “embed settled” point
Win32 pump drains the whole thread queue with input-first mouse/keyboard passes, then a 4 ms general-drain budget
Geometry sync uses SetWindowPos() for Wine rects and the native peer backend for host-relative placement

6.3 Wayland display adoption

The Wayland path has one extra invariant: host and Wine surfaces must belong to the same wl_display. JUCE-NSPA’s Wayland peer layer exposes the process display, and the winelib host publishes it before Wine creates plugin editor surfaces. winewayland.drv then validates the display match before it turns the Wine HWND surface into a subsurface of the host parent.

This keeps the Wayland path native. It does not force an X11 wrapper window when the host shell is already running on Wayland.


7. CLAP GUI ownership

The CLAP editor path uses the same embed substrate, but it adds one important threading rule: the clap.gui lifecycle calls are performed directly on the JUCE message thread.

That is deliberate. Routing them through the per-instance dispatcher would deadlock the same thread that must keep the Win32 pump alive for set_parent() and show().

Current CLAP editor setup:

  1. resolve clap.gui on the plugin’s main thread
  2. is_api_supported(CLAP_WINDOW_API_WIN32, false)
  3. create(CLAP_WINDOW_API_WIN32, false)
  4. get_size()
  5. create and show WineHWNDEmbedComponent
  6. set_parent() with the embed component’s Wine HWND
  7. show()
  8. drive plugin-requested resize/close through a 30 Hz timer

Teardown is symmetrical: hide() then destroy() on the same JUCE message thread, while WineHWNDEmbedComponent handles the deferred native-side cleanup.

CLAP editor handshake on the JUCE message thread JUCE message thread 1. resolve `clap.gui` 2. `is_api_supported(WIN32)` 3. `create(WIN32)` 4. `get_size()` 5. add embed component 6. `set_parent(hwnd)` 7. `show()` Embed component creates Wine HWND resolves native host parent sends backend `EMBED_WINDOW` input-first + 4 ms bounded drain tracks `EMBED_DONE` timer also drives resize sync Wine embed path reparents X11 child or installs `wl_subsurface` settles embedded native state keeps Win32 rects authoritative posts `EMBED_DONE` CLAP plugin GUI Win32 parent API only plugin-requested resizes land in atomics 30 Hz timer applies them to the host plugin-close request closes the editor window Why this stays on one thread `set_parent()` and `show()` need the same message-thread pump that services the embedded Wine HWND, so dispatcher-hopping would block the wrong thread

8. Relationship to Wine-NSPA

JUCE-NSPA depends on specific Wine-NSPA surfaces.

Surface Use in JUCE-NSPA
WM_X11DRV_NSPA_EMBED_WINDOW / WM_X11DRV_NSPA_EMBED_DONE embeds Wine editor windows under a Linux JUCE-NSPA peer without reimplementing Wine’s embedded-window bookkeeping
WM_WAYLANDDRV_NSPA_EMBED_WINDOW / WM_WAYLANDDRV_NSPA_EMBED_DONE embeds Wine editor windows under a Wayland JUCE-NSPA peer through a shared host wl_display and wl_subsurface parentage
Wine LoadLibraryW() path loads Windows VST2, VST3, and CLAP modules in-process
Wine CreateThread path gives async plugin creation a Win32-registered worker thread with valid TEB/SEH/loader state
vendored rtpi.h wrappers provides PiMutex / PiCond wrappers for the host-side dispatcher layer, including a process-shared-compatible shape
Win32 runtime under Wine-NSPA supplies the Win32 message queue and window semantics the embedded editor path still relies on

JUCE-NSPA is therefore not a generic Wine wrapper layer. It is a framework fork written around Wine-NSPA’s specific load, thread, sync, and embed rules.


9. Current scope

Area Current behavior
Host architecture Linux-only x86_64 winelib host
Plugin formats Windows VST2, VST3, and CLAP
Async instantiation one-shot Wine CreateThread worker, callback returned to the JUCE message thread
GUI embedding Wine HWND-backed editor windows inside X11 or Wayland Linux JUCE-NSPA peers
Current application consumer Lulada

JUCE-NSPA is a framework fork, not an end-user host by itself. Its value is that application ports can build on one winelib + Wine-NSPA substrate instead of re-solving the same module load, Win32 dispatch, and editor embed problems separately.


10. References