Wine-NSPA – NSPA X11 Embed Protocol

This page documents Wine-NSPA’s X11 embed protocol for winelib hosts that need to place a Wine HWND-backed X11 child under a foreign Linux parent window.

Table of Contents

  1. Overview
  2. Public contract
  3. Embed lifecycle
  4. Geometry and resize ownership
  5. Consumer pump discipline
  6. Input and drag correctness
  7. Current consumers
  8. References

1. Overview

The embed protocol gives a winelib host one supported way to reparent a Wine window into a foreign X11 parent without reimplementing Wine’s embedded-window bookkeeping itself.

This page is X11-specific. Wayland embedding uses a separate Wine-NSPA contract because host and Wine surfaces must share one wl_display and the native parent relation is a wl_subsurface, not an X11 reparent.

The problem it solves is not just XReparentWindow(). A working host also needs Wine to:

Wine-NSPA exposes that as a small driver-private message contract instead of making each host rebuild the old wrapper-window and synthetic event machinery.


2. Public contract

The supported interface lives in include/wine/nspa_x11_embed.h.

Symbol Meaning
WM_X11DRV_NSPA_EMBED_WINDOW request atomic embed under an external X11 parent
WM_X11DRV_NSPA_EMBED_DONE async completion message posted after reparent + map + state settle

2.1 WM_X11DRV_NSPA_EMBED_WINDOW

Send the message to the top-level Wine HWND that owns the X11 child.

Field Meaning
wParam X11 Window id of the foreign parent
lParam MAKELPARAM(peerX, peerY) using parent-relative coordinates

peerX and peerY are load-bearing. Wine locks embedded-mode position state when the window flips into embedded mode, so the initial reparent has to use the host’s intended parent-relative origin.

2.2 WM_X11DRV_NSPA_EMBED_DONE

Wine posts WM_X11DRV_NSPA_EMBED_DONE back to the same HWND after:

The message is asynchronous. It lands on the HWND’s message queue and is observed when the host’s Win32 message pump runs.

2.3 Consumer responsibilities

The protocol does not make the host passive. After the embed handoff:

That split is deliberate: Wine keeps Win32 state authoritative, while the host keeps the X11 embed tree authoritative.

2.4 Minimal host-side flow

The working host-side sequence is small, but each step is load-bearing:


SendMessageW(embedHwnd, WM_X11DRV_NSPA_EMBED_WINDOW,
             (WPARAM) foreignParent,
             MAKELPARAM(peerX, peerY));

