react-x11 2.52.0 → 2.53.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 (112) 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 +1091 -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/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -1,357 +0,0 @@
1
- # Protocol efficiency: the checkbox-hover audit
2
-
3
- _Research document, 2026-08-22. Measured on the `claude/x11-protocol-efficiency`
4
- checkout with ntk 8.2.0 / x11 3.9.0 installed, on one machine (macOS 15.2,
5
- node v26): live XQuartz (X.Org 21.x) through the
6
- [x11-protocol-visualizer](https://github.com/sidorares/x11-protocol-visualizer)
7
- MITM proxy, and node-x11's in-process pure-JS X server via `scripts/bench/`.
8
- File:line references will drift with the code; request counts will not, because
9
- the bench now prices them._
10
-
11
- ---
12
-
13
- ## 0. TL;DR
14
-
15
- A protocol trace of _hovering a `<Checkbox>`_ in `examples/form/index.jsx` — a frame
16
- whose visible result is one 18×18 rounded rectangle changing colour — showed
17
- ~26 requests per frame, among them a `CreatePixmap` of a **420×380 depth-8
18
- pixmap created and freed every frame**. The audit attributed every request in
19
- that frame and in the mount that precedes it, across all three layers
20
- (react-x11, ntk, node-x11).
21
-
22
- **Findings, compressed:**
23
-
24
- 1. **[ntk] `_fillRect` misses the rectangular-clip fast path** — the dominant
25
- cost, and the whole explanation of the per-frame pixmap. Every `fillRect`
26
- under _any_ clip materializes a **window-sized a8 clip mask** and
27
- composites through it, when the glyph, trapezoid and image routes all
28
- already special-case a rectangular clip stack. A one-function fix
29
- (intersect the fill rect with the clip rect, composite directly) removes
30
- 7 requests and ~2 full surfaces of server pixel work from every partial
31
- repaint. Verified byte-identical.
32
- 2. **[ntk] Picture-clip churn** — the rounded-rect glyph route brackets every
33
- shape in `SetPictureClipRectangles` + reset, so a box's fill and border
34
- pay 4 clip requests where 1 would do, and consecutive shapes re-set the
35
- clip they just reset. A sticky, lazily-reset picture clip removes ~75% of
36
- them. Verified byte-identical.
37
- 3. **[react-x11] Event mask declared one listener at a time** — nine
38
- `ChangeWindowAttributes` per window for a constant value. Fixed on this
39
- branch: the union goes in `CreateWindow`.
40
- 4. **[react-x11 / ntk / node-x11] Startup round trips serialized** — ~10
41
- dependent round trips before the first `CreateWindow`. Partially fixed on
42
- this branch (`createRoot`'s two probes now run concurrently); the ntk and
43
- node-x11 halves are proposed below.
44
- 5. The rest of the hover frame is honest work (corner glyphs, border,
45
- blit) — and the event side (XI2 Motion at ~136 B/event) is the price of
46
- smooth scrolling, with one narrow refinement available.
47
-
48
- Prototyped fixes 1+2 against the whole bench: **requests −35…−80% and
49
- composite pixel area −45…−95% on every partial-repaint scenario**, pixels
50
- byte-identical (SHA-256 of full-window `GetImage` across five interaction
51
- states), full test suite green.
52
-
53
- ---
54
-
55
- ## 1. The trace, explained line by line
56
-
57
- The captured hover frame (reproduced byte-for-byte on the in-process server
58
- and on live XQuartz through the proxy — same requests, same sizes):
59
-
60
- | request | bytes | why it is there |
61
- | --------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
- | `ChangeWindowAttributes {cursor}` | 16 | the hover path crossed into/out of a node with a `cursor`; react-x11 already dedupes (`EventManager._updateCursor`), so one request per boundary crossing is correct |
63
- | `GetInputFocus` | 4 | **node-x11's void-sync**, not the frame fence (ntk#309 settled this from the reply positions: it follows the cursor write and precedes the paint, where a fence would trail `Present:Pixmap`). ntk passes a discarded `() => {}` to `ChangeWindowAttributes`, and node-x11 guarantees a callback on a void request eventually fires by injecting a `GetInputFocus` round trip. The fence proper (`Window#_armFence`) only runs where Present completions are not clocking frames |
64
- | `CreatePixmap 420×380 depth=8` | 16 | **the bug.** `_fillRect` under the damage-rect clip takes the mask path: this is a full-window a8 clip mask |
65
- | `RENDER:CreatePicture` | 20 | picture over that mask |
66
- | `RENDER:FillRectangles Src (0,0,0,0)` | 28 | clear the _entire_ 420×380 mask (159,600 px of server work) |
67
- | `RENDER:FillRectangles Src (0,0,0,1) [18×18 rect]` | 28 | write the clip rect into the mask |
68
- | `RENDER:Composite Over src=solid mask=a8 → backing` | 36 | the window background, painted through the mask |
69
- | `RENDER:SetPictureClipRectangles [18×18]` | 20 | clip for the checkbox body's corner glyphs |
70
- | `RENDER:CompositeGlyphs8` | 76 | the body's rounded corners, as cached server-side glyphs (`shapeglyphs.js` — this part is the fast path working as designed) |
71
- | `RENDER:FillRectangles Over ×3 rects` | 44 | the body's straight strips |
72
- | `RENDER:SetPictureClipRectangles [reset]` | 20 | eager reset after the body |
73
- | `RENDER:SetPictureClipRectangles [the same 18×18]` | 20 | …immediately re-set for the border |
74
- | `RENDER:CompositeGlyphs8` | 76 | border corner glyphs |
75
- | `RENDER:FillRectangles Over ×4 rects` | 52 | border strips |
76
- | `RENDER:SetPictureClipRectangles [reset]` | 20 | eager reset again |
77
- | `RENDER:FreePicture` + `FreePixmap` | 16 | the mask ceremony's teardown |
78
- | `XFIXES:SetRegion` | 16 | the frame's damage as the present region (region id reused — correct) |
79
- | `Present:Pixmap` | 72 | the blit, scheduled against vblank |
80
-
81
- So of ~26 requests, **8 are the clip-mask ceremony** (finding 1), **3 of 4
82
- clip set/resets are redundant** (finding 2), and the remaining ~12 are the
83
- actual ink plus the blit. The pixel-work story is worse than the request
84
- count: the ceremony touches ~2 × 160k px per frame for an 18×18 change.
85
-
86
- The damage scoping above all of this is _correct_: react-x11 repaints exactly
87
- the 18×18 checkbox box (`_takeDamage` → one clipped `_paintRegion` pass), and
88
- a 120-motion hover sweep coalesces into 7 presents. The waste is entirely in
89
- how the bounded repaint is translated to RENDER.
90
-
91
- ## 2. How to measure (the e2e methodology)
92
-
93
- Two complementary rigs, both now in-repo or one command away:
94
-
95
- **Deterministic (CI-grade): `npm run bench`.** Runs scenarios against
96
- node-x11's in-process X server and counts requests / bytesOut / replies /
97
- `Composite` count / **composite pixel area** / **convolved pixels** per
98
- scenario (`scripts/bench/xcount.js`). Requests and bytes miss pixel work
99
- entirely — a Composite is 36 bytes whether it touches ten pixels or the whole
100
- surface — so the Mpx column is the one that catches "correct but does far too
101
- much". The convMpx column catches the layer under _that_: area prices every
102
- destination pixel the same, but a source or mask picture carrying RENDER's
103
- `convolution` filter is re-convolved by the server on every composite that
104
- reads it, so `kernel_w × kernel_h × area` is what the server actually runs.
105
- Issue #413 is the case that motivated it — a blurred `boxShadow` left the
106
- blur as a picture filter rather than baking it into the pixels, which made
107
- every shadowed repaint ~700x slower with requests, bytes, composites, area,
108
- stalls, dups, churn and the rendered pixels all unchanged.
109
- Since #380 the same counting proxy also analyses the stream: `stalls`
110
- (blocking round trips nothing was pipelined behind), `dupQueries` (repeated
111
- identical reply-carrying requests) and `churn` (server resources created and
112
- freed inside the measured window, keyed by creation parameters — the
113
- "window-sized mask pixmap rebuilt every frame" class reads as one line with
114
- a count), with `--hotspots` for the per-scenario detail, `--record-dir` for
115
- x11vis-format captures of each scenario, and a CI job gating `--check`.
116
- This audit added the two scenarios the trace exposed as unpriced:
117
-
118
- - `fills: 20 fillRects each under a damage clip` — the window background of
119
- every partial repaint;
120
- - `hover: rounded box state flip, on and off` — the whole per-frame tail of a
121
- pointer interaction (damage scoping → background under clip → corner
122
- glyphs → border → blit).
123
-
124
- **Live (ground truth): x11vis.** The MITM proxy records byte-exact captures
125
- against a real server, with decode, per-request RTT, stats and resource-
126
- lifecycle lints:
127
-
128
- ```bash
129
- # terminal 1 — proxy the real display, record
130
- x11vis --no-ui --quiet --record hover.x11cap
131
-
132
- # terminal 2 — the app under test, through the proxy
133
- DISPLAY=127.0.0.1:1 npm run examples:form
134
- ```
135
-
136
- Driving the interaction reproducibly on XQuartz (no XTEST there): the app's
137
- own connection can sweep the real pointer with core-protocol `WarpPointer`
138
- across the widget row — real `EnterNotify`/`MotionNotify`/XI2 events follow,
139
- so the full input path is exercised. The capture is then sliced between
140
- `Present:Pixmap` requests to isolate steady-state frames, and
141
- `x11vis --diff a.x11cap b.x11cap` compares before/after sessions.
142
-
143
- **The correctness bar.** "No visual degradation" is verified as _pixel_
144
- byte-identity, not request-stream identity (an optimized stream is different
145
- by definition): render the same tree through the same interaction script
146
- under both implementations and compare SHA-256 digests of full-window
147
- `GetImage` readbacks at each settled state. The in-process server makes this
148
- deterministic (no font/AA variance across runs). Five states were compared
149
- for this audit — mounted, hover on, hover wiggle, hover off, clicked — all
150
- identical.
151
-
152
- **The latency metric.** Requests are a proxy; the goal posts are (a) **round
153
- trips on the critical path** (the `replies` column; each is a full RTT on a
154
- remote display) and (b) **server pixel work per frame** (Mpx), which is what
155
- turns into input→photon latency once the server is the bottleneck.
156
-
157
- ## 3. Findings in detail
158
-
159
- ### 3.1 [ntk] `_fillRect` + any clip = a window-sized mask (the headline)
160
-
161
- `renderingcontext_2d.js` has a rectangular-clip fast path — "a stack of
162
- rectangles stays virtual" — and `_drawGlyphsDevice`, `drawTraps` and
163
- `_beginDirectComposite` (images) all use it. `_fillRect` is the one direct
164
- route that never checks: it calls `_compositeMask()` unconditionally, whose
165
- own comment claims it is "reached only when the clip is not a rectangle".
166
- Under react-x11 every partial repaint clips to the damage rect first, so
167
- _every window-background fill of every bounded frame_ takes the mask path:
168
- `CreatePixmap` (window-sized, depth 8) + `CreatePicture` + full-surface
169
- clear + rect write + `Composite` through the mask + `FreePicture` +
170
- `FreePixmap` — recreated per frame because `restore()` drops the mask.
171
-
172
- **Fix.** In the identity-transform branch of `_fillRect`: if the clip stack
173
- is all rectangles, intersect the fill rect with `_clipRect()` and composite
174
- directly (`mask=0`, or a 1×1 repeating solid for `globalAlpha < 1`, exactly
175
- as `_beginDirectComposite` already does). Fall back to `_compositeMask()`
176
- only for a genuinely non-rectangular stack. The same treatment applies to
177
- the remaining `_compositeMask()` call sites (the two `drawImage` legs).
178
-
179
- **Why byte-identical:** compositing `src IN mask(=1 inside R, 0 outside)`
180
- bounded to rect D writes exactly the pixels of D∩R; compositing unmasked
181
- bounded to D∩R writes the same pixels with the same operator arithmetic.
182
- Verified empirically (§5).
183
-
184
- **Bonus:** this also stops per-frame XID allocate/free churn — the capture's
185
- lint pass flags the freed mask pixmap's id being recycled into a Present
186
- event id, which is legal but makes captures harder to read.
187
-
188
- ### 3.2 [ntk] Sticky picture clip
189
-
190
- `_emitShapeGlyphs` (and the text fast path) bracket each run in
191
- `SetPictureClipRectangles(clip)` … `SetPictureClipRectangles(0,0,32767,32767)`.
192
- A rounded box paints as _fill bracket + border bracket_: four clip requests,
193
- two of them setting the value the previous one just reset.
194
-
195
- **Fix.** Make the destination picture's rectangular clip a piece of tracked
196
- state: a set that matches the applied clip is skipped, and the reset is
197
- recorded lazily and only flushed by the next operation that draws to the
198
- picture under a different (or no) clip. Every RENDER op that names the
199
- context's picture as destination goes through one accessor, which is what
200
- makes the laziness safe — an unobserved reset-then-set-same pair is, by
201
- construction, unobservable to the server. In the hover frame this turns 4
202
- clip requests into 1.
203
-
204
- ### 3.3 [react-x11, landed] The event mask, one request instead of nine
205
-
206
- ntk grows a window's server-side event mask from a `newListener` hook — one
207
- `ChangeWindowAttributes` per first listener of each kind. react-x11's
208
- subscriptions are a constant (pointer ×4, keys ×2, focus ×2, plus ntk's own
209
- Exposure for the backing store), so mount paid nine requests for a value
210
- known before the window existed. `WindowNode.realize()` now passes the union
211
- as a `CreateWindow` attribute; ntk ORs it in and every lazy grow becomes a
212
- no-op. _(A complementary ntk fix — coalescing mask grows per tick — would
213
- serve non-react ntk apps too.)_
214
-
215
- ### 3.4 [all three, partially landed] Startup round-trip pipelining
216
-
217
- Before the first `CreateWindow`, instrumented reply-by-reply:
218
-
219
- | layer | chain | round trips |
220
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
221
- | node-x11 | connection handshake | 1 |
222
- | ntk `createClient` | `require('glx')` → `require('render')` (QueryExtension → QueryPictFormats, nested) → `Render.QueryVersion` | 4–5, **serial** |
223
- | react-x11 `createRoot` | `beginCompositing` (InternAtom → GetSelectionOwner → XFixes QueryExtension) then `beginScreens` (QueryExtension → InternAtom → GetProperty) | 6, **was serial** |
224
-
225
- On a remote display at 20 ms RTT that is ~200 ms of pure serialization before
226
- the first pixel. Landed here: `createRoot` awaits its two probes with
227
- `Promise.all` (the trace now shows them interleaved — the floor is the
228
- slower chain, not the sum). Proposed for ntk: run the `glx` and `render`
229
- requires concurrently and pipeline `QueryVersion` behind `QueryExtension`'s
230
- reply (both only need the major opcode). Proposed for node-x11: the render
231
- extension's own `QueryVersion`/`QueryPictFormats` can go out together.
232
-
233
- Also verified, no action needed: node-x11's write path already batches
234
- Xlib-style (16–64 KB buffer, flush on reply-bearing requests, `setImmediate`
235
- backstop), atoms are cached with the 68 standard atoms pre-seeded, and the
236
- 17 `InternAtom`s react-x11's dnd module issues at mount are pipelined (one
237
- RTT total). The one node-x11 nicety worth having: dedupe _concurrent_
238
- in-flight `InternAtom`s for the same name (the cache only catches repeats
239
- after the reply).
240
-
241
- ### 3.5 [ntk] Smaller findings
242
-
243
- - **`_compositeMask` alpha fill is window-sized** — when `globalAlpha < 1`
244
- it fills the whole scratch mask with the alpha; a 1×1 repeating solid (as
245
- `_beginDirectComposite` uses) or a fill bounded to the composite rect is
246
- the same result without the full-surface write.
247
- - **The backing store's birth clear** — every backing pixmap is
248
- `PolyFillRectangle`d white at creation, and react-x11's first frame then
249
- paints every pixel of it. Skipping the clear when a full-window paint is
250
- already queued would save one full-surface fill per window/resize.
251
- - **`AddGlyphs` per batch** — a first paint uploads new glyphs as
252
- encountered (20 `AddGlyphs` at form mount). Same bytes, fewer requests if
253
- accumulated per (frame × glyphset).
254
-
255
- ### 3.6 The event side (bytes _in_)
256
-
257
- In the live hover capture, `XInputExtension:Motion` is the single largest
258
- item: 120 events, 16,320 bytes — 96% of event traffic, ~136 B per motion
259
- against 32 B for core `MotionNotify`. This is the cost of XI2 Motion
260
- selection, which is what smooth (sub-notch, valuator-based) scrolling rides
261
- on, so it is bought deliberately (issue #273). One refinement is available:
262
- ntk already queries the seat's scroll axes for its wheel bookkeeping, so on
263
- a seat with _no_ scroll valuators the XI2 Motion selection buys nothing and
264
- could be skipped, keeping 32-byte core motion. Not worth conditioning on
265
- anything finer — device hot-plug would re-open the question mid-session.
266
-
267
- Also checked and fine: motion coalescing (120 injected motions → 7 painted
268
- frames), `PointerMotion` selection itself (hover styling needs it), and the
269
- cursor updates (deduped; one request per hover-boundary crossing).
270
-
271
- ### 3.7 Why the traced session showed `GetInputFocus` _and_ `Present:Pixmap`
272
-
273
- _Corrected 2026-08-23 — the first version of this section blamed the frame
274
- fence; ntk#309's position analysis showed otherwise._
275
-
276
- The per-frame `GetInputFocus` in the original capture is **node-x11's
277
- void-sync**, not ntk's fence. ntk's `setCursor` (and its lazy event-mask
278
- grows) pass a no-op `() => {}` callback to `ChangeWindowAttributes`, and
279
- node-x11 honours its guarantee that a callback on a void request eventually
280
- fires by injecting a `GetInputFocus` round trip when no reply-bearing
281
- request follows in the same tick. The tell is the position: each
282
- `GetInputFocus` follows a cursor write and precedes the frame's drawing,
283
- where a fence would trail `Present:Pixmap` in the same synchronous run —
284
- and the client never waited on the reply. The Present completion clock was
285
- active in that capture all along (a live probe on this machine's XQuartz
286
- confirms `frameClock: 'present'`); its 20.5 ms RTT is still informative —
287
- that is the server draining the frame's mask ceremony, which finding 1
288
- removes. Fixes: drop the discarded callbacks (ntk#309 / PR ntk#311, which
289
- also memoizes the cursor id ntk-side). The fence itself needs no change: it
290
- is the flow control everywhere Present completions cannot clock frames, and
291
- one 4-byte request per frame is the correct price there.
292
-
293
- ## 4. Numbers
294
-
295
- Bench, ntk 8.2.0 as published ("before") vs the same with fixes 3.1+3.2
296
- prototyped ("after"). Composites and text/glyph bytes are unchanged
297
- throughout — the ink is the same ink.
298
-
299
- | scenario | reqs before → after | Mpx before → after |
300
- | -------------------------------------------- | ------------------- | ------------------ |
301
- | fills: 20 fillRects each under a damage clip | **150 → 30** | **3.36 → 0.18** |
302
- | hover: rounded box state flip, on and off | **37 → 20** | 0.00 → 0.00 |
303
- | clips: 40 nested rect clips with text | 148 → 108 | 0.16 → 0.16 |
304
- | scroll: 10 notches over 500 rows | 329 → 207 | 0.64 → 0.39 |
305
- | svg: 10 unchanged icons in a damaged row | 41 → 16 | 0.01 → 0.01 |
306
- | update: 5 color flips, no layout | 49 → 19 | 0.03 → 0.03 |
307
- | update: 5 absolute box moves (layout) | 48 → 18 | 0.03 → 0.03 |
308
- | scene: 5 drag steps over 300 cells | 240 → 146 | 0.08 → 0.01 |
309
- | scene: 5 pan steps over 374 cells | 983 → 623 | 0.30 → 0.06 |
310
- | scene: 5 pan steps with a HUD strip | 3060 → 1992 | 1.10 → 0.43 |
311
-
312
- Full-repaint scenarios (mount, icon wall, gradient cards) are unchanged —
313
- they never clip, so they never paid the tax. The `Mpx` collapses are the
314
- through-mask composites and full-surface mask clears disappearing; on a
315
- slow or software-rendering server that column is frame time.
316
-
317
- react-x11 changes landed with this document (measured on the mount trace):
318
- 9 `ChangeWindowAttributes` → 0, and the pre-window probe serialization
319
- 6 round trips → the slower of two ~3-trip chains.
320
-
321
- Verification: the full test suite passes with the prototyped ntk (the six
322
- known macOS-only appearance failures and nothing else), and SHA-256 digests
323
- of full-window `GetImage` readbacks across five interaction states are
324
- identical between published and prototyped ntk.
325
-
326
- ## 5. What lands where
327
-
328
- - **This branch (react-x11):** event mask at `CreateWindow`; concurrent
329
- `createRoot` probes; the two bench scenarios + baseline refresh; this
330
- document.
331
- - **ntk (proposed, prototype validated):** `_fillRect` rectangular-clip fast
332
- path (§3.1, also the `drawImage` `_compositeMask` legs); sticky picture
333
- clip (§3.2); bounded/solid alpha mask (§3.5); optional: coalesced event-
334
- mask grows, deferred backing clear, per-frame `AddGlyphs` batching,
335
- scroll-axis-gated XI2 Motion. Once released, react-x11 re-saves the bench
336
- baseline to lock the improvement in (the skipped-on-CI trap: CI installs
337
- published ntk, so the baseline must always be saved against it).
338
- - **node-x11 (proposed):** pipeline extension init round trips; dedupe
339
- concurrent `InternAtom`s.
340
-
341
- ---
342
-
343
- ## Addendum, 2026-08-23: where the proposals went
344
-
345
- Filed and in flight the day after the audit:
346
-
347
- - react-x11#380 (landed) — the bench's counting proxy grew the `stalls` /
348
- `dupQueries` / `churn` analysis, a pointer-driven Checkbox hover scenario,
349
- `--hotspots`, x11vis-format `--record-dir`, and a CI `--check` gate.
350
- - ntk#307 → PR ntk#312 — the `fillRect` rectangular-clip bypass (§3.1),
351
- including `globalAlpha` as a 1×1 solid on every direct composite.
352
- - ntk#308 — the sticky, lazily-reset picture clip (§3.2).
353
- - ntk#309 → PR ntk#311 — drop the no-op callbacks whose void-sync this
354
- section originally misread as the fence (§3.7), and memoize the cursor.
355
- - ntk#313 — the residual `_compositeMask` fallback: full-surface alpha
356
- stamp and clip intersection bounded to the composite box (§3.5's first
357
- bullet, narrowed to what PR ntk#312 leaves behind).