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/debugging.md CHANGED
@@ -6,20 +6,17 @@ raises: _what went on the wire_ (`REACT_X11_TRACE`), _what got repainted_
6
6
  both). They complement [devtools.md](devtools.md) (component tree) and
7
7
  `REACT_X11_DEBUG_LAYOUT=1` (outline every drawn node, colour by depth).
8
8
 
9
- X11 is a network protocol even on a local socket, and the renderer's
10
- performance model — damage rects, one `CopyArea` per presented rect — is
11
- invisible at runtime without these. The failure mode they exist for is the
12
- silent full-window repaint: correct pixels, quietly costing the whole
13
- window every frame.
14
-
15
- Everything here is built to cost nothing when off: the environment
16
- switches are read **once at startup** (a `process.env` read is a real
17
- environment lookup, and these sit on the frame path), the tracer module is
18
- not even imported unless `REACT_X11_TRACE` is set or you import
19
- `react-x11/debug` yourself, stacks are only captured under
20
- `REACT_X11_DEBUG_PAINT=full` or `+stacks`, and the per-frame reason
21
- bookkeeping is a handful of set insertions. Measuring the renderer must
22
- not change what it measures.
9
+ The failure mode they exist for is the silent full-window repaint: correct
10
+ pixels, quietly costing the whole window every frame. X11 is a network
11
+ protocol even on a local socket, and damage rects are invisible at runtime
12
+ without these.
13
+
14
+ Everything here costs nothing when off. The environment switches are read
15
+ **once at startup** unless a section says otherwise, and the `NO_…` and
16
+ `STRICT_…` switches answer only to `1` — any other value leaves the default
17
+ in place. The tracer module is not imported unless `REACT_X11_TRACE` is set
18
+ or you import `react-x11/debug` yourself, and stacks are captured only under
19
+ `REACT_X11_DEBUG_PAINT=full` or `+stacks`.
23
20
 
24
21
  ## Protocol tracing — `REACT_X11_TRACE`
25
22
 
@@ -37,21 +34,16 @@ REACT_X11_TRACE=summary npm run examples:dashboard
37
34
  - `requests` — one stderr line per request as it is sent
38
35
  (`x11 → Render.FillRectangles 36B`), plus one line per painted frame
39
36
  (`frame 412: 320x24@8,40 reasons=style-state landed=16.4ms`). X errors are