for (;;) {
    while (PeekMessageW(&msg, nullptr, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    while (PeekMessageW(&msg, nullptr, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    const auto deadlineMs = now_ms() + 4;
    while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_X11DRV_NSPA_EMBED_DONE)
            goto embed_done;
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
        if (now_ms() >= deadlineMs)
            break;
    }

    poll_linux_host_events();
}

embed_done:
    ;

After that completion edge, the host still owns X11 geometry:


SetWindowPos(embedHwnd, 0, screenX, screenY, width, height, SWP_NOZORDER);
XMoveResizeWindow(display, wineChild, peerX, peerY, width, height);

That split is the protocol. SetWindowPos() keeps Wine’s Win32-side rects honest; XMoveResizeWindow() keeps the foreign-parent X11 tree honest.

NSPA X11 embed handshake winelib host owns foreign X11 parent computes `peerX`, `peerY` calls `SendMessageW()` embed request `WM_X11DRV_NSPA_EMBED_WINDOW` `wParam = parent X11 Window` `lParam = MAKELPARAM(peerX, peerY)` win32u routing driver-private message range message reaches `winex11.drv` from PE or winelib caller `winex11.drv` reparent + mark embedded map child X11 window post `EMBED_DONE` Stable result Wine's Win32-side rects keep tracking the child HWND the host owns the foreign X11 parent and future X11 sizing the embed handshake ends with `WM_X11DRV_NSPA_EMBED_DONE` instead of a timing guess

3. Embed lifecycle

The runtime sequence is:

  1. the host creates a real top-level Wine HWND
  2. the host resolves the backing X11 child and the foreign X11 parent
  3. the host sends WM_X11DRV_NSPA_EMBED_WINDOW
  4. winex11.drv reparents the child at the supplied parent-relative origin
  5. Wine flips the window to embedded mode and records the foreign parent
  6. Wine maps the X11 child and posts WM_X11DRV_NSPA_EMBED_DONE

Two details matter:

This is why the protocol replaces a pile of host-local X11 bookkeeping instead of just hiding one XReparentWindow() call.


4. Geometry and resize ownership

After the embed handoff, geometry ownership is intentionally split.

Path Owner Why
parent-relative X11 position and size host the host owns the foreign parent window tree
Win32 RECT state, hit-testing, and message dispatch Wine the plugin still runs as a Win32 window

In practice that means:

Geometry split after embedding host layout peer-relative area changes toolbar offsets, parent moves, resize callbacks host decides X11 child geometry Win32 side `SetWindowPos()` updates Wine rects mouse hit-testing and plugin window messages stay correct no host-local synthetic message translation needed X11 side host calls `XMoveResizeWindow()` Wine suppresses its own X11 move/size emission foreign parent remains authoritative Why the split exists Wine owns Win32 semantics, but the host owns the foreign X11 parent tree keeping both layers authoritative in their own domain avoids duplicate move logic and stale geometry

5. Consumer pump discipline

WM_X11DRV_NSPA_EMBED_DONE is only one part of the consumer contract. The host-side message pump also matters.

The incorrect shape is an unbounded while (PeekMessage) drain on the same thread that must return to X11 polling. A redraw-heavy plugin can keep the queue non-empty with WM_TIMER and WM_PAINT, which means:

The required shape is:

Rule Why
pump the whole thread queue, not just the top-level HWND child editor windows carry paint, mouse, and keyboard traffic
dispatch mouse and keyboard ranges ahead of the general drain redraw-heavy backlogs should not bury interactive input
bound the general drain by time or message count the host thread must return to X11 polling predictably
Bounded consumer pump for embedded Wine HWNDs host tick JUCE timer callback or host event-loop turn same thread also polls X11 input-first pass drain `WM_MOUSEFIRST..LAST` then `WM_KEYFIRST..LAST` whole thread queue, `hwnd = nullptr` general drain all remaining Win32 messages bounded by time or count stop after `EMBED_DONE` or the budget return to host poll service X11 button, motion, key keep editor interaction live host retains wall-clock fairness Current consumer shapes JUCE-NSPA: input-first mouse/keyboard passes + 4 ms general-drain budget Lulada: inherits JUCE-NSPA exactly Yabridge-NSPA: input-first passes + 20-message cap, extended to 8192 for JUCE floods Failure mode avoided an unbounded general drain can loop forever on `WM_TIMER` + `WM_PAINT` and starve the host's X11 poll

6. Input and drag correctness

Two follow-on pieces keep embedded editors usable under host motion and plugin cursor save/restore behavior.

6.1 Host-drag rect propagation

When the host moves the embedded child at the X11 layer, Wine also updates the Win32-side rect state for the embedded HWND. That keeps:

in sync with the host’s actual X11 layout.

6.2 Button-state freeze and cursor-jump suppression

During click-drag-release sequences, plugins may save screen coordinates on mouse-down and call SetCursorPos() on release. Mid-drag rect churn can make that restore point wrong.

Wine-NSPA therefore:

The result is not a generic mouse optimization. It is targeted correctness for embedded Win32 editors living inside foreign X11 hosts.


7. Current consumers

The protocol is currently used by Wine-NSPA winelib hosts that embed Win32 plugin editors into native Linux UI trees.

Consumer Use
JUCE-NSPA on X11 peers, WineHWNDEmbedComponent creates a real Wine HWND, pumps the full thread queue with input-first mouse/keyboard passes, and bounds the general drain to 4 ms per timer tick
Lulada uses this path on X11 sessions for VST2, VST3, and CLAP editors; Wayland sessions use the Wayland embed protocol
Yabridge-NSPA keeps a wrapper window as the immediate host-owned parent, hands the Wine child off through the atomic embed message, and uses input-first passes plus a capped general drain in HostBridge::handle_events()

The protocol stays intentionally small. It is a Wine-side primitive for hosts that already manage a foreign X11 parent, not a full host toolkit.


8. References