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.
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:
PiMutex / PiCond wrappers where the host-facing sync layer needs
Wine-NSPA-compatible PI behaviorWineHWNDEmbedComponent for VST2, VST3, and CLAP editors under native
Linux X11 or Wayland peersJUCE-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.
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.
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.
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 |
The VST3 side loads PE plugin modules through Wine’s LoadLibraryW() path.
That is paired with:
The VST2 side adds the corresponding Windows-module assumptions:
.dll, not .soms_abi where JUCE’s generic host
layer would otherwise assume the native Linux calling conventionThe CLAP side adds a third Windows plugin format to the same winelib host substrate.
The current implementation is split in two:
CLAPPluginFormatHeadless in juce_audio_processors_headlessCLAPPluginFormat in juce_audio_processors for GUI/editor supportImportant details:
CLAP_ABI shim on x86_64 so host/plugin callbacks
follow the Windows ABI instead of native SysVJUCE_PLUGINHOST_CLAP gates the format wiring.clap PE DLLs; there is no Linux .so
CLAP loader path hereThe headless CLAP implementation already covers the load-bearing runtime surfaces:
AudioPlayHeadMidiBufferacceptsMidi() and producesMidi() are derived from the plugin’s note-port
shape, not hardcoded.
The CLAP audio path does more than just “call process.”
At block time:
MidiBuffer events are translated into CLAP events.CLAP_EVENT_NOTE_ON/OFFCLAP_EVENT_MIDIfillTransportFromPlayHead() builds clap_event_transport_t from JUCE’s
AudioPlayHead.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.
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 |
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.
Under winelib, asynchronous plugin creation cannot use a plain pthread worker.
The reason is architectural:
LoadLibraryW() and PE module initialization need Wine-owned thread statestd::thread / pthread workerSo JUCE-NSPA changes createPluginInstanceAsync() to:
CreateThreadcreatePluginInstance() on that workerMessageManager::callAsyncThat keeps the documented JUCE callback contract intact while moving the actual plugin load off the message thread.
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.
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:
WM_MOUSEFIRST .. WM_MOUSELASTWM_KEYFIRST .. WM_KEYLASTThat 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:
SetWindowPos() updates Wine’s Win32-side window rectsThe 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 |
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 |
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.
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:
clap.gui on the plugin’s main threadis_api_supported(CLAP_WINDOW_API_WIN32, false)create(CLAP_WINDOW_API_WIN32, false)get_size()WineHWNDEmbedComponentset_parent() with the embed component’s Wine HWNDshow()Teardown is symmetrical: hide() then destroy() on the same JUCE message
thread, while WineHWNDEmbedComponent handles the deferred native-side cleanup.
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.
| 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.
modules/juce_audio_processors_headless/format/juce_AudioPluginFormat.cppmodules/juce_audio_processors_headless/format_types/juce_CLAPPluginFormatHeadless.hmodules/juce_audio_processors_headless/format_types/juce_CLAPPluginFormatImpl.hmodules/juce_audio_processors_headless/format_types/clap/juce_clap_winelib_abi.hmodules/juce_audio_processors/format_types/juce_CLAPPluginFormat.cppmodules/juce_audio_processors/format_types/juce_VSTPluginFormat.cppmodules/juce_audio_processors/format_types/juce_VST3PluginFormat.cppmodules/juce_audio_processors_headless/format_types/juce_winelib_pi_sync.hmodules/juce_gui_extra/embedding/juce_WineHWNDEmbedComponent.hmodules/juce_gui_extra/native/juce_WineHWNDEmbedComponent_linux.cppmodules/juce_gui_basics/native/juce_WaylandWindowSystem.hmodules/juce_gui_basics/native/juce_WaylandWindowSystem.cppmodules/juce_gui_basics/native/juce_Windowing_wayland.cppmodules/juce_opengl/native/juce_OpenGL_wayland.h