40
- decoded and name the failing request. `landed` is ntk's `frameLatency`:
41
- how long the previous frame took to be answered, which means different
42
- things on the two frame clocks. On ntk >= 7 a window presents by default
43
- and its frames end on the display, so this is time-to-display and reads
44
- about **one refresh period** — 16ms on a 60Hz screen is the system
45
- working. Client work and server work still separate here, but the question
46
- to ask of a large number is which clock produced it: on `frameClock: 'fence'`
47
- it is a server round trip, and a paint that builds in 0.3ms
48
- against 20ms there _is_ a server-side problem (software-fallback RENDER
49
- ops, a virtualized GPU) rather than a renderer one.
50
- `npm run bench:frames` reports the same split as a summary. A frame the
51
- frame pacer held ([elements.md](elements.md#framerate--pacing-the-frames-under-a-flood))
37
+ decoded and name the failing request. `landed` is how long the previous
38
+ frame took to be answered. A window presents by default, so this is
39
+ time-to-display and reads about **one refresh period** — 16ms on a 60Hz
40
+ screen is the system working. On `frameClock: 'fence'` it is a server
41
+ round trip instead, and a paint that builds in 0.3ms against 20ms there
42
+ _is_ a server-side problem (software-fallback RENDER ops, a virtualized
43
+ GPU) rather than a renderer one. A frame the frame pacer held
44
+ ([elements.md](elements.md#framerate--pacing-the-frames-under-a-flood))
52
45
  carries `waited=18.0ms` — how long the claim waited before the clock got
53
- it; a frame that was not held carries nothing, so the default's lines are
54
- the lines they always were.
46
+ it.
55
47
  - `chrome:/tmp/trace.json` — [Chrome Trace Event
56
48
  JSON](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU),
57
49
  written at exit. Open it in Perfetto or `about:tracing`: React commits
@@ -60,15 +52,14 @@ REACT_X11_TRACE=summary npm run examples:dashboard
60
52
  `CompositeGlyphs32` inside one commit".
61
53
 
62
54
  Append `+stacks` to `requests` or `chrome` to capture a JS stack per
63
- request (node-x11's seq2stack mechanism). An X error is asynchronous — it
64
- arrives long after the call that caused it returned — and the captured
65
- stack is the only thing that maps it back to your code. It costs an
66
- `Error` capture per request, so it is opt-in.
55
+ request. An X error arrives long after the call that caused it returned,
56
+ and the captured stack is the only thing that maps it back to your code. It
57
+ costs an `Error` capture per request, so it is opt-in.
67
58
 
68
59
  The tracer attaches to a connection _after_ its handshake, so the
69
- connection-setup packet — the one carrying the X auth cookie — is
70
- structurally invisible to it, and payload bytes are never recorded: only
71
- opcode, length and direction.
60
+ connection-setup packet — the one carrying the X auth cookie — is never
61
+ seen, and payload bytes are never recorded: only opcode, length and
62
+ direction.
72
63
 
73
64
  ### `startTrace()` — the same thing as an API
74
65
 
@@ -81,12 +72,9 @@ const { requests, bytesOut, replies, byOpcode } = trace.stop();
81
72
  ```
82
73
 
83
74
  With no `app` the trace follows every connection the renderer has open or
84
- opens later; pass `startTrace({ app })` to trace one specific connection
85
- (the bench harness does this). `byOpcode` maps decoded request names —
86
- `'CopyArea'`, `'Render.CompositeGlyphs32'` — to counts. This is the same
87
- splitter `npm run bench` uses, promoted to a runtime feature; for
88
- committed regression numbers still use the bench (`scripts/bench/`), whose
89
- in-process server keeps them deterministic.
75
+ opens later; pass `startTrace({ app })` to trace one specific connection.
76
+ `byOpcode` maps decoded request names — `'CopyArea'`,
77
+ `'Render.CompositeGlyphs32'` — to counts.
90
78
 
91
79
  ## Repaint flashing — `REACT_X11_DEBUG_PAINT`
92
80
 
@@ -114,71 +102,67 @@ Error: invalidated here
114
102
  at ...
115
103
  ```
116
104
 
117
- Full repaints are the renderer's main performance bug class (see
118
- "Protocol efficiency" in AGENTS.md); before this switch nothing surfaced
119
- them. Expected full repaints exist too — a resize, the first frame after a
120
- mount, ntk invalidating its backing store — and their reasons say so.
105
+ Full repaints are the main performance bug to look for. Expected ones exist
106
+ too — a resize, the first frame after a mount, the backing store being
107
+ invalidated — and their reasons say so.
121
108
 
122
- A bounded frame can still be doing the work of an unbounded one, and this is
123
- where to notice it: an element that draws a whole **scene** into one node — a
124
- graph view, a chart, an editor — is one node to the damage machinery, so a
125
- tight outline around what moved can sit over a pane that redrew all of
126
- itself inside it. Such an element reads the pass's rect and claims its own
127
- commits ([extending.md](extending.md#drawing-a-scene-into-one-node)).
109
+ A bounded frame can still be doing the work of an unbounded one: an element
110
+ that draws a whole **scene** into one node — a graph view, a chart, an
111
+ editor — is one node to the damage machinery, so a tight outline around
112
+ what moved can sit over a pane that redrew all of itself inside it. Such an
113
+ element reads the pass's rect and claims its own commits
114
+ ([extending.md](extending.md#drawing-a-scene-into-one-node)).
128
115
 
129
116
  ## `REACT_X11_NO_SCOPED_FLOORS=1`
130
117
 
131
118
  Measures the content floors over the whole tree on every layout change,
132
- the way they were measured before a change inside an absolutely positioned
133
- box with its own width and height was measured in that box alone — and
134
- before a change down a column whose height is its content's (a document, a
135
- log, a feed in a scroll pane) was measured alone and the column summed. For
136
- measuring the two against each other on the same build, and as first aid if
137
- a box ever comes out the wrong size after its content changes. Read once at
138
- startup; answers only to `1`.
119
+ instead of measuring a change inside an absolutely positioned box with its
120
+ own width and height in that box alone, or a change down a column whose
121
+ height is its content's (a document, a log, a feed in a scroll pane) alone
122
+ with the column summed. For comparing the two on the same build, and as
123
+ first aid if a box ever comes out the wrong size after its content changes.
139
124
 
140
125
  ## `REACT_X11_NO_UNREAD_WIDTHS=1`
141
126
 
142
127
  Measures every box's min-content width in the width passes — over the whole
143
- tree, and over a box measured alone — the way it was measured before a pass
144
- read only the widths a floor is written from. Without it, a leaf that nothing
145
- reads the width of — a paragraph down a column in a document, where no row's
146
- floor depends on it — is not set a word to a line to find one, which on
147
- 600 KB of Markdown was a second of the first frame. For measuring the two against each other, and as first aid if
128
+ tree, and over a box measured alone — instead of only the widths a floor is
129
+ written from. By default a leaf whose width nothing reads (a paragraph down
130
+ a column in a document) is not set a word to a line to find one, which
131
+ matters for a large first frame. For comparing the two, and as first aid if
148
132
  something in a row ever comes out narrower than its content after the first
149
- frame. Read once at startup; answers only to `1`.
133
+ frame.
150
134
 
151
135
  ## `REACT_X11_NO_REFIT_HOLD=1`
152
136
 
153
- Lays an `'auto'`-sized window out at the size it has on the frame its
154
- content asked for a new one, the way it did before that frame waited for the
155
- server's answer ([elements.md](elements.md#natural-size)). On X11 that frame shows
156
- the new content at the old size — a label that grew wraps, and is cut off —
157
- for the round trip before the resize lands. For measuring the two against
158
- each other, and as first aid if an `'auto'` window ever stops repainting
159
- after its content changes. Read once at startup; answers only to `1`.
137
+ By default, the frame on which an `'auto'`-sized window's content asks for a
138
+ new size waits for the server's answer ([elements.md](elements.md#natural-size)).
139
+ This switch lays it out at the size it has on that frame instead, so on X11
140
+ that frame shows the new content at the old size — a label that grew wraps,
141
+ and is cut off — for the round trip before the resize lands. For comparing
142
+ the two, and as first aid if an `'auto'` window ever stops repainting after
143
+ its content changes.
160
144
 
161
145
  ## `REACT_X11_NO_SCROLL_BLIT=1`
162
146
 
163
- Disables the scroll-blit fast path (a pure scroll `CopyArea`s the
164
- surviving band and repaints only the exposed strip), so a scroll frame
165
- repaints its whole viewport again. For measuring the blit against the
166
- plain path on the same build, and as first aid if a scroll ever
167
- misrenders. Read once at startup, like the switches above, and like them
168
- it answers only to `1` — any other value leaves the blit on.
169
-
170
- It covers the element-owned form of the same shift as well —
171
- [`scrollContents`](extending.md#panning-a-scene-you-drew), which a pane
172
- that pans a scene it drew itself calls — so a scene that misrenders while
173
- panning is one variable away from being told apart from one that misdraws.
174
- And it covers the `<glarea>` overlay's copy: a child of a surface that only
175
- moved is moved on its pane rather than repainted
176
- ([elements.md](elements.md#glarea)), and with this set it is repainted
177
- there instead. So too the window's own copy of a subtree that only moved: a
178
- card with a background nothing shows through, dragged across a pane, is
179
- copied where it went and the frame paints the strip it uncovered
180
- ([extending.md](extending.md#an-element-that-covers-its-box)); with this
181
- set it is repainted where it was and where it went.
147
+ Disables the scroll-blit fast path (a pure scroll copies the surviving band
148
+ and repaints only the exposed strip), so a scroll frame repaints its whole
149
+ viewport again. For measuring the blit against the plain path on the same
150
+ build, and as first aid if a scroll ever misrenders.
151
+
152
+ It also covers the other paths that move retained pixels instead of
153
+ repainting them:
154
+
155
+ - [`scrollContents`](extending.md#panning-a-scene-you-drew), which a pane
156
+ that pans a scene it drew itself calls — so a scene that misrenders while
157
+ panning is one variable away from being told apart from one that misdraws;
158
+ - the `<glarea>` overlay's copy, where a child of a surface that only moved
159
+ is moved on its pane rather than repainted ([elements.md](elements.md#glarea));
160
+ - the window's own copy of a subtree that only moved: a card with a
161
+ background nothing shows through, dragged across a pane, is copied where
162
+ it went and the frame paints the strip it uncovered
163
+ ([extending.md](extending.md#an-element-that-covers-its-box)).
164
+
165
+ With this set, each of them repaints instead.
182
166
 
183
167
  ## `REACT_X11_NO_PANE_SHAPE=1`
184
168
 
@@ -188,8 +172,7 @@ actually paints ([elements.md](elements.md#glarea)). A rectangle covers the
188
172
  GL frame with the surface's `clearColor` wherever the child's reach goes
189
173
  past its ink — a `<Flow>` graph's edges stop short of every card and lose
190
174
  their arrowheads — so this is for measuring the cut against the rectangle,
191
- and as first aid if a pane's cut is ever wrong. Read once at startup; it
192
- answers only to `1`.
175
+ and as first aid if a pane's cut is ever wrong.
193
176
 
194
177
  ## `REACT_X11_STRICT_TOKENS=1`
195
178
 
@@ -199,11 +182,10 @@ By default a token the palette in force does not define is dropped, named
199
182
  loudly on stderr — token, element, owning component, and every token the
200
183
  palette _does_ have — and counted as a failure through `process.exitCode`.
201
184
  The widget paints one property short, which is visible, and the rest of the
202
- app keeps running. That is the default because the check runs when a node's
203
- ancestry completes, and on most of those paths a throw cannot be caught: on
204
- a fresh mount React is completing the `<window>`, so the error is attributed
205
- above every boundary inside it, and a desktop light/dark switch reaches the
206
- tree from an X event with no React on the stack at all.
185
+ app keeps running. (A throw there could not be caught by most boundaries:
186
+ the check runs as a node's ancestry completes, often while React completes
187
+ the `<window>` or from a desktop light/dark switch with no React on the
188
+ stack.)
207
189
 
208
190
  Set this when you would rather stop than paint something wrong — a kiosk, or
209
191
  a CI job that should fail at the first bad token rather than at the
@@ -213,84 +195,64 @@ screenshot diff:
213
195
  REACT_X11_STRICT_TOKENS=1 npm run examples:theming
214
196
  ```
215
197
 
216
- Strict mode still throws somewhere useful. The error is raised from the
217
- offending node's own `commitMount`, on its own fiber, so an
218
- [error boundary](react-features.md) at any depth inside the window catches
219
- it and renders a fallback for that subtree rather than losing the window.
220
- With no boundary above it, it reaches `onUncaughtError` and the process
221
- stops — which is the point. The one path that cannot be routed is the
222
- desktop appearance switch; there strict mode crashes.
223
-
224
- Read once at startup like the switches above, and answers only to `1`.
198
+ The error is raised from the offending node's own `commitMount`, on its own
199
+ fiber, so an [error boundary](react-features.md) at any depth inside the
200
+ window catches it and renders a fallback for that subtree rather than
201
+ losing the window. With no boundary above it, it reaches `onUncaughtError`
202
+ and the process stops. The one path that cannot be routed is the desktop
203
+ appearance switch; there strict mode crashes.
225
204
 
226
205
  ## `REACT_X11_NO_BOUNDS_CACHE=1`
227
206
 
228
- Disables the cached paint reach. A bounded frame asks every subtree on the
229
- way to its rect whether it reaches in, and the answer — the node's rect
230
- unioned with its descendants' — is kept until something that moves it says
231
- so: a rect assigned by layout, a child list change, a style swap, a state
232
- flip, a change the node announces about itself. Recomputing it per pass made
233
- a one-cell repaint cost the whole tree (1.25ms at 3,600 nodes, 0.37ms
234
- cached). With this set every walk is fresh, so if a node ever goes missing
235
- from a bounded repaint — culled by a reach nobody updated — this is the
236
- first thing to try, and a fix that lands here is a change nobody announced
237
- through `invalidate`. Read once at startup; answers only to `1`.
207
+ Disables the cached paint reach — the rect of a node unioned with its
208
+ descendants', which a bounded frame asks of every subtree on the way to its
209
+ rect, and which is kept until layout, a child list change, a style swap, a
210
+ state flip or the node's own `invalidate` moves it. With this set every walk
211
+ is fresh, so if a node ever goes missing from a bounded repaint — culled by
212
+ a reach nobody updated — this is the first thing to try, and a fix that
213
+ lands here is a change nobody announced through `invalidate`.
238
214
 
239
215
  ## `REACT_X11_NO_STROKE_CHUNKING=1` (macOS, Windows)
240
216
 
241
- Makes the 2d context the Cocoa and win32 backends share
242
- (`src/backend/context2d.js`) hand every stroke to the platform in one call —
243
- one `CGContextStrokePath` on CoreGraphics — instead of splitting a path with
244
- many subpaths into several —
245
- [docs/macos.md](macos.md#stroking-a-path-with-many-subpaths) for what the
246
- split buys (a 4,000-subpath stroke goes from 986ms to 113ms) and
247
- the one thing it costs (the antialiased fringe where two subpaths' strokes
248
- overlap is half-covered twice rather than covered once, and reads a little
249
- lighter). Set this to compare the two on the same build, or as first aid if
250
- a drawing that strokes thousands of subpaths ever looks wrong at its seams.
251
- `ctx.strokeChunking = false` is the same switch for one context. Read once
252
- at startup, like the switches above, and answers only to `1`.
253
-
254
- Nothing on X11 reads it: there a stroke is one coverage mask over the
255
- path's bounding box, so a bigger path is _fewer_ uploads and the split
217
+ The 2d context the Cocoa and win32 backends share splits a stroke with many
218
+ subpaths into several platform calls, which makes a stroke of thousands of
219
+ subpaths many times faster; the antialiased fringe where two subpaths'
220
+ strokes overlap is then half-covered twice rather than covered once, and
221
+ reads a little lighter. This switch hands every stroke to the platform in
222
+ one call. Set it to compare the two on the same build, or as first aid if a
223
+ drawing that strokes thousands of subpaths ever looks wrong at its seams.
224
+ `ctx.strokeChunking = false` is the same switch for one context.
225
+
226
+ Nothing on X11 reads it: there a stroke is one coverage mask, so splitting
256
227
  would be a loss.
257
228
 
258
229
  ## `REACT_X11_NO_TRANSPARENCY=1`
259
230
 
260
231
  Makes every display answer the way one with no 32-bit visual does:
261
232
  [`transparent`](elements.md#transparent--rounded-corners-and-translucency)
262
- is ignored, windows are created on
263
- the ordinary visual, `'@supports transparency'`
264
- ([styling.md](styling.md#capability-queries)) never matches, and
265
- `useSupports('transparency')` is false.
233
+ is ignored, windows are created on the ordinary visual,
234
+ `'@supports transparency'` ([styling.md](styling.md#capability-queries))
235
+ never matches, and `useSupports('transparency')` is false.
266
236
 
267
237
  ```sh
268
238
  REACT_X11_NO_TRANSPARENCY=1 npm run examples:tooltips
269
239
  ```
270
240
 
271
- It exists because the fallback design is otherwise unreachable on the
272
- machine you work on. Rounded menus, the tooltip's arrow and any
273
- `@supports` block of your own have a second look for displays that cannot
274
- composite — and the only way to see it used to be stopping the compositor,
275
- which takes every other window on the desktop with it. Both halves answer
276
- this switch at once, deliberately: a window that took the ARGB visual under
277
- a `useSupports` that said no would size a popup for chrome it then refused
278
- to draw.
241
+ It lets you see the fallback design — rounded menus, the tooltip's arrow,
242
+ any `@supports` block of your own — without stopping the compositor, which
243
+ would take every other window on the desktop with it. Both halves answer
244
+ this switch at once, so a window never takes the ARGB visual under a
245
+ `useSupports` that said no.
279
246
 
280
247
  What it models is a display with **no 32-bit visual** — XQuartz, a plain
281
248
  remote server. The other degraded case, an ARGB window with _nothing
282
- compositing it_, is the one where a cleared corner comes out black rather
283
- than showing the desktop; that one needs a real session with the compositor
284
- off (or `setCompositingForTests`), since the visual has to be taken for the
285
- question to arise. Both render the same by design, which is the invariant
286
- worth checking.
287
-
288
- Like the switches above it answers only to `1`, so a stale
289
- `NO_TRANSPARENCY=0` cannot quietly mean the opposite of what it says.
290
- Unlike them it is read per call rather than once at startup — it is asked
291
- when a window is created and when a `@supports` block resolves, never on
292
- the frame path, and being live is what lets a test turn it on around a
293
- single mount.
249
+ compositing it_ (a cleared corner comes out black rather than showing the
250
+ desktop), needs a real session with the compositor off, or
251
+ `setCompositingForTests`. Both should render the same.
252
+
253
+ Unlike the switches above it is read per call rather than once at startup —
254
+ when a window is created and when a `@supports` block resolves, never on the
255
+ frame path — so a test can turn it on around a single mount.
294
256
 
295
257
  ## The paint cache — `REACT_X11_NO_PAINT_CACHE`, `REACT_X11_PAINT_CACHE=verify`
296
258
 
@@ -327,35 +289,16 @@ Three switches, all read once at startup:
327
289
 
328
290
  ## Rounded boxes — the corner-glyph fast path
329
291
 
330
- A rounded box is the most common non-rectangular thing a UI draws and,
331
- historically, the most expensive: its corners become a polygon, and a
332
- polygon becomes either server-side trapezoids (which glamor does not
333
- accelerate — measured at ~5.6ms per operation on a virtualized GPU) or a
334
- client-side coverage mask that is uploaded again every frame.
335
-
336
- ntk 6.7.0 recognizes the shape instead. A `fill()` or `stroke()` whose path
337
- is exactly one axis-aligned `roundRect` on integer geometry goes out as
338
- **four cached corner glyphs plus `FillRectangles`** — the corners ride the
339
- glyph path every X server optimizes, cached server-side after first use,
340
- and the straight runs between them are plain rectangles. Nothing is
341
- rasterized or uploaded in steady state. `_paintBackground` and
342
- `_paintBorder` already draw exactly that shape, so every `borderRadius` box
343
- takes the route with no change to your code.
344
-
345
- What it is worth, measured on this repo's `bench:frames` cards mode against
346
- a glamor/virgl server — 48 cards, all recoloured every frame:
292
+ On X11, a `fill()` or `stroke()` whose path is exactly one axis-aligned
293
+ `roundRect` on integer geometry — what every `borderRadius` box draws — goes
294
+ out as four cached corner glyphs plus rectangles, with nothing rasterized or
295
+ uploaded in steady state. A box that falls back still renders, identically,
296
+ as a polygon, which on a server without accelerated trapezoids (a
297
+ virtualized GPU) can cost an order of magnitude more per frame. The fallback
298
+ is silent, so the counters below are how you notice it.
347
299
 
348
- | | fps | paint | landed | wire/frame |
349
- | ------------------------ | ---- | ------- | ------- | ---------- |
350
- | `--border=2` (fast path) | 57.8 | 0.78 ms | 0.70 ms | 13 KB |
351
- | `--border=2 --no-glyphs` | 3.8 | 7.53 ms | 287 ms | 342 KB |
352
-
353
- (measured on the fence clock, where the column is server drain — on the
354
- default vertical-blank clock the fast-path row would read about one refresh
355
- period instead, and the cliff would show in `fps` and `dropped`)
356
-
357
- A bail-out is a silent perf cliff — the box still renders, identically,
358
- just via the route above — so the counters matter more than usual.
300
+ Design notes and measurements:
301
+ [design/debugging.md](../design/debugging.md#rounded-boxes--the-corner-glyph-fast-path).
359
302
 
360
303
  ### Seeing which boxes took it
361
304
 
@@ -371,43 +314,25 @@ REACT_X11_TRACE=summary npm run examples:tasks
371
314
 
372
315
  The same tally is on `trace.stop().shapes` as `{ hits, misses }`, and ntk
373
316
  prints a process-wide version under `NTK_DEBUG_SHAPES=1`. In the opcode
374
- histogram the route shows up directly: `Render.CompositeGlyphs8` and
375
- `Render.FillRectangles` where `Render.Trapezoids`, `Render.AddTraps` or a
376
- `PutImage`-heavy `Render.Composite` used to be.
317
+ histogram the fast path shows as `Render.CompositeGlyphs8` and
318
+ `Render.FillRectangles`; the fallback as `Render.Trapezoids`,
319
+ `Render.AddTraps` or a `PutImage`-heavy `Render.Composite`.
377
320
 
378
321
  ### Why a box falls back
379
322
 
380
323
  `misses` counts by reason. In rough order of how often they bite:
381
324
 
382
- - `fractional` — the box, or the stroke's band, is not on whole pixels.
383
- Usually **fractional layout geometry**: a box laid out on a half-pixel
384
- bails whatever its border width is.
385
-
386
- On ntk < 7 an _odd_ border width bailed too, and it was the one to know
387
- about. `_paintBorder` strokes the box inset by half the border width,
388
- putting the stroke's centre-line radius at `borderRadius - borderWidth/2`
389
- — half-integer whenever the border is odd — and the stroke path wanted an
390
- integer radius. A 1px or 3px rounded border therefore kept its corners on
391
- the polygon route while the fill underneath took the fast one, which on
392
- the 48-card wall was 26x the wire per frame (341 KB against 13 KB) and 5x
393
- the client paint. ntk 7 takes those radii (sidorares/ntk#218), so a 1px
394
- border is now as cheap as a 2px one and no border width needs choosing
395
- around this.
396
-
325
+ - `fractional` — the box, or the stroke's band, is not on whole pixels:
326
+ usually a box laid out on a half-pixel. Any border width, odd or even,
327
+ takes the fast path.
397
328
  - `radius-cap` — the corner radius is over `shapePolicy.maxRadius` (64 by
398
- default; glyph-atlas behaviour past that is driver-specific). Also what
399
- the off switch reports.
329
+ default). Also what the off switch reports.
400
330
  - `gradient` — the fill or stroke style is not a solid colour.
401
331
  - `clip-mask` — a non-rectangular clip is in effect. Rectangular clips are
402
- fine; they go out as picture clip rectangles. A box's own `overflow` with
403
- a `borderRadius` is not one of these on X11: its children are clipped to
404
- its rectangle and the corner squares they reach are put back afterwards
405
- (`src/nodes/roundclip.js`, issue #685), so everything inside a rounded
406
- card or pane stays on the route. The exceptions still clip to the
407
- outline: a box that is corner all the way along a side (a pill whose
408
- corner squares would overlap), and one laid out off the pixel grid. So a
409
- `clip-mask` miss points at one of those, or at a clip an element sets
410
- itself.
332
+ fine, and so, on X11, is a rounded box's own `overflow` clip: everything
333
+ inside a rounded card or pane stays on the fast path. The exceptions are a
334
+ box that is corner all the way along a side (a pill), one laid out off the
335
+ pixel grid, and a clip an element sets itself.
411
336
  - `dashes` — `borderStyle: 'dashed'`.
412
337
  - `transform` — the context's matrix is not translate-only.
413
338
  - `composite-op`, `geometry`, `radii-mix`, `join` — a non-`source-over`
@@ -425,40 +350,22 @@ root.app.shapePolicy = { maxRadius: 32, cacheBytes: 128 << 10 };
425
350
  ```
426
351
 
427
352
  `NTK_NO_SHAPE_GLYPHS=1` does the same from the environment. Both are for
428
- A/B measurement and for cornering a suspected rendering difference — the
429
- route is not something an application should need to turn off.
430
-
431
- ```sh
432
- npm run bench:frames -- cards 8 --border=2 # fast path
433
- npm run bench:frames -- cards 8 --border=2 --no-glyphs # same scene, polygons
434
- npm run bench:frames -- cards 8 --border=1 # the odd-width cliff
435
- ```
436
-
437
- The two routes are not bit-identical: they rasterize the same arc by
438
- different means, so corner pixels differ by antialiasing — under 4/255 for
439
- fills, up to 48/255 on a stroked corner. Everything outside the corner
440
- boxes is exact, which is the property the decomposition rests on (the
441
- glyphs and the rectangles partition the pixels, so no pixel is painted
442
- twice and translucent colours are safe). `test/rounded-box-glyphs.test.js`
443
- asserts exactly that split.
444
-
445
- In practice the difference does not reach a screenshot: regenerating
446
- `docs/img/*.png` with the route off moves between 0 and 0.06% of pixels,
447
- by at most 8/255. If `npm run screenshots` ever produces a bigger diff than
448
- that after a toolkit bump, the cause is somewhere else — ntk 6.7.0's other
449
- change, an arc flattener that subdivides curves more exactly, moved those
450
- same screenshots far more than this route did.
353
+ A/B measurement and for cornering a suspected rendering difference — an
354
+ application should not need to turn it off. The two routes differ only in
355
+ corner antialiasing (under 4/255 for fills, up to 48/255 on a stroked
356
+ corner); every pixel outside the corners is exact, and translucent colours
357
+ are safe.
451
358
 
452
359
  ## Invalidation reasons
453
360
 
454
- Every internal `invalidate()` call now names why it ran, from a small
455
- closed set (`INVALIDATE_REASONS` in `src/nodes/damage.js`, which says what
456
- each one means): `props`, `position`, `style-state`, `shadow`, `outline`,
457
- `theme`, `ink`, `direction`, `scale`, `units`, `animation`, `scroll`, `text`,
458
- `selection`, `content`, `measure`, `child-list`, `focus`, `caret`, `resize`,
459
- `mount`, `expose`, `highlight`, `trace-updates`, `capabilities`, `layout`. The
460
- frame's collected reasons are what `REACT_X11_TRACE=requests` frame lines,
461
- the chrome trace's frame slices, the full-repaint warning and
462
- `examples/stress/perf.js` print. After a frame they are readable on the
463
- window node as `root._lastReasons` (instrumentation surface, like
361
+ Every internal `invalidate()` call names why it ran, from a small closed
362
+ set: `props`, `position`, `style-state`, `shadow`, `outline`, `theme`,
363
+ `ink`, `direction`, `scale`, `units`, `animation`, `scroll`, `text`,
364
+ `selection`, `content`, `measure`, `child-list`, `focus`, `caret`,
365
+ `resize`, `mount`, `expose`, `highlight`, `trace-updates`, `capabilities`,
366
+ `layout` (`INVALIDATE_REASONS` in `src/nodes/damage.js` says what each one
367
+ means). A frame's collected reasons are what `REACT_X11_TRACE=requests`
368
+ frame lines, the chrome trace's frame slices and the full-repaint warning
369
+ print. After a frame they are readable on the window node as
370
+ `root._lastReasons` (an instrumentation surface, like
464
371
  `root._lastDamageRects` — not public API).