react-x11 2.52.0 → 2.54.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.
Files changed (115) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1096 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
package/docs/wayland.md DELETED
@@ -1,545 +0,0 @@
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.