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.
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.
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 |
WM_X11DRV_NSPA_EMBED_WINDOWSend 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.
WM_X11DRV_NSPA_EMBED_DONEWine posts WM_X11DRV_NSPA_EMBED_DONE back to the same HWND after:
XReparentWindow()XMapWindow()The message is asynchronous. It lands on the HWND’s message queue and is observed when the host’s Win32 message pump runs.
The protocol does not make the host passive. After the embed handoff:
XMoveResizeWindow() or equivalentSetWindowPos() so Wine’s Win32-side rects track the
correct absolute screen locationThat split is deliberate: Wine keeps Win32 state authoritative, while the host keeps the X11 embed tree authoritative.
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.
The runtime sequence is:
WM_X11DRV_NSPA_EMBED_WINDOWwinex11.drv reparents the child at the supplied parent-relative originWM_X11DRV_NSPA_EMBED_DONETwo details matter:
This is why the protocol replaces a pile of host-local X11 bookkeeping instead
of just hiding one XReparentWindow() call.
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:
SetWindowPos() when the embedded child moves in screen spaceXMoveResizeWindow() or xcb_configure_window() on the
embedded X11 child when the parent-relative X11 geometry changesnspa_embedded windows so the embedder stays authoritative at the X11 layerWM_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 |
Two follow-on pieces keep embedded editors usable under host motion and plugin cursor save/restore behavior.
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.
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.
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.
include/wine/nspa_x11_embed.hdlls/win32u/message.cdlls/winex11.drv/window.cdlls/winex11.drv/mouse.c