react-x11 2.51.0 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
package/docs/wayland.md
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# A native Wayland backend
|
|
2
|
+
|
|
3
|
+
**Status: research RFC, since built.** The backend it plans landed in #548
|
|
4
|
+
as `src/wayland/`, opt-in; [wayland-backend.md](wayland-backend.md) is the
|
|
5
|
+
record of what was built, and where the two disagree that one ran. The RFC
|
|
6
|
+
below was written before any code, 2026-08, against react-x11 2.2.1,
|
|
7
|
+
ntk 8.7.0, node-x11 4.1.0, x11-dri 0.6.0; the line counts and library
|
|
8
|
+
versions below are measurements from that date, not estimates. Two upstream
|
|
9
|
+
issues were filed from this research
|
|
10
|
+
([x11-dri#19](https://github.com/sidorares/node-x11-dri/issues/19),
|
|
11
|
+
[x11-dri#20](https://github.com/sidorares/node-x11-dri/issues/20)) and one
|
|
12
|
+
upstream capability landed just before it
|
|
13
|
+
([node-x11#290](https://github.com/sidorares/node-x11/pull/290)).
|
|
14
|
+
|
|
15
|
+
## What this is, and is not
|
|
16
|
+
|
|
17
|
+
This is a plan for a **second backend, not a migration**. X11 stays a
|
|
18
|
+
first-class target: the remote case ([remote.md](remote.md)) is the
|
|
19
|
+
flagship reason this project exists, old X11-only systems stay supported,
|
|
20
|
+
and nothing below proposes touching how the X11 path works. The question
|
|
21
|
+
this document answers is what it would take to run the same React tree
|
|
22
|
+
natively on a Wayland compositor — no Xwayland in the picture — on the
|
|
23
|
+
systems where that is the better experience.
|
|
24
|
+
|
|
25
|
+
The goals that shape every choice here are the project's standing ones:
|
|
26
|
+
|
|
27
|
+
- **a small, portable codebase** — no compiled dependencies where possible;
|
|
28
|
+
a native module is admissible where a runtime leaves no other way to
|
|
29
|
+
reach a required OS facility, or where a hot path is significantly faster
|
|
30
|
+
compiled and the pure path remains as fallback;
|
|
31
|
+
- **the whole range of desktop UI** — full applications, small utilities,
|
|
32
|
+
tray tools, panels and desktop-integration services, window managers;
|
|
33
|
+
- **the best experience available per system** — perceived performance
|
|
34
|
+
first (input-to-photon), then stability, then client CPU and memory.
|
|
35
|
+
|
|
36
|
+
One goal meets a hard wall immediately and it is better to say so up top:
|
|
37
|
+
**a window manager cannot be a Wayland client.** Compositing, stacking and
|
|
38
|
+
input routing are the compositor's own body, not a role a client can take.
|
|
39
|
+
`examples/wm.jsx` and the window-management half of the API remain X11-only
|
|
40
|
+
under this plan; the shell-adjacent use cases that _can_ be served
|
|
41
|
+
client-side (panels, docks, launchers, lock screens, tray tools) map to
|
|
42
|
+
their own protocols and are covered below. Being the compositor — a
|
|
43
|
+
react-x11 embedded in a wlroots- or Smithay-class host — is a different
|
|
44
|
+
project and out of scope here.
|
|
45
|
+
|
|
46
|
+
## What Wayland actually changes
|
|
47
|
+
|
|
48
|
+
Wayland's wire protocol is a **control plane**. Objects, requests, events;
|
|
49
|
+
interfaces defined in XML; two 32-bit words of header and 32-bit-aligned
|
|
50
|
+
arguments in host byte order
|
|
51
|
+
([the spec](https://wayland.freedesktop.org/docs/book/Protocol.html), and
|
|
52
|
+
[wayland-book.com](https://wayland-book.com/) as the readable tour). What it
|
|
53
|
+
does not carry — ever, not just at setup — is drawing. There is no
|
|
54
|
+
`PolyFillRectangle`, no RENDER `Composite`, no `PutImage`. A client hands
|
|
55
|
+
the compositor **finished pixels** by reference: a `wl_buffer` naming
|
|
56
|
+
either shared memory (`wl_shm`) or a GPU buffer
|
|
57
|
+
([linux-dmabuf](https://wayland.app/protocols/linux-dmabuf-v1)), attached
|
|
58
|
+
to a surface, damaged, committed. Surface state is double-buffered and
|
|
59
|
+
atomic — everything set since the last `commit` applies in one step.
|
|
60
|
+
|
|
61
|
+
That deletes the premise of ntk's rendering layer. It is tempting to read
|
|
62
|
+
this codebase as "canvas-like drawing code that happens to target X11", and
|
|
63
|
+
most planning mistakes would start there. ntk's 2d context is an **XRender
|
|
64
|
+
encoder**: fills are server-side trapezoids or rectangles, gradients are
|
|
65
|
+
`CreateLinearGradient` objects, text is `CompositeGlyphs` against a
|
|
66
|
+
server-resident glyph cache, `drawImage` is a `Composite`, and a `Surface`
|
|
67
|
+
is a pixmap+picture pair. The server rasterizes and composites; the client
|
|
68
|
+
describes. On Wayland the client rasterizes and composites, full stop —
|
|
69
|
+
which is why the largest workstream below is a renderer, not a protocol.
|
|
70
|
+
|
|
71
|
+
The other structural inversion: on X11 the client asks and the server
|
|
72
|
+
answers (`GetGeometry`, `QueryPointer`, properties); requests take effect
|
|
73
|
+
as issued. On Wayland the client **requests and the compositor decides**,
|
|
74
|
+
answering with `configure` events the client must ack. A window does not
|
|
75
|
+
know its own screen position, cannot place itself in globals, and cannot
|
|
76
|
+
read other windows. Several react-x11 features live or die on that line —
|
|
77
|
+
the inventory is below.
|
|
78
|
+
|
|
79
|
+
Two things get simpler, and they are not small:
|
|
80
|
+
|
|
81
|
+
- **Frame pacing.** ntk earns its frame clock the hard way — Present
|
|
82
|
+
completions, a fence fallback, a quantile estimator over completion
|
|
83
|
+
history to guess the refresh rate. Wayland hands the same thing over
|
|
84
|
+
directly: `wl_surface.frame` callbacks say "now is a good time to draw",
|
|
85
|
+
and [presentation-time](https://wayland.app/protocols/presentation-time)
|
|
86
|
+
reports when a committed frame actually reached the screen, with clock
|
|
87
|
+
id, refresh interval and flags. For a project whose stated first goal is
|
|
88
|
+
input-to-photon, this is the first target where **photon time is
|
|
89
|
+
observable**, not inferred.
|
|
90
|
+
- **Scale.** The four-generation ladder in [scale.md](scale.md) —
|
|
91
|
+
environment, XSETTINGS, `Xft.dpi`, RandR-millimetre audit — collapses to
|
|
92
|
+
`wl_output.scale` plus
|
|
93
|
+
[fractional-scale-v1](https://wayland.app/protocols/fractional-scale-v1)
|
|
94
|
+
with [viewporter](https://wayland.app/protocols/viewporter): authoritative,
|
|
95
|
+
per-output, and live.
|
|
96
|
+
|
|
97
|
+
## What already carries over
|
|
98
|
+
|
|
99
|
+
Measured against 2.2.1 (`src/`, excluding `.d.ts`):
|
|
100
|
+
|
|
101
|
+
| slice | lines | Wayland fate |
|
|
102
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------- |
|
|
103
|
+
| portable core — reconciler, yoga, styles/decorations parsing, palette, a11y tree + AT-SPI, keysym tables, compose, text selection model, D-Bus stack, anchors | ~16,200 | unchanged |
|
|
104
|
+
| `components/` + `frame/` + `refresh/` | ~8,700 | unchanged (`<Frame>`'s host seam changes) |
|
|
105
|
+
| X-touching modules | ~23,400 | see below |
|
|
106
|
+
|
|
107
|
+
The 23,400 overstates the rewrite. `nodes.js` is 11,515 lines of which only
|
|
108
|
+
`WindowNode` + `PopupNode` (~3,100) speak X — `BoxNode`, `TextNode`,
|
|
109
|
+
`ImageNode`, `CanvasNode`, `TextInputNode` and the layout/paint/hit-test
|
|
110
|
+
machinery drive a 2d context and never name the display system. The honest
|
|
111
|
+
rewrite surface is **~9–10k lines**: window realize/present, `events.js`,
|
|
112
|
+
`dnd.js`, `clipboard.js`, `screens.js`, `windowstate.js`, `scale.js`,
|
|
113
|
+
`keyboardstate.js`, `compositing.js`, `inputtime.js`, `startup.js`.
|
|
114
|
+
|
|
115
|
+
On the ntk side the split is starker. The portable half of a renderer
|
|
116
|
+
**already exists** there, built for the local-raster path and for glyphs:
|
|
117
|
+
|
|
118
|
+
- `rasterize.js` — analytic coverage accumulation (the font-rs algorithm),
|
|
119
|
+
outlines/polygons in, A8 out;
|
|
120
|
+
- `path.js` — Path2D, flattening, stroking, transforms;
|
|
121
|
+
- `shadow.js` — `blurCoverage`, separable gaussian, baked once;
|
|
122
|
+
- `text/` — fontkit faces, shaping, bidi, line breaking, layout — the
|
|
123
|
+
entire text stack short of the final composite;
|
|
124
|
+
- `fontconfig.js`, `imagedata.js` (premultiply/unpremultiply, pixel
|
|
125
|
+
layouts), `color.js`, `region.js`.
|
|
126
|
+
|
|
127
|
+
What does not exist is the piece XRender was doing: the **span
|
|
128
|
+
compositor** — A8 coverage × paint source → premultiplied ARGB with
|
|
129
|
+
Porter-Duff, clipping, gradient evaluation, transformed image sampling —
|
|
130
|
+
and a client-side **glyph atlas** where the server-side `GlyphSet` used to
|
|
131
|
+
be. That is the gap, whatever tier below fills it.
|
|
132
|
+
|
|
133
|
+
Desktop integration is the pleasant surprise: it was built D-Bus- and
|
|
134
|
+
portal-first, X as a fallback rung ([dbus.md](dbus.md),
|
|
135
|
+
[filedialog.md](filedialog.md), [appearance.md](appearance.md),
|
|
136
|
+
[eyedropper.md](eyedropper.md), [globalmenu.md](globalmenu.md), AT-SPI in
|
|
137
|
+
[accessibility.md](accessibility.md)). On Wayland those modules mostly
|
|
138
|
+
_lose a rung_ rather than gain one. The X-only integration channels
|
|
139
|
+
(`xsettings.js`, startup-notification's X path, the eyedropper's crosshair
|
|
140
|
+
grab) simply end at the ladder's portal rung.
|
|
141
|
+
|
|
142
|
+
## The transport: file descriptors, both ways
|
|
143
|
+
|
|
144
|
+
Everything in Wayland that moves bulk data moves a descriptor: the
|
|
145
|
+
compositor sends every client its keymap **as an fd**; `wl_shm` pools go
|
|
146
|
+
over as fds; clipboard and drag-and-drop hand the receiving side a pipe fd
|
|
147
|
+
and stream content through it. Descriptors ride SCM_RIGHTS ancillary data,
|
|
148
|
+
matched to `fd`-typed arguments **by order, not by message** — the spec is
|
|
149
|
+
blunt that "any byte of the stream, even the message header, may carry the
|
|
150
|
+
ancillary data". A transport that cannot receive descriptors is not a
|
|
151
|
+
limited Wayland client; it is not a Wayland client.
|
|
152
|
+
|
|
153
|
+
Where the runtimes stand:
|
|
154
|
+
|
|
155
|
+
| runtime | send fds | receive fds | verdict |
|
|
156
|
+
| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------- | ----------------------------- |
|
|
157
|
+
| Node | yes — node-x11's `fdpass.js` via `process.binding` internals | **no** — an arriving fd on a libuv-read socket aborts the process | needs a small native module |
|
|
158
|
+
| Bun | yes — `bun:ffi` → `sendmsg(2)` (node-x11 4.1.0) | **yes** — `bun:ffi` → `recvmsg(2)` + a poll thread (node-x11 4.1.0) | works today, pure JS + dlopen |
|
|
159
|
+
| Node+addon | yes | yes — an addon owning the socket end to end (~200 lines of C) | the eventual portable answer |
|
|
160
|
+
|
|
161
|
+
Node upstream is not coming:
|
|
162
|
+
[nodejs/node#53391](https://github.com/nodejs/node/issues/53391) — filed
|
|
163
|
+
specifically for Wayland — is closed "not planned". Bun landed in
|
|
164
|
+
[node-x11#290](https://github.com/sidorares/node-x11/pull/290)
|
|
165
|
+
(`lib/fdpass-bun.js`, 884 lines): `dlopen` of libc, no compiler and no
|
|
166
|
+
headers, two transports. The default keeps Bun's own socket and adds
|
|
167
|
+
`sendFds()`; `receiveFds: true` owns the descriptor end to end — `recvmsg`
|
|
168
|
+
on the main thread, readiness from a worker blocked in `poll(2)` — because
|
|
169
|
+
Bun's reader silently drops ancillary data it did not ask for. Three
|
|
170
|
+
properties make it a Wayland transport with zero changes: it connects to
|
|
171
|
+
**any** unix path, it is `net.Socket`-shaped with real backpressure, and
|
|
172
|
+
its ownership contract (sent fds consumed; received fds owned by the caller
|
|
173
|
+
once taken) is the one Wayland's object model wants.
|
|
174
|
+
|
|
175
|
+
**Decision: prototype on Bun.** It is the only runtime where the whole
|
|
176
|
+
stack stays JS-plus-dlopen today. The Node fallback is not abandoned — it
|
|
177
|
+
is a bounded ~200-line native module that owns one socket, and `x11-dri`
|
|
178
|
+
already exists as a home for exactly this kind of gap-filler — but it is
|
|
179
|
+
not on the critical path, and building Bun-first keeps the prototype free
|
|
180
|
+
of a compile step. This matches the portability goal's own exception
|
|
181
|
+
clause: a compiled module is admissible where the runtime leaves no other
|
|
182
|
+
option, and on Node it does.
|
|
183
|
+
|
|
184
|
+
## The protocol layer: mostly already written
|
|
185
|
+
|
|
186
|
+
Prior art, surveyed 2026-08:
|
|
187
|
+
|
|
188
|
+
- **[node-wayland-client](https://github.com/sdumetz/node-wayland-client)**
|
|
189
|
+
(npm `wayland-client` 3.0.0, Apache-2.0, active — last commit 2026-06).
|
|
190
|
+
Pure TS, zero runtime dependencies; parses `wayland.xml` (or precompiled
|
|
191
|
+
JSON) and generates typed proxies. The core is **~1,270 lines** —
|
|
192
|
+
`display.ts` 382, `interface.ts` 257, `args.ts` 231 — with ~2,350 lines
|
|
193
|
+
of tests. The socket is injected through the constructor, the `fd`
|
|
194
|
+
argument type is already encoded in all three codec paths, and the one
|
|
195
|
+
missing piece is a single guard: a request whose signature contains an
|
|
196
|
+
`fd` throws `"sending ancillary data not supported"`. The README asks
|
|
197
|
+
for exactly the contribution this project would make.
|
|
198
|
+
- **[westfield](https://github.com/udevbe/westfield)** /
|
|
199
|
+
[Greenfield](https://github.com/udevbe/greenfield) — a full Wayland
|
|
200
|
+
**server** in TypeScript for a browser-hosted compositor. Wrong
|
|
201
|
+
direction, but proof the wire level is comfortable in JS, and its XML
|
|
202
|
+
codegen is worth reading.
|
|
203
|
+
- Everything else (2013-era `node-wayland` bindings to libwayland) is dead.
|
|
204
|
+
|
|
205
|
+
So the layer is either **adopt `wayland-client` and contribute the fd
|
|
206
|
+
path** (a few hundred lines against node-x11's Bun transport), or write an
|
|
207
|
+
in-house equivalent in node-x11's idiom (~1.5–2.5k lines plus codegen).
|
|
208
|
+
Adopt-first is the default; the fork-shaped risk — the project is one
|
|
209
|
+
person's — is mitigated by its size: this is a library we could absorb,
|
|
210
|
+
not a dependency we would be stuck on. Either way the per-protocol cost
|
|
211
|
+
after that is ~zero, because every interface is generated from XML.
|
|
212
|
+
_As built: adopted, and forked as
|
|
213
|
+
[`@windowkit/wayland`](https://github.com/windowkit/wayland) —
|
|
214
|
+
`wayland-client` 3.0.0 plus descriptor passing both ways, a synchronous `$`
|
|
215
|
+
request path and callback payloads, offered back upstream._
|
|
216
|
+
|
|
217
|
+
The shopping list, mapped to what each protocol replaces here:
|
|
218
|
+
|
|
219
|
+
| protocol | stands in for |
|
|
220
|
+
| -------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
221
|
+
| core (`wl_compositor`, `wl_shm`, `wl_seat`, `wl_output`, `wl_data_device`) | connection, buffers, input, screens, clipboard/DnD |
|
|
222
|
+
| [xdg-shell](https://wayland.app/protocols/xdg-shell) | `WM_*`/EWMH window lifecycle, `<popup>` placement |
|
|
223
|
+
| xdg-decoration (+ kde-server-decoration) | asking for server-side decorations (often refused) |
|
|
224
|
+
| [linux-dmabuf](https://wayland.app/protocols/linux-dmabuf-v1) | the GPU tier's buffer handoff |
|
|
225
|
+
| fractional-scale-v1 + viewporter | the whole of `scale.js`'s ladder |
|
|
226
|
+
| [presentation-time](https://wayland.app/protocols/presentation-time) | Present completions; adds real photon timestamps |
|
|
227
|
+
| cursor-shape-v1 | server-side cursor names |
|
|
228
|
+
| primary-selection | the PRIMARY half of [clipboard.md](clipboard.md) |
|
|
229
|
+
| xdg-activation | `_NET_ACTIVE_WINDOW` + startup notification's focus half |
|
|
230
|
+
| [layer-shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) | `_NET_WM_STRUT_PARTIAL` — panels/docks (wlroots + KDE; not GNOME) |
|
|
231
|
+
| ext-session-lock, ext-idle-notify | lock screens; `useIdle()` |
|
|
232
|
+
| text-input-v3 | the input-method story `compose.js` approximates |
|
|
233
|
+
|
|
234
|
+
## Rendering: the tiers
|
|
235
|
+
|
|
236
|
+
First, what the field does — because "software rendering on Wayland" is
|
|
237
|
+
sometimes assumed to be a second-class citizen, and it is not:
|
|
238
|
+
|
|
239
|
+
| toolkit | rasterizer | handoff |
|
|
240
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
241
|
+
| QtWidgets | CPU raster engine | `wl_shm` |
|
|
242
|
+
| GTK4 | GSK — cairo (CPU), GL, Vulkan; [Vulkan default on Wayland since 4.16](https://blog.gtk.org/2024/01/28/new-renderers-for-gtk/) | either |
|
|
243
|
+
| foot, most wlroots tools | pixman (CPU) | `wl_shm` |
|
|
244
|
+
| Firefox | WebRender (GPU), software WebRender fallback | dmabuf/shm |
|
|
245
|
+
|
|
246
|
+
CPU-to-shm is mainstream, and the one systemic cost it had — the
|
|
247
|
+
compositor's copy to GPU-visible memory — was addressed on the compositor
|
|
248
|
+
side in 2026:
|
|
249
|
+
[KWin 6.7 imports page-aligned shm buffers as udmabufs](https://zamundaaa.github.io/wayland/2026/05/06/making-wl-shm-fast.html)
|
|
250
|
+
and samples them directly, taking KWin from 80–90% of a core to ~20% while
|
|
251
|
+
a CPU-rendered app scrolls. The client's whole obligation is page-aligned
|
|
252
|
+
pool allocations and GPU-friendly strides (~256-byte multiples; ≈1.6%
|
|
253
|
+
padding at 4K). Any pool we write complies from day one.
|
|
254
|
+
|
|
255
|
+
Also load-bearing for everything below: `wl_shm` +
|
|
256
|
+
`wl_surface.damage_buffer` preserves ntk's **dirty-rectangle model
|
|
257
|
+
verbatim** — paint the damaged rects into the mapped buffer, report
|
|
258
|
+
exactly those rects, commit. The entire performance architecture
|
|
259
|
+
(`MAX_DIRTY_RECTS`, scroll blits as `memmove`, the paint cache) transfers
|
|
260
|
+
without redesign. A GPU swapchain does _not_ preserve it — buffers rotate,
|
|
261
|
+
so damage must be tracked per buffer age or the frame fully redrawn — which
|
|
262
|
+
is half the argument for the tier ordering below.
|
|
263
|
+
|
|
264
|
+
### Tier A — our own span compositor, pure JS
|
|
265
|
+
|
|
266
|
+
Extend what ntk already has (coverage rasterizer, paths, shadows, text
|
|
267
|
+
shaping) with the missing span compositor and a client glyph atlas, and
|
|
268
|
+
register it as a sibling 2d context. The seam already exists and is the
|
|
269
|
+
whole reason this is tractable: every context registers in
|
|
270
|
+
`Drawable.renderingContextFactory` (`'2d'`, `'x11'`, `'opengl'`, `'gles'`,
|
|
271
|
+
`'cgl'` today), and `src/nodes/` asks for a context by name and never looks
|
|
272
|
+
behind it.
|
|
273
|
+
|
|
274
|
+
- **Portability: perfect.** JS + typed arrays; runs everywhere including
|
|
275
|
+
the in-process test server.
|
|
276
|
+
- **Performance: the open question, but a bounded one.** A widget UI
|
|
277
|
+
repaints small dirty regions; the analytic rasterizer measures ~25µs for
|
|
278
|
+
a 20px icon; glyphs come out of an atlas as `memcpy`+blend. The risk is
|
|
279
|
+
large-area work — full-window gradients, big images under transform —
|
|
280
|
+
where JS with no SIMD pays. Mitigations exist per-hot-path (see tiers
|
|
281
|
+
below); the floor is validated by every pixman-based tool on the desktop.
|
|
282
|
+
- **Why it is first anyway:** it can be built and shipped **on the X11
|
|
283
|
+
backend first** as `getContext('2d-sw')`, diffed against XRender output
|
|
284
|
+
with the existing pixel-hash gates and benches — a reference
|
|
285
|
+
implementation and a working test harness on day one, no Wayland in the
|
|
286
|
+
loop. It also answers standing X11 wants: rendering where RENDER is poor
|
|
287
|
+
or absent, and one day feeding DRI3/Present directly.
|
|
288
|
+
|
|
289
|
+
### Tier B — a Wasm rasterizer
|
|
290
|
+
|
|
291
|
+
[CanvasKit](https://skia.org/docs/user/modules/canvaskit/) (Skia compiled
|
|
292
|
+
to Wasm, full `CanvasRenderingContext2D`) or
|
|
293
|
+
[tiny-skia](https://github.com/linebender/tiny-skia) behind a thin binding.
|
|
294
|
+
No native toolchain — Wasm loads everywhere — so this **respects the
|
|
295
|
+
portability rule** while buying SIMD and two decades of Skia's raster
|
|
296
|
+
tuning.
|
|
297
|
+
|
|
298
|
+
The catch is not speed but scope: adopting Skia's canvas means adopting
|
|
299
|
+
Skia's text, fonts and geometry, displacing ntk's own text stack — the
|
|
300
|
+
part of this codebase most worth keeping. The honest use of this tier is
|
|
301
|
+
narrower: **a span-filling kernel behind Tier A's API** (tiny-skia fits;
|
|
302
|
+
CanvasKit does not slice that thin), swapped in per-context where
|
|
303
|
+
profiling says the JS compositor is the bottleneck. Same API, same tests,
|
|
304
|
+
a hot-path exception exactly as the goals allow.
|
|
305
|
+
|
|
306
|
+
### Tier C — a native 2d library
|
|
307
|
+
|
|
308
|
+
[skia-canvas](https://github.com/samizdatco/skia-canvas) ships prebuilt
|
|
309
|
+
Skia with GPU raster and the canvas API; published comparisons put
|
|
310
|
+
CanvasKit ~25× behind it on some workloads (numbers to re-measure on ours,
|
|
311
|
+
not to inherit). This is the "just works, fastest today" tier and a
|
|
312
|
+
legitimate prototyping shortcut — the RFC's own preference for a compile
|
|
313
|
+
step over a blocked prototype applies. It is not the destination: a
|
|
314
|
+
multi-megabyte native dependency for every app contradicts the packaging
|
|
315
|
+
story, and its GPU path duplicates Tier D with less control. If used, it
|
|
316
|
+
hides behind the same `renderingContextFactory` name so nothing above the
|
|
317
|
+
seam knows.
|
|
318
|
+
|
|
319
|
+
### Tier D — our own accelerated 2d over x11-dri
|
|
320
|
+
|
|
321
|
+
The addon misnamed by history: `x11-dri`'s `Gpu` is **GBM + EGL + GLES on
|
|
322
|
+
a DRM render node and never touches X**. Its swapchain output is already
|
|
323
|
+
the exact shape the Wayland GPU path consumes —
|
|
324
|
+
`surface.swap()` → `{ fd, stride, offset, modifier }` →
|
|
325
|
+
`zwp_linux_dmabuf_v1.create_params.add(...)` → `wl_buffer` — and dmabuf
|
|
326
|
+
import is fd-**send** only, which even plain Node can do. A
|
|
327
|
+
NanoVG-shaped renderer (stencil-then-cover fills, glyphs and gradients as
|
|
328
|
+
textures) over the existing `dri.gl` binding gives GPU 2d with the text
|
|
329
|
+
stack still ours.
|
|
330
|
+
|
|
331
|
+
Filed from this research, both small and both blocking this tier:
|
|
332
|
+
|
|
333
|
+
- [x11-dri#19](https://github.com/sidorares/node-x11-dri/issues/19) — the
|
|
334
|
+
EGL config never asks for stencil bits, while the binding exports the
|
|
335
|
+
full stencil API against it. Stencil-then-cover needs `stencilSize`.
|
|
336
|
+
- [x11-dri#20](https://github.com/sidorares/node-x11-dri/issues/20) — no
|
|
337
|
+
in-place resize (a drag-resize rebuilds the swapchain per frame), and
|
|
338
|
+
GEM handle reuse across recreation makes the documented `key`-cache
|
|
339
|
+
pattern silently unsafe. Wants `resize()` + a `generation` counter.
|
|
340
|
+
|
|
341
|
+
Costs to keep in view: per-buffer damage tracking replaces the shm model's
|
|
342
|
+
free dirty rects; a render-node requirement (fall back to Tier A where
|
|
343
|
+
there is none — same seam); and it is a compiled dependency, prebuilt for
|
|
344
|
+
common platforms, `dlopen`-degrading elsewhere — inside the goals'
|
|
345
|
+
exception, but an exception.
|
|
346
|
+
|
|
347
|
+
**Recommendation:** A first, validated on X11 before Wayland exists; D as
|
|
348
|
+
the performance tier sharing all the Wayland plumbing; B as a swappable
|
|
349
|
+
kernel inside A if profiling demands it; C at most as a prototype-phase
|
|
350
|
+
crutch. All four sit behind one context name, chosen by policy the way the
|
|
351
|
+
GL backends already are.
|
|
352
|
+
|
|
353
|
+
## Windowing semantics: what maps, what bends, what breaks
|
|
354
|
+
|
|
355
|
+
| react-x11 today | Wayland | verdict |
|
|
356
|
+
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
357
|
+
| `<window>`, WM-managed | `xdg_toplevel`; states arrive in `configure` and are acked, not set | maps — state model inverts to request/answer |
|
|
358
|
+
| `<popup>` (override-redirect, self-placed) | `xdg_popup` + `xdg_positioner`: anchor rect, gravity, constraint adjustment; compositor places and dismisses | maps _better_ — but only for anchored popups |
|
|
359
|
+
| `<popup x y>` at screen coordinates | — | **gone**; no client knows screen coordinates |
|
|
360
|
+
| `anchorRect` flip/clamp in `realize()` | the same policy, expressed as positioner flags, executed compositor-side | our one-source-of-truth design survives; the math moves |
|
|
361
|
+
| decorations (WM frames; `_MOTIF_WM_HINTS`) | client-side by default; xdg-decoration is advisory and GNOME declines server-side | **new work**: draw and hit-test our own titlebar/borders/resize edges |
|
|
362
|
+
| `windowstate.js` (`_NET_WM_STATE` set) | `set_maximized`/`set_fullscreen`/`set_minimized` + configure states; attention via xdg-activation | maps; `_NET_WM_DESKTOP`, `ABOVE`, user-time have no equivalents |
|
|
363
|
+
| struts (panels reserving edges) | layer-shell — wlroots and KDE, **not GNOME** | maps with a hole |
|
|
364
|
+
| frame clock (Present + fence + refresh estimator) | `wl_surface.frame` + presentation-time | simpler and more honest; photon time becomes measurable |
|
|
365
|
+
| `keyboardstate.js` (XKB over the wire) | keymap arrives as an fd; interpret with libxkbcommon (`bun:ffi`, ~300 lines) — do not hand-write a keymap parser | maps; `keyboard.js`'s Latin-accelerator resolution survives unchanged above it |
|
|
366
|
+
| clipboard/DnD (selections, INCR, XDND) | `wl_data_device`: offers are pipe fds, content streams | maps; the INCR machinery dies unmourned; MIME plumbing in `transfer.js` reusable |
|
|
367
|
+
| `<foreign>` / `<Frame>` (XEmbed,[embedding.md](embedding.md)) | `wl_subsurface` is same-client; xdg-foreign is parenting, not embedding | **gone by design**; `<Frame>` becomes a parented toplevel or an in-process pane |
|
|
368
|
+
| `examples/wm.jsx`, window lists, global grabs, `GetImage` screen reads | compositor-private | **gone by design**; portals cover capture with consent |
|
|
369
|
+
| eyedropper X rung | portal `PickColor` rung already first | loses the fallback rung only |
|
|
370
|
+
| `ssh -X` remoting ([remote.md](remote.md)) | no network transparency; [waypipe](https://gitlab.freedesktop.org/mstoeckl/waypipe) proxies fds over ssh | X11 backend remains the remote answer |
|
|
371
|
+
| tray | StatusNotifierItem was always D-Bus | unchanged |
|
|
372
|
+
|
|
373
|
+
The one to sit with is the popup row: `anchor.js` exists because a
|
|
374
|
+
`<popup>` must place itself with no round trip available, flipping and
|
|
375
|
+
clamping against screen geometry the client had to learn. Wayland's
|
|
376
|
+
positioner is that exact policy as protocol — a cleaner home for the
|
|
377
|
+
intent, and the end of the screen-coordinate escape hatch in the same
|
|
378
|
+
stroke. The API consequence (some props meaning nothing on one backend) is
|
|
379
|
+
an open question below, not something to paper over.
|
|
380
|
+
|
|
381
|
+
## Testing
|
|
382
|
+
|
|
383
|
+
The X11 suite's superpower is `x11/lib/xserver` — a 3,828-line pure-JS X
|
|
384
|
+
server, in-process, no display. The Wayland twin is **smaller**, because
|
|
385
|
+
the hard part of that server is the drawing protocol and Wayland has none:
|
|
386
|
+
a mock compositor implements the registry, surface/buffer lifecycle,
|
|
387
|
+
`configure` handshakes and seat event injection — and every committed
|
|
388
|
+
buffer is **plain memory the test can read**. Pixel assertions stop being
|
|
389
|
+
`GetImage` round trips and become array reads; `fireEvent` maps to
|
|
390
|
+
injecting seat events; the harness stays "real protocol, zero display",
|
|
391
|
+
which is the property worth preserving.
|
|
392
|
+
|
|
393
|
+
## The plan
|
|
394
|
+
|
|
395
|
+
Each phase has an exit that makes the next one safe to start.
|
|
396
|
+
|
|
397
|
+
- **Phase 0 — the spike (days).** Bun + `wayland-client` + node-x11's fd
|
|
398
|
+
transport: bind globals, memfd a page-aligned pool, put a solid-colour
|
|
399
|
+
`xdg_toplevel` on a real compositor, and receive the keymap fd. _Exit:
|
|
400
|
+
fd receive proven end-to-end; adopt-vs-write settled for the protocol
|
|
401
|
+
layer._
|
|
402
|
+
- **Phase 1 — software 2d, on X11 (the long pole, start immediately).**
|
|
403
|
+
The span compositor + glyph atlas as `'2d-sw'` behind the factory seam;
|
|
404
|
+
diffed against XRender via the pixel-hash gates; benched against the
|
|
405
|
+
scenarios in `scripts/bench`. _Exit: visual parity on the gates;
|
|
406
|
+
interactive scenarios within an agreed factor of XRender locally._
|
|
407
|
+
- **Phase 2 — surface + shell.** `WaylandWindow` beside ntk's `Window`
|
|
408
|
+
(frame clock, damage, backing pool); `WindowNode.realize()` grows a
|
|
409
|
+
second path; configure/ack, CSD drawing, `xdg_popup` placement. _Exit:
|
|
410
|
+
the examples that need no embedding run natively._
|
|
411
|
+
- **Phase 3 — input and integration.** Seat events into the synthetic
|
|
412
|
+
event system; xkbcommon keymaps; clipboard/DnD over pipes; scale,
|
|
413
|
+
outputs, activation; the portal-first modules verified with their X
|
|
414
|
+
rungs dark. _Exit: the component test suite green on the mock
|
|
415
|
+
compositor._
|
|
416
|
+
- **Phase 4 — the accelerated tier.** The GLES renderer over x11-dri
|
|
417
|
+
(after [#19](https://github.com/sidorares/node-x11-dri/issues/19)/
|
|
418
|
+
[#20](https://github.com/sidorares/node-x11-dri/issues/20)), dmabuf
|
|
419
|
+
handoff, buffer-age damage. _Exit: chosen scenarios beat Tier A on a
|
|
420
|
+
render-node machine; automatic fallback proven._
|
|
421
|
+
- **Phase 5 — the payoff.** presentation-time-driven latency measurement;
|
|
422
|
+
input-to-photon on the dashboard; scheduling tuned against real photon
|
|
423
|
+
timestamps. _Exit: the number the project optimizes for, finally
|
|
424
|
+
measured rather than modeled._
|
|
425
|
+
|
|
426
|
+
## Needs validation
|
|
427
|
+
|
|
428
|
+
What the backend does today was built and checked on one machine — a
|
|
429
|
+
virtual GNOME/mutter desktop on virgl, one virtual monitor, a mouse and a
|
|
430
|
+
keyboard — and some of it was built to the specification because this
|
|
431
|
+
machine cannot exercise it. Each item names the check that would close it;
|
|
432
|
+
[wayland-backend.md](wayland-backend.md) has the design behind each.
|
|
433
|
+
|
|
434
|
+
- **Monitors.** Two outputs, one at 200% and one at 100%: `useScreens()`
|
|
435
|
+
lists both in left-to-right order with per-screen `scale`; the first
|
|
436
|
+
window lays out at the densest output's scale (`app.scale` is seeded
|
|
437
|
+
before any window); dragging a window between heads changes `win.output`
|
|
438
|
+
and its `available` rect once. Plug and unplug an output while running:
|
|
439
|
+
one re-render per event. GNOME at 150%: `screens[0].scale === 1.5` with
|
|
440
|
+
no 2→1.5 correction visible on the first frame. A compositor without
|
|
441
|
+
fractional-scale-v1 and `wl_surface` v6 (older wlroots, Weston): the
|
|
442
|
+
buffer scale follows the output's integer scale on `enter`.
|
|
443
|
+
- **Touch.** A touchscreen, `examples/widgets.jsx`: a tap clicks, a finger
|
|
444
|
+
drags a slider live, a second finger clicks nothing; a finger on the
|
|
445
|
+
titlebar moves the window and on a corner resizes it (the touch `down`
|
|
446
|
+
serial is what `xdg_toplevel.move` gets); `onMouseDown`'s
|
|
447
|
+
`nativeEvent.pointerType === 'touch'`.
|
|
448
|
+
- **Tablet.** A pen: hovering shows the arrow (I-beam over text), the tip
|
|
449
|
+
clicks and drags, the side button is button 3, `nativeEvent.pressure`
|
|
450
|
+
varies, and pulling the pen away mid-press releases the control. Pads
|
|
451
|
+
(rings, strips) are accepted and ignored by design.
|
|
452
|
+
- **Input methods.** An IBus engine installed (`ibus-anthy`, `ibus-libpinyin`;
|
|
453
|
+
`gsettings set org.gnome.desktop.input-sources sources
|
|
454
|
+
"[('xkb','us'),('ibus','anthy')]"`, restored afterwards), `examples/form/index.jsx`
|
|
455
|
+
under `REACT_X11_WAYLAND_TRACE=1`: typing `nihongo` shows an underlined
|
|
456
|
+
preedit, Space a segment underline, Enter the committed text and one
|
|
457
|
+
`onChange`; `Ctrl+Shift+U e9 Space` exercises the same path without an
|
|
458
|
+
engine. Only the protocol half — `enter`, `enable`, `done`, `leave` —
|
|
459
|
+
could be seen here; the compositor never composed, because nothing here
|
|
460
|
+
types.
|
|
461
|
+
- **Drag and drop.** A real pointer, two react-x11 processes
|
|
462
|
+
(`examples/dnd-source.jsx`, `examples/dnd-target.jsx`) and a GNOME app:
|
|
463
|
+
a file from Files onto the target, text from the source into Text Editor,
|
|
464
|
+
and a drag between the two windows of one process (`e.source ===
|
|
465
|
+
'internal'`, items by reference) and of two.
|
|
466
|
+
- **Other compositors.** Server-side decorations, layer-shell and
|
|
467
|
+
wlr-screencopy were validated on this machine under **sway 1.10.1** and
|
|
468
|
+
**labwc 0.8.3** (nested and headless, screenshots by grim; `capture()`
|
|
469
|
+
agreed with grim on every pixel, and the eyedropper picked the exact box
|
|
470
|
+
colour). Still to see: `ext-image-copy-capture` (written to the spec; no
|
|
471
|
+
compositor here has it — sway 1.11 / wlroots 0.19 will), KDE Plasma
|
|
472
|
+
(xdg-decoration and its own capture portal), and a dock on a compositor
|
|
473
|
+
with two outputs (`layerShell.output`).
|
|
474
|
+
- **The GPU path on real hardware.** `test/wayland/context2d-gpu.test.js`
|
|
475
|
+
reads antialiased fills and stencil clips back from a render node; it
|
|
476
|
+
has run on virgl only. Intel, AMD and NVIDIA (Mesa and proprietary) each
|
|
477
|
+
want one run — the stencil arithmetic is the kind of thing a driver
|
|
478
|
+
disagrees about. And the frame-time number: `Surface.swap()` costs
|
|
479
|
+
24.5 ms here, of which ~10 ms is virgl's host round trip on `glFinish`
|
|
480
|
+
and none is the protocol layer; only bare metal says what Tier D costs.
|
|
481
|
+
- **Multisampling** is designed and not built (GLES 3.0 cannot blit into a
|
|
482
|
+
multisampled framebuffer, so the target's partial repaints would become
|
|
483
|
+
textured draws); `MAX_SAMPLES` is 1 here. It needs a machine that reports
|
|
484
|
+
4 or more.
|
|
485
|
+
- ~~**Bun after an `npm install`.**~~ Closed: node-x11 ships the EINTR fix
|
|
486
|
+
for `fdpass-bun.js` since x11 4.2.1
|
|
487
|
+
([sidorares/node-x11#297](https://github.com/sidorares/node-x11/pull/297)),
|
|
488
|
+
and the lockfile resolves 4.2.3, so a fresh install no longer closes every
|
|
489
|
+
Bun example's connection ~0.3 s in.
|
|
490
|
+
|
|
491
|
+
## Open questions
|
|
492
|
+
|
|
493
|
+
1. **API surface.** Is the Wayland backend API-compatible-minus-features
|
|
494
|
+
(same JSX, some props inert, `<foreign>` absent) or a declared subset?
|
|
495
|
+
Current lean: same JSX, loud dev-mode warnings on inert props, feature
|
|
496
|
+
detection à la `app.capabilities`. _As built: same JSX; the X-only
|
|
497
|
+
methods are absent rather than stubbed, so the tree's own probes
|
|
498
|
+
(`wnd.setProperty?.()`, `useSupports('embedding')`) answer for it._
|
|
499
|
+
2. **Where the code lives.** ntk grows `WaylandWindow` beside `Window`
|
|
500
|
+
behind the same `App`/`Drawable` contracts vs. a sibling package.
|
|
501
|
+
Current lean: inside ntk — the factory seam and the portable raster
|
|
502
|
+
half are already there. _As built: `src/wayland/` in this repository,
|
|
503
|
+
behind the same app/window contract the cocoa backend keeps; ntk is
|
|
504
|
+
still the long-term home._
|
|
505
|
+
3. **Runtime floor.** Bun-only until when? Current lean: prototype through
|
|
506
|
+
Phase 2 on Bun alone; decide on the Node addon when someone needs it,
|
|
507
|
+
not before. _As built: Node works through x11-dri's `UnixSocket`
|
|
508
|
+
(uv_poll, fd passing); Bun through node-x11's `bun:ffi` transport. The
|
|
509
|
+
protocol layer stayed pure JS, having been measured not to be the
|
|
510
|
+
cost._
|
|
511
|
+
4. **x11-dri's name.** Its GBM/EGL core is display-server-neutral and this
|
|
512
|
+
plan leans on it twice (GPU tier, and possibly the Node fd/xkb shims).
|
|
513
|
+
Splitting a `dri-core` out is upstream's call; noted here so the
|
|
514
|
+
dependency direction is explicit.
|
|
515
|
+
5. **libxkbcommon.** `dlopen` it (compiled, but universally present on
|
|
516
|
+
Wayland systems) vs. a pure-JS keymap interpreter. Current lean:
|
|
517
|
+
dlopen — a keymap parser is the kind of code this project writes well,
|
|
518
|
+
but not before a working backend exists. _As built: the pure-JS parser
|
|
519
|
+
(`xkb.js`), against the compositor's own keymap text; no libxkbcommon._
|
|
520
|
+
|
|
521
|
+
## References
|
|
522
|
+
|
|
523
|
+
- [The Wayland Book](https://wayland-book.com/) — the tour worth reading
|
|
524
|
+
end to end.
|
|
525
|
+
- [Protocol and model of operation](https://wayland.freedesktop.org/docs/book/Protocol.html) —
|
|
526
|
+
the wire format, normatively.
|
|
527
|
+
- [wayland.app](https://wayland.app/protocols/) — every protocol, browsable;
|
|
528
|
+
start with [xdg-shell](https://wayland.app/protocols/xdg-shell).
|
|
529
|
+
- [wayland.xml](https://gitlab.freedesktop.org/wayland/wayland/-/blob/main/protocol/wayland.xml)
|
|
530
|
+
and [wayland-protocols](https://gitlab.freedesktop.org/wayland/wayland-protocols) —
|
|
531
|
+
the codegen inputs.
|
|
532
|
+
- [Making wl_shm fast](https://zamundaaa.github.io/wayland/2026/05/06/making-wl-shm-fast.html) —
|
|
533
|
+
the page-alignment contract this plan adopts.
|
|
534
|
+
- [node-wayland-client](https://github.com/sdumetz/node-wayland-client),
|
|
535
|
+
[westfield](https://github.com/udevbe/westfield) — prior art.
|
|
536
|
+
- [node-x11#290](https://github.com/sidorares/node-x11/pull/290),
|
|
537
|
+
[nodejs/node#53391](https://github.com/nodejs/node/issues/53391) — the
|
|
538
|
+
fd story.
|
|
539
|
+
- [x11-dri#19](https://github.com/sidorares/node-x11-dri/issues/19),
|
|
540
|
+
[x11-dri#20](https://github.com/sidorares/node-x11-dri/issues/20) — filed
|
|
541
|
+
from this research.
|
|
542
|
+
- [NanoVG](https://github.com/memononen/nanovg) — the GPU-2d shape Tier D
|
|
543
|
+
follows; [CanvasKit](https://skia.org/docs/user/modules/canvaskit/),
|
|
544
|
+
[tiny-skia](https://github.com/linebender/tiny-skia),
|
|
545
|
+
[skia-canvas](https://github.com/samizdatco/skia-canvas) — Tiers B/C.
|