react-x11 2.51.1 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +9 -2
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# Runtime diagnostics
|
|
2
|
+
|
|
3
|
+
Three switches answer the three questions a slow or misbehaving frame
|
|
4
|
+
raises: _what went on the wire_ (`REACT_X11_TRACE`), _what got repainted_
|
|
5
|
+
(`REACT_X11_DEBUG_PAINT`), and _why_ (invalidation reasons, printed by
|
|
6
|
+
both). They complement [devtools.md](devtools.md) (component tree) and
|
|
7
|
+
`REACT_X11_DEBUG_LAYOUT=1` (outline every drawn node, colour by depth).
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Protocol tracing — `REACT_X11_TRACE`
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
REACT_X11_TRACE=summary npm run examples:dashboard
|
|
28
|
+
# at exit:
|
|
29
|
+
# react-x11 trace: 4211 requests (312.4KB out), 89 replies, 41 events, 0 errors (48.1KB in)
|
|
30
|
+
# 1780 Render.CompositeGlyphs32
|
|
31
|
+
# 512 Render.Composite
|
|
32
|
+
# ...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `summary` — request/byte totals and an opcode histogram, printed to
|
|
36
|
+
stderr when the process exits.
|
|
37
|
+
- `requests` — one stderr line per request as it is sent
|
|
38
|
+
(`x11 → Render.FillRectangles 36B`), plus one line per painted frame
|
|
39
|
+
(`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))
|
|
52
|
+
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.
|
|
55
|
+
- `chrome:/tmp/trace.json` — [Chrome Trace Event
|
|
56
|
+
JSON](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU),
|
|
57
|
+
written at exit. Open it in Perfetto or `about:tracing`: React commits
|
|
58
|
+
and painted frames are slices, requests and errors are instant events —
|
|
59
|
+
so "this interaction sent 4000 requests" becomes "3600 of them were
|
|
60
|
+
`CompositeGlyphs32` inside one commit".
|
|
61
|
+
|
|
62
|
+
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.
|
|
67
|
+
|
|
68
|
+
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.
|
|
72
|
+
|
|
73
|
+
### `startTrace()` — the same thing as an API
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { startTrace } from 'react-x11/debug';
|
|
77
|
+
|
|
78
|
+
const trace = startTrace(); // sink: 'summary' | 'requests' | 'chrome'
|
|
79
|
+
await interaction();
|
|
80
|
+
const { requests, bytesOut, replies, byOpcode } = trace.stop();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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.
|
|
90
|
+
|
|
91
|
+
## Repaint flashing — `REACT_X11_DEBUG_PAINT`
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
REACT_X11_DEBUG_PAINT=1 npm run examples:dashboard
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every painted frame strokes its damage rects in a colour that rotates per
|
|
98
|
+
frame. A region repainting every frame strobes; one that painted once
|
|
99
|
+
leaves a single quiet outline. This is the browser's "paint flashing" for
|
|
100
|
+
this renderer.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
REACT_X11_DEBUG_PAINT=full npm run examples:dashboard
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Additionally warns — with the invalidation's reasons and the stack of the
|
|
107
|
+
`invalidate()` call that made the frame unbounded — whenever a frame
|
|
108
|
+
degrades to a full-window repaint:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
react-x11: full-window repaint (800x600) reasons=props
|
|
112
|
+
Error: invalidated here
|
|
113
|
+
at WindowNode.invalidate (src/nodes/invalidate.js:…)
|
|
114
|
+
at ...
|
|
115
|
+
```
|
|
116
|
+
|
|
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.
|
|
121
|
+
|
|
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)).
|
|
128
|
+
|
|
129
|
+
## `REACT_X11_NO_SCOPED_FLOORS=1`
|
|
130
|
+
|
|
131
|
+
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`.
|
|
139
|
+
|
|
140
|
+
## `REACT_X11_NO_UNREAD_WIDTHS=1`
|
|
141
|
+
|
|
142
|
+
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
|
|
148
|
+
something in a row ever comes out narrower than its content after the first
|
|
149
|
+
frame. Read once at startup; answers only to `1`.
|
|
150
|
+
|
|
151
|
+
## `REACT_X11_NO_REFIT_HOLD=1`
|
|
152
|
+
|
|
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`.
|
|
160
|
+
|
|
161
|
+
## `REACT_X11_NO_SCROLL_BLIT=1`
|
|
162
|
+
|
|
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.
|
|
182
|
+
|
|
183
|
+
## `REACT_X11_NO_PANE_SHAPE=1`
|
|
184
|
+
|
|
185
|
+
Leaves every X11 pane over a `<glarea>` the rectangle of its child's reach,
|
|
186
|
+
where by default a pane is cut with the SHAPE extension to what its child
|
|
187
|
+
actually paints ([elements.md](elements.md#glarea)). A rectangle covers the
|
|
188
|
+
GL frame with the surface's `clearColor` wherever the child's reach goes
|
|
189
|
+
past its ink — a `<Flow>` graph's edges stop short of every card and lose
|
|
190
|
+
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`.
|
|
193
|
+
|
|
194
|
+
## `REACT_X11_STRICT_TOKENS=1`
|
|
195
|
+
|
|
196
|
+
Makes an unresolvable `$token` fatal instead of reported.
|
|
197
|
+
|
|
198
|
+
By default a token the palette in force does not define is dropped, named
|
|
199
|
+
loudly on stderr — token, element, owning component, and every token the
|
|
200
|
+
palette _does_ have — and counted as a failure through `process.exitCode`.
|
|
201
|
+
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.
|
|
207
|
+
|
|
208
|
+
Set this when you would rather stop than paint something wrong — a kiosk, or
|
|
209
|
+
a CI job that should fail at the first bad token rather than at the
|
|
210
|
+
screenshot diff:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
REACT_X11_STRICT_TOKENS=1 npm run examples:theming
|
|
214
|
+
```
|
|
215
|
+
|
|
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`.
|
|
225
|
+
|
|
226
|
+
## `REACT_X11_NO_BOUNDS_CACHE=1`
|
|
227
|
+
|
|
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`.
|
|
238
|
+
|
|
239
|
+
## `REACT_X11_NO_STROKE_CHUNKING=1` (macOS, Windows)
|
|
240
|
+
|
|
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
|
|
256
|
+
would be a loss.
|
|
257
|
+
|
|
258
|
+
## `REACT_X11_NO_TRANSPARENCY=1`
|
|
259
|
+
|
|
260
|
+
Makes every display answer the way one with no 32-bit visual does:
|
|
261
|
+
[`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.
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
REACT_X11_NO_TRANSPARENCY=1 npm run examples:tooltips
|
|
269
|
+
```
|
|
270
|
+
|
|
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.
|
|
279
|
+
|
|
280
|
+
What it models is a display with **no 32-bit visual** — XQuartz, a plain
|
|
281
|
+
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.
|
|
294
|
+
|
|
295
|
+
## The paint cache — `REACT_X11_NO_PAINT_CACHE`, `REACT_X11_PAINT_CACHE=verify`
|
|
296
|
+
|
|
297
|
+
`<svg>` renders its content once and composites the result on
|
|
298
|
+
later repaints, keyed on _what is drawn_ rather than on which node drew it —
|
|
299
|
+
so a wall of 400 cells holding eight distinct icons keeps eight rendered
|
|
300
|
+
copies, and a repaint of unchanged content is one composite per cell. Your own
|
|
301
|
+
drawings opt in with [`<canvas cacheKey>`](elements.md#cachekey--draw-it-once).
|
|
302
|
+
|
|
303
|
+
Three switches, all read once at startup:
|
|
304
|
+
|
|
305
|
+
- `REACT_X11_NO_PAINT_CACHE=1` disables it entirely, so everything paints
|
|
306
|
+
live. For measuring against the cached path on the same build, and as first
|
|
307
|
+
aid if anything ever renders stale — same role as `REACT_X11_NO_SCROLL_BLIT`.
|
|
308
|
+
- `REACT_X11_PAINT_CACHE=verify` re-renders every cache hit and compares a
|
|
309
|
+
digest of the drawing calls against the one recorded when the entry was
|
|
310
|
+
made. A cache key that fails to name something the drawing reads then says
|
|
311
|
+
so, loudly, at the moment it would otherwise have shown a stale pixel:
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
react-x11: paint cache key does not cover the paint of <canvas>.
|
|
315
|
+
key: canvas|64x24@1|spark:3
|
|
316
|
+
The drawing changed while the key did not, so a cached frame would show
|
|
317
|
+
stale pixels. Add whatever changed to the key.
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Slow by construction — it does all the work the cache exists to avoid, plus
|
|
321
|
+
the comparison. Worth leaving on while developing a `cacheKey`.
|
|
322
|
+
|
|
323
|
+
- `REACT_X11_DEBUG_PAINT_CACHE=1` prints a line per frame: entries, bytes
|
|
324
|
+
held, and hits/misses/renders/evictions. Rising `renders` on a still screen
|
|
325
|
+
means keys that change when they should not; rising `evictions` means the
|
|
326
|
+
working set does not fit the budget.
|
|
327
|
+
|
|
328
|
+
## Rounded boxes — the corner-glyph fast path
|
|
329
|
+
|
|
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:
|
|
347
|
+
|
|
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.
|
|
359
|
+
|
|
360
|
+
### Seeing which boxes took it
|
|
361
|
+
|
|
362
|
+
`REACT_X11_TRACE=summary` ends its histogram with the tally:
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
REACT_X11_TRACE=summary npm run examples:tasks
|
|
366
|
+
# react-x11 trace: 83 requests (15.6KB out), 32 replies, ...
|
|
367
|
+
# 6 Render.CompositeGlyphs8
|
|
368
|
+
# 6 Render.FillRectangles
|
|
369
|
+
# rounded boxes: 6 fast (glyph+rect), 2 fell back (fractional 2)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The same tally is on `trace.stop().shapes` as `{ hits, misses }`, and ntk
|
|
373
|
+
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.
|
|
377
|
+
|
|
378
|
+
### Why a box falls back
|
|
379
|
+
|
|
380
|
+
`misses` counts by reason. In rough order of how often they bite:
|
|
381
|
+
|
|
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
|
+
|
|
397
|
+
- `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.
|
|
400
|
+
- `gradient` — the fill or stroke style is not a solid colour.
|
|
401
|
+
- `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.
|
|
411
|
+
- `dashes` — `borderStyle: 'dashed'`.
|
|
412
|
+
- `transform` — the context's matrix is not translate-only.
|
|
413
|
+
- `composite-op`, `geometry`, `radii-mix`, `join` — a non-`source-over`
|
|
414
|
+
operation, a degenerate or oversized box, mixed per-corner radii on a
|
|
415
|
+
stroke, a non-miter join on a square one.
|
|
416
|
+
|
|
417
|
+
### Turning it off
|
|
418
|
+
|
|
419
|
+
`app.shapePolicy` sits beside `rasterPolicy` and `textPolicy`:
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
const root = await createRoot();
|
|
423
|
+
root.app.shapePolicy = { maxRadius: 0 }; // off; every box goes back to polygons
|
|
424
|
+
root.app.shapePolicy = { maxRadius: 32, cacheBytes: 128 << 10 };
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`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.
|
|
451
|
+
|
|
452
|
+
## Invalidation reasons
|
|
453
|
+
|
|
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
|
|
464
|
+
`root._lastDamageRects` — not public API).
|