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.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
|
@@ -1,854 +0,0 @@
|
|
|
1
|
-
# Windowed regions: when X11 subwindows pay, and what API to give them
|
|
2
|
-
|
|
3
|
-
_Research document, 2026-08-01. Measured against the `perf/scroll-blit` checkout (nodes.js @
|
|
4
|
-
4219 lines), ntk 4.2.0 installed. All measurements ran on one machine (Apple M1 Pro, macOS 15.2,
|
|
5
|
-
node v26): live XQuartz 2.8.6 (X.Org 21.1.23, custom build), Xvfb 21.1.23 (stock Xorg
|
|
6
|
-
dix/mi/fb), and node-x11's in-process pure-JS X server via the repo's own bench harness.
|
|
7
|
-
Benchmark scripts and raw research notes are preserved out-of-tree (see §10); file:line
|
|
8
|
-
references are to the checkout above and will drift with the code._
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## 0. TL;DR
|
|
13
|
-
|
|
14
|
-
Today every control is drawn into one surface per `<window>`/`<popup>`; hover on 1 of 1000
|
|
15
|
-
controls means `PointerMotion` selected on the whole window plus an O(N) JS hit test per motion
|
|
16
|
-
frame. The research question: when should a control or region get a real X subwindow instead,
|
|
17
|
-
and how automatic should that be?
|
|
18
|
-
|
|
19
|
-
**Findings, compressed:**
|
|
20
|
-
|
|
21
|
-
1. **Subwindows are cheap as objects and expensive as a lifestyle.** A window costs ~290 B server
|
|
22
|
-
heap, ~5 µs to create, 8 B to destroy recursively. But per-child map/move storms are O(siblings)
|
|
23
|
-
each (654 ms to nudge 5000 mapped children on XQuartz), every toolkit that shipped
|
|
24
|
-
window-per-widget abandoned it, and core X cannot alpha-blend a child over its parent — no
|
|
25
|
-
translucency, no antialiased rounded corners, no drawn overlays above a windowed region.
|
|
26
|
-
**Window-per-control is dead on arrival; nothing here re-litigates that.**
|
|
27
|
-
|
|
28
|
-
2. **The measured hover cost is client-side JS, not the wire — and it is fixable client-side.**
|
|
29
|
-
A motion frame at 1000 controls costs ~27 µs and ~8–11 KB of allocation, ~94% of it in
|
|
30
|
-
`hitTest`'s per-node `paintOrder()` churn and unpruned descent, not in the hover machinery.
|
|
31
|
-
GTK4 hit-tests every motion client-side and considers it fine; Flutter does it at 120 Hz on
|
|
32
|
-
phones — the fix is caching + pruning (see §5.1), not protocol surgery. Subwindow event masks
|
|
33
|
-
would win the _wire_ (zero motion traffic, server-side Enter/Leave picking at ~3 ns per mapped
|
|
34
|
-
sibling), which matters for remote X and battery, but they are not needed to fix the CPU cost.
|
|
35
|
-
|
|
36
|
-
3. **The one region where a subwindow structurally wins is the scroll pane.** Moving a child window
|
|
37
|
-
is a server-side blit with Expose only for the revealed strip — verified on XQuartz and Xorg —
|
|
38
|
-
i.e. exactly what the scroll-blit branch hand-builds, minus all ~12 purity gates, the per-frame
|
|
39
|
-
O(N) safety walk, the scrollbar-repair choreography, and the per-notch full-tree
|
|
40
|
-
relayout+absolutize. A `viewport window + content window` scroll pane turns a scroll into one
|
|
41
|
-
20-byte ConfigureWindow — and on Composite servers (**the deployment majority**: Xorg+
|
|
42
|
-
compositor, XWayland), a `backing-store: WhenMapped` content pane scrolls with **zero Expose**:
|
|
43
|
-
the server restores the revealed strip from its own backing. XQuartz (minority, no Composite)
|
|
44
|
-
falls back to Expose-strip repaints served from ntk's pixmap. Constraints: positions are INT16
|
|
45
|
-
(±32767 → re-anchoring for long content), backing memory for the content pane, and drawn rows
|
|
46
|
-
only (windowed rows inside the pane would pay O(children) clip revalidation per scroll frame).
|
|
47
|
-
|
|
48
|
-
4. **API answer from history and from our numbers: automatic-off, explicit-in, at region
|
|
49
|
-
granularity.** Every surviving toolkit converged on: native windows for toplevels + popups
|
|
50
|
-
(react-x11 already does this), plus a small number of explicitly-promoted surfaces where the
|
|
51
|
-
server gives something the drawn path cannot (GL/video/foreign — `<glarea>` already exists),
|
|
52
|
-
plus framework-inserted promotion at _known_ hotspots (Flutter's repaint boundaries around
|
|
53
|
-
scrollables). Fully automatic promotion exists only in browsers and needed a decade of
|
|
54
|
-
squashing heuristics. The concrete proposal is in §8: keep `<box windowed>` out of the public
|
|
55
|
-
API; instead (a) fix the client-side hit test, (b) make the scroll pane internally windowed
|
|
56
|
-
behind a feature gate, (c) keep real windows implicit-by-capability (glarea, future
|
|
57
|
-
`<foreign>`), and (d) optionally spike InputOnly hit-region windows for per-region cursors and
|
|
58
|
-
hover-during-scroll correctness — the only subwindow variant with zero rendering downside.
|
|
59
|
-
**§8.1 turns this into an issue-ready, dependency-ordered action plan** (code, API, and docs
|
|
60
|
-
items with acceptance criteria).
|
|
61
|
-
|
|
62
|
-
5. **Gravity is a latency mask, not a layout engine.** `winGravity` can pin corner/edge-anchored
|
|
63
|
-
children server-side during a parent resize (it works on XQuartz — quartz-wm itself relies on
|
|
64
|
-
it), but it expresses only `x = a + b·W, b ∈ {0, ½, 1}` and never resizes anything; flex-grow,
|
|
65
|
-
stretch (yoga's default), percentages and space-between all need the client anyway. Useful only
|
|
66
|
-
to hide one frame of resize latency on anchored panes; not a reason to add windows.
|
|
67
|
-
|
|
68
|
-
### 0.1 Cross-check against the project goals
|
|
69
|
-
|
|
70
|
-
The goal hierarchy this document is graded against: **(main) perceived performance** — minimum
|
|
71
|
-
input→screen delay, achieved by prepping while idle, updating as much as possible instantly, and
|
|
72
|
-
catching up afterwards; **(note) visual stability** — no stutter, no unexpected jumps, no
|
|
73
|
-
queued-event storms; **(secondary) client CPU first, server CPU second.**
|
|
74
|
-
|
|
75
|
-
| item | input→photon latency | stability | client CPU | server CPU |
|
|
76
|
-
| -------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------------ |
|
|
77
|
-
| A. hit-test caching/pruning | small direct win; big _jitter_ win (less GC) | **GC-pause stutter shrinks** (8–11 KB/event → ~1 KB) | the top measured win | — |
|
|
78
|
-
| B. windowed scroll pane | **the headline win**: scroll applied at server speed, decoupled from React | server blit is atomic; re-anchor seams and hover churn must be engineered (below) | deletes per-notch relayout + gate walks | small (blit + O(pane siblings) clip) |
|
|
79
|
-
| C. capability windows (glarea/foreign) | n/a (capability, not perf) | airspace rules as documented | — | — |
|
|
80
|
-
| D. InputOnly hit regions | **cursor feedback becomes server-side-instant**, immune to client busyness | Enter/Leave uncoalesced → needs frame debounce | motion stream off when idle | ~µs picking |
|
|
81
|
-
| gravity (§5.5) | **upgraded under this lens**: "latency mask" _is_ the main goal during resize gestures | `winGravity: Unmap` prevents stale-position jumps | — | — |
|
|
82
|
-
| window-per-control | still dead: storms _add_ latency and jank | storms are anti-stability | — | O(siblings) each |
|
|
83
|
-
|
|
84
|
-
What the lens changes, specifically:
|
|
85
|
-
|
|
86
|
-
- **B's real rationale is latency, not request counts.** Today a wheel notch is: discrete
|
|
87
|
-
ButtonPress → `scrollBy` → `invalidate(true)` → _wait for the paced frame_ (up to 16 ms +
|
|
88
|
-
fence) → full-tree relayout + absolutize → repaint/blit → present. With a windowed pane the
|
|
89
|
-
ConfigureWindow can be written **synchronously inside the wheel handler, before React runs
|
|
90
|
-
anything** — the screen moves at server speed, and layout/virtualization catch up on the
|
|
91
|
-
following frames. Scroll responsiveness decouples from render cost entirely: even mid-GC or
|
|
92
|
-
mid-heavy-render, queued wheel events still turn into 20-byte moves the server applies
|
|
93
|
-
smoothly, instead of accumulating into one visible catch-up jump. This is the
|
|
94
|
-
"update instantly, then catch up" pattern made structural.
|
|
95
|
-
- **Idle prep has a concrete new lever: overdraw prepainting.** A content pane sized
|
|
96
|
-
viewport+overdraw can have its off-screen band rasterized into backing _while idle_ — then the
|
|
97
|
-
next N notches reveal already-painted pixels with zero client work at input time (server
|
|
98
|
-
restores from backing on Composite targets). Today's window-sized backing cannot prepaint
|
|
99
|
-
beyond the viewport at all. Idle time becomes scroll headroom; re-anchor work also belongs in
|
|
100
|
-
idle, never on the input path.
|
|
101
|
-
- **Stability items promote from "risks" to requirements.** (1) One shared frame clock /
|
|
102
|
-
atomic present per top-level is a _stability_ requirement, not an optimization — two regions
|
|
103
|
-
presenting independently in one gesture is visible tearing (Present is the long-term protocol
|
|
104
|
-
answer). (2) Enter/Leave arrive uncoalesced → hover restyles need a frame-level debounce or
|
|
105
|
-
crossing storms cause per-crossing paints. (3) Re-anchor seams must be invisible: prepaint the
|
|
106
|
-
re-anchored backing before the swap, swap in one frame. (4) Windowed regions should be
|
|
107
|
-
size-stable during interactive resize (GTK's "partial results drawn" is precisely an
|
|
108
|
-
unexpected-jump failure) — or `winGravity: Unmap` them for the gesture.
|
|
109
|
-
- **A's alloc-churn reduction is a stability fix as much as a CPU fix**: per-event garbage is
|
|
110
|
-
what turns into GC pauses, and GC pauses are stutter. The acceptance criterion in §8.1 tracks
|
|
111
|
-
bytes/event, not just µs/event.
|
|
112
|
-
- **An adjacent lever this research surfaced but that needs no windows: the frame clock taxes
|
|
113
|
-
every discrete input with up to one frame of latency.** Coalescing is right for motion;
|
|
114
|
-
for discrete inputs (click, keypress, wheel) an immediate-flush policy — paint the damage from
|
|
115
|
-
a discrete event as soon as the handler returns instead of waiting for the paced frame — is
|
|
116
|
-
probably the single cheapest perceived-latency win in the whole stack, orthogonal to
|
|
117
|
-
subwindows. Added to §8.1 as item 4b.
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## 1. Where we are: the single-surface pipeline and what it costs
|
|
122
|
-
|
|
123
|
-
One ntk `Window` per `<window>`/`<popup>`. All controls are retained JS nodes painted through one
|
|
124
|
-
2d context whose XRender Picture targets the window's **backing pixmap**
|
|
125
|
-
(`ntk/lib/renderingcontext_2d.js:198-218`); ntk presents by `CopyArea(backing → window)` per dirty
|
|
126
|
-
rect and serves real server Expose from the pixmap with zero JS when the backing is valid
|
|
127
|
-
(`ntk/lib/window.js:652-658`).
|
|
128
|
-
|
|
129
|
-
### 1.1 Events
|
|
130
|
-
|
|
131
|
-
`EventManager.attach()` subscribes mousedown/up/move/out + keys + focus **unconditionally**
|
|
132
|
-
([events.js:84](../../src/events.js)), so every window selects `PointerMotion` over its whole area
|
|
133
|
-
whether or not anything wants hover. ntk coalesces motion keep-last per paced 16 ms frame, so the
|
|
134
|
-
expensive work runs at frame rate, not device rate. Per motion frame:
|
|
135
|
-
`hitTest` ([nodes/hittest.js](../../src/nodes/hittest.js)) does a front-to-back recursive descent calling
|
|
136
|
-
`paintOrder()` — three array allocations, a per-child wrapper object, and a sort — _at every
|
|
137
|
-
visited node_, and only prunes a
|
|
138
|
-
subtree when the point is outside **and** the node clips ([nodes/hittest.js](../../src/nodes/hittest.js));
|
|
139
|
-
non-clipping rows recurse into all children regardless. Then the hover path is diffed, `_path` is
|
|
140
|
-
computed twice (hover + dispatch), and `:hover` flips invalidate.
|
|
141
|
-
|
|
142
|
-
**Measured (in-process server, real renderer, 800×600 window, R×C grid of hover-styled boxes):**
|
|
143
|
-
|
|
144
|
-
| controls | `_onMouseMove`, no hover change | alternating cells (hover flip) | garbage/event |
|
|
145
|
-
| -------- | ------------------------------- | ------------------------------ | ------------- |
|
|
146
|
-
| 100 | 3.3 µs | 4.4 µs | ~2–6 KB |
|
|
147
|
-
| 1000 | 27.3 µs | 29.0 µs | ~8–11 KB |
|
|
148
|
-
| 5000 | 132 µs | 135 µs | ~120 KB |
|
|
149
|
-
|
|
150
|
-
~94% of every motion event is the hit test; the hover diff machinery itself is ~1.7 µs. Scaling is
|
|
151
|
-
linear in N (~27–55 ns/control/event). Worst case is the **top-left** control, not bottom-right:
|
|
152
|
-
`hitTest` scans paint order back-to-front (front-to-back in z), so the first-painted control is
|
|
153
|
-
tested last — 57 µs at 1000. Every event also allocates a closure-laden synthetic event object.
|
|
154
|
-
|
|
155
|
-
### 1.2 Damage and paint
|
|
156
|
-
|
|
157
|
-
Damage is a capped list of ≤4 disjoint rects at the window root with least-waste merging
|
|
158
|
-
([nodes/damage.js](../../src/nodes/damage.js)). A hover-flip frame produces one coalesced ~65×16 rect and
|
|
159
|
-
31 ctx calls — the damage _narrowing_ works well. The cost that remains is the walk that exploits
|
|
160
|
-
it: culling tests each candidate child via `_subtreeBounds()`, which is recursive and deliberately
|
|
161
|
-
uncached, so repainting a 65×16 rect at 1000 controls made **2158 recursive `_subtreeBounds`
|
|
162
|
-
calls**. Measured hover-flip frame: ~69 µs flush, split ≈ 55% cull walk / 30% request emission /
|
|
163
|
-
15% bookkeeping; a full-window repaint for comparison is ~1 ms. End-to-end hover flip
|
|
164
|
-
(event + frame) ≈ **105–130 µs of main-thread JS at 1000 controls** — fine at 60 Hz, but all of it
|
|
165
|
-
is O(N) walks that don't shrink when the damage does.
|
|
166
|
-
|
|
167
|
-
Two ntk-side notes that surfaced: (a) each damage pass's first `ctx.clip()` rasterizes a
|
|
168
|
-
**full-surface** a8 clip mask (`renderingcontext_2d.js:992-1005`) — an O(window-area) server cost
|
|
169
|
-
even for a 20×20 rect; (b) `'draw'` with invalid backing is always full-window — expose-rect
|
|
170
|
-
granularity exists only when the backing is valid.
|
|
171
|
-
|
|
172
|
-
### 1.3 Layout
|
|
173
|
-
|
|
174
|
-
One `needsLayout` boolean per window; a layout frame runs yoga (`yoga-layout@3.2.1` **wasm**,
|
|
175
|
-
shared with ntk) once at the root, then an O(N) `absolutize` walk — 4 embind crossings per node —
|
|
176
|
-
regardless of what changed. **Every wheel notch is `invalidate(true, …, 'scroll')` — a full-tree
|
|
177
|
-
relayout + absolutize per notch** ([nodes/layout.js](../../src/nodes/layout.js)), because scroll offset is
|
|
178
|
-
baked into `abs` during absolutize. The scroll-blit fast path then tries to narrow the repaint,
|
|
179
|
-
behind ~12 gates and a per-frame O(N) `_scrollBlitSafe` walk — and is currently **inert at
|
|
180
|
-
runtime** (installed ntk 4.2.0 has no `scrollRegion`; the chain ntk#139 → release → #137/#139 is
|
|
181
|
-
still in flight).
|
|
182
|
-
|
|
183
|
-
### 1.4 Wire (protocol bench, in-process server, counted with the repo's own `xcount.js`)
|
|
184
|
-
|
|
185
|
-
| scenario | requests | bytes out | Composite px |
|
|
186
|
-
| --------------------------------------------------- | ----------------------- | ------------------------ | -------------------------------------------- |
|
|
187
|
-
| mount 1000-control grid | 1038 | 36.8 KB | 640 K |
|
|
188
|
-
| hover flip to adjacent control (today) | ~14 net | ~400 B | 2.6 K |
|
|
189
|
-
| 8 motion samples inside one cell (steady hover tax) | 0 | 0 | 0 — but 32 B _in_ + 1 JS hit test per sample |
|
|
190
|
-
| create+map 1000 child X windows | 2002 | 52 KB | 0 |
|
|
191
|
-
| move one child (ConfigureWindow) | 1 | 20 B | 0 |
|
|
192
|
-
| move all 1000 (ntk as-is) | 2002 + **1001 replies** | 24 KB | 0 |
|
|
193
|
-
| move all 1000 (`frameSync:false`) | 1002 | 20 KB | 0 |
|
|
194
|
-
| windowed hover: crossing a child boundary | **0** | 0 (64 B in: Leave+Enter) | 0 |
|
|
195
|
-
|
|
196
|
-
The `move all 1000` row exposes an ntk design cost: each ntk `Window` has its own frame clock and
|
|
197
|
-
arms a `GetInputFocus` fence when its ConfigureNotify arrives — a 1000-child move storm doubles
|
|
198
|
-
request count and adds 1000 replies. Any windowed-region design must share one frame clock per
|
|
199
|
-
top-level (or create children `frameSync:false, coalesceEvents:false`).
|
|
200
|
-
|
|
201
|
-
### 1.5 What already exists
|
|
202
|
-
|
|
203
|
-
Four kinds of real X windows already work: nested `<window>` (prop-driven geometry, stacking
|
|
204
|
-
synced via `_restackWindowChildren` with server-verified QueryTree tests), `<popup>`
|
|
205
|
-
(override-redirect, focus-delegating), `<glarea>` (the **only layout-driven child window** —
|
|
206
|
-
`absolutize → _syncGeometry` diffs the rounded rect and issues one setState; clipping vs
|
|
207
|
-
scroll panes unsolved, stacking vs `<window>` siblings deliberately undefined), and foreign windows
|
|
208
|
-
(the WM example adopts other clients' windows). `NEXT_STEPS.md` §4 already states the policy this
|
|
209
|
-
document mostly re-confirms: a real window **only when the server gives something the drawn path
|
|
210
|
-
cannot** — "never because it has event handlers or a background color."
|
|
211
|
-
|
|
212
|
-
The isWindow boundary is already carved through every subsystem (yoga membership, absolutize,
|
|
213
|
-
`_subtreeBounds`, `_setRoot`, `paintOrder`, blit safety) — a "windowed region" is a drawn subtree
|
|
214
|
-
given the WindowNode treatment. The genuinely missing piece is small: a yoga node that stays in
|
|
215
|
-
the **parent's** layout tree whose computed rect is pushed to the child window as ConfigureWindow
|
|
216
|
-
(glarea's `_syncGeometry` generalized), plus a paint root that isn't a full WindowNode
|
|
217
|
-
(no WM hints, shared frame clock).
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## 2. What the server actually offers (spec-verified)
|
|
222
|
-
|
|
223
|
-
All claims below were verified verbatim against the X11R7.7 protocol spec (section names cited
|
|
224
|
-
in the research notes; spec source URLs in §11).
|
|
225
|
-
|
|
226
|
-
**Event routing.** The server hit-tests: pointer events originate at the deepest containing window
|
|
227
|
-
and propagate **up** to the first window selecting the type; `do_not_propagate_mask` blocks
|
|
228
|
-
per-type. There is no capture phase — React capture stays client-side. The **`child` field** of
|
|
229
|
-
every delivered pointer event names the immediate child containing the pointer even when _no_
|
|
230
|
-
child selects anything: with one flat layer of region windows this is a free server-side hit test
|
|
231
|
-
one level deep, delivered on the events we already get. node-x11 parses it; ntk and react-x11
|
|
232
|
-
currently discard it.
|
|
233
|
-
|
|
234
|
-
**Enter/Leave.** Delivered only to windows selecting them, never propagated, never compressed.
|
|
235
|
-
Sibling→sibling crossing = exactly 2 events (Nonlinear pair), nothing on the parent; nested
|
|
236
|
-
crossings add Virtual events on strictly-intermediate windows. The `detail` field
|
|
237
|
-
(Ancestor/Inferior/Virtual/Nonlinear…) natively encodes the DOM mouseout-vs-mouseleave
|
|
238
|
-
distinction react-x11 synthesizes by path-diffing; `mode` distinguishes real crossings from
|
|
239
|
-
Grab/Ungrab pseudo-crossings. Thin siblings skipped between pointer samples generate no events —
|
|
240
|
-
crossings are computed between successive positions, not along the geometric path.
|
|
241
|
-
|
|
242
|
-
**Implicit grab.** ButtonPress starts an automatic grab: all drag motion and the release deliver
|
|
243
|
-
to the press's event window (negative coords when outside) until buttons release. This reproduces
|
|
244
|
-
DOM implicit pointer capture _and_ "hover stays put during drag" server-side — react-x11 already
|
|
245
|
-
silently relies on it at the whole-window level (`_pressOutside`, scrollbar drags).
|
|
246
|
-
|
|
247
|
-
**InputOnly windows.** Depth 0, border 0, only win-gravity/event-mask/do-not-propagate/
|
|
248
|
-
override-redirect/cursor attributes (anything else is BadMatch). Invisible to all rendering:
|
|
249
|
-
never Expose, never occlude output, cannot be drawables. They own the pointer where they cover:
|
|
250
|
-
local event masks, per-region cursors, server-side Enter/Leave — with **zero** paint-model
|
|
251
|
-
consequences. Gotcha: they occlude _input_ from siblings beneath them; overlap must mirror hit
|
|
252
|
-
priority in stacking order, and non-rectangular hit areas need SHAPE input regions instead.
|
|
253
|
-
|
|
254
|
-
**Gravity.** `winGravity` repositions children on parent resize by the 8 compass deltas (+Static,
|
|
255
|
-
+Unmap which auto-unmaps for "client will relayout anyway"); `bitGravity` retains window bits.
|
|
256
|
-
`winGravity` was live-probed and works on both XQuartz and Xvfb — quartz-wm's own growbox is a
|
|
257
|
-
SouthEastGravity child and its title-bar prelight is an InputOnly tracking window, so it is
|
|
258
|
-
exercised daily on our platform. `bitGravity` is implemented in Xorg's mi layer
|
|
259
|
-
(source-verified, `miSlideAndSizeWindow`) but was **not** probed on XQuartz's rootless resize
|
|
260
|
-
path. Gravity moves, never resizes; servers may legally ignore bitGravity.
|
|
261
|
-
|
|
262
|
-
**Scroll-by-move.** "If … a window is moved … the contents of the window are not lost but move
|
|
263
|
-
with the window" — the server blits, and Expose covers only newly-valid regions. This is the
|
|
264
|
-
protocol-guaranteed version of scroll-blit. Coordinate limits: positions are INT16 (±32767);
|
|
265
|
-
sizes are CARD16 (≤65535 on the wire) but effectively bounded by the 16-bit coordinate/region
|
|
266
|
-
space — treat ±32767 as the safe envelope for any window geometry.
|
|
267
|
-
|
|
268
|
-
**Backing store.** Advisory in the spec; in practice a **majority-target capability**. Modern
|
|
269
|
-
Xorg (≥1.15) implements it via per-window Composite automatic redirection — a full window-sized
|
|
270
|
-
server pixmap each — and any Composite-capable server advertises `WhenMapped` (verified live: a
|
|
271
|
-
`WhenMapped` child on Xvfb survived occlude-then-reveal with zero Expose). Since most expected
|
|
272
|
-
users run Xorg+compositor or XWayland, **server-side content preservation for opted-in region
|
|
273
|
-
windows is available on the deployment majority**, feature-detectable for free from the
|
|
274
|
-
connection setup's per-screen `backing-stores` field. The exception is XQuartz — this dev
|
|
275
|
-
machine — which ships without Composite: no core backing store at all (probe confirmed: the same
|
|
276
|
-
`WhenMapped` child Exposed on reveal), and its rootless frame buffer covers _toplevel_ occlusion
|
|
277
|
-
only. `save-under` is hardwired dead everywhere. Design consequence: treat server backing store
|
|
278
|
-
as a **feature-detected acceleration tier** — on Composite servers it can carry a region's
|
|
279
|
-
content preservation outright (and should _replace_, not double, ntk's backing pixmap for that
|
|
280
|
-
window, since both cost the same window-sized pixmap); ntk's own backing pixmap remains the
|
|
281
|
-
portable fallback that keeps XQuartz correct. Don't sprinkle it on small controls (a hidden
|
|
282
|
-
pixmap + Damage record per window); use it at region granularity.
|
|
283
|
-
|
|
284
|
-
**Translucency — the hard rendering limit.** Core X is a pixel-ownership model: a child window
|
|
285
|
-
_replaces_ parent pixels where mapped; there is no core operation blending child over parent. An
|
|
286
|
-
ARGB child overwrites rather than blends even under a compositor (compositors redirect and blend
|
|
287
|
-
**top-level hierarchies only**; children are flattened inside by core clipping). The only route is
|
|
288
|
-
Composite manual redirection + client-side Render — i.e. rebuilding by hand the compositing the
|
|
289
|
-
single drawn surface gives for free (available on the Composite majority, impossible on XQuartz —
|
|
290
|
-
so even as an advanced option it cannot be relied on universally). SHAPE gives non-rectangular
|
|
291
|
-
boundaries but binary/aliased ones. **Opaque + rectangular is the promotion boundary**; shadows, rounded corners, translucent
|
|
292
|
-
overlays, cross-fades disqualify a subtree.
|
|
293
|
-
|
|
294
|
-
**Costs.** CreateWindow 32+4n B, ConfigureWindow 12+4n B (move = 20 B), Map/Unmap/Destroy 8 B,
|
|
295
|
-
all round-trip-free; DestroyWindow is recursive (one 8-byte request tears down a subtree,
|
|
296
|
-
measured 20 000 windows in 3.9 ms). XIDs: 29-bit space, 2 097 152 per client at Xorg defaults
|
|
297
|
-
(protocol floor: 2^18). **node-x11's `AllocID` has an explicit unhandled-overflow TODO and ntk
|
|
298
|
-
never releases window IDs** (it does for pixmaps/pictures/glyphsets and cursor GCs; the 2d
|
|
299
|
-
context's GC and the present/clear GCs also never release) — harmless today, a real leak under
|
|
300
|
-
window churn; the XC-MISC binding already exists in node-x11, just unwired.
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## 3. Server reality on our targets (measured)
|
|
305
|
-
|
|
306
|
-
**Target weighting.** Most expected users run Linux Xorg (with a compositor) or XWayland —
|
|
307
|
-
Composite present, backing store available, standard DDX paths. XQuartz is where development
|
|
308
|
-
happens but is a **minority deployment**: a custom rootless server without Composite, ~2× the
|
|
309
|
-
dix cost floor on window storms, and its own bug surface. Read the numbers below accordingly:
|
|
310
|
-
the Xvfb column approximates what the majority sees (a stock-Xorg dix floor); the XQuartz column
|
|
311
|
-
is the compatibility floor the design must not break, **not** the environment to optimize for.
|
|
312
|
-
|
|
313
|
-
### 3.1 Per-window server cost — a non-issue
|
|
314
|
-
|
|
315
|
-
| measure | value | source |
|
|
316
|
-
| ------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
317
|
-
| heap per plain window (Xorg 21.1.23, exact malloc accounting) | **293 B** (256 B WindowRec+privates + 32 B resource + hash growth) | heap-tool diff, 20 000 windows |
|
|
318
|
-
| with WindowOptRec (cursor/backing/shape/…) | +96 B exact | same |
|
|
319
|
-
| RSS per unmapped window, live XQuartz | ~250–460 B | two independent runs disagree 1.8× (10k- and 20k-window deltas); macOS RSS is compression-noisy — heap figure above is authoritative |
|
|
320
|
-
| RSS per mapped window, XQuartz | ~1.3 KB | 5000-window delta (clip data included) |
|
|
321
|
-
| clip fragmentation from 10 000 randomly overlapping siblings | ~3.7 B/window | 1-box/empty regions stored inline; fully-obscured = empty clip |
|
|
322
|
-
|
|
323
|
-
Even 20 000 windowed regions ≈ 5–25 MB server-side. Memory never binds; CPU does:
|
|
324
|
-
|
|
325
|
-
### 3.2 The algorithmic costs — where window-per-control dies
|
|
326
|
-
|
|
327
|
-
| operation | XQuartz | Xvfb (Xorg dix floor) |
|
|
328
|
-
| ---------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
329
|
-
| CreateWindow, batched | ~2–7 µs/win | ~1–4 µs/win |
|
|
330
|
-
| MapWindow storm, 1000 / 5000 mapped children | 12–37 µs/win / **91 µs/win (455 ms)** | 10 µs / 42 µs/win |
|
|
331
|
-
| ConfigureWindow move, mapped, 1000 / 5000 siblings | 21–53 µs/op / **131 µs/op (654 ms total)** | 15–18 µs / 68 µs/op |
|
|
332
|
-
| same, unmapped (control) | ~4 µs/op | — |
|
|
333
|
-
| raise 1 of 5000 siblings | 1.3 ms | 0.09 ms |
|
|
334
|
-
| map / move **one** window among N mapped siblings | — | 0.13–0.22 ms @1k → 1.0–1.4 ms @20k (O(N)) |
|
|
335
|
-
| server event pick (miSpriteTrace), flat worst case | — | **1.5 µs @100 → 60 µs @20k motions (~3 ns per mapped sibling, ≈6 ns per sibling scanned)** |
|
|
336
|
-
| scroll = move **one** container child (1000 grandchild windows riding) | 4.9 ms/frame | 0.27 ms/frame |
|
|
337
|
-
| parent toplevel resize | **14–20 ms regardless of children** | 0.14–0.46 ms |
|
|
338
|
-
| destroy 10–20k subtree (one request) | 4–8 ms | 3.9 ms |
|
|
339
|
-
|
|
340
|
-
Readings: clip revalidation (`miValidateTree`) is O(mapped siblings) per configure/map and the
|
|
341
|
-
storms are superlinear; XQuartz is ~2× the dix floor and its rootless toplevel-resize path is its
|
|
342
|
-
fragile spot (multiple crash bugs filed). Server-side _picking_ is effectively free and
|
|
343
|
-
hierarchy-aware. InputOnly children: 1000 created in ~2 ms on XQuartz, zero errors — and XQuartz's
|
|
344
|
-
own WM depends on them.
|
|
345
|
-
|
|
346
|
-
**The 4.9 ms scroll row needs care:** that pane carried 1000 windowed grandchildren — the
|
|
347
|
-
anti-pattern — plus a full XSync round trip per frame; how much of the 4.9 ms is per-child clip
|
|
348
|
-
work vs pane-area blit was **not isolated** (a §10 follow-up; miComputeClips also has a VTMove
|
|
349
|
-
translate-only fast path, so "revalidated 1000 clips" is not established). A content pane with
|
|
350
|
-
_drawn_ rows should be one window move — expected µs-scale by extrapolation from the small-window
|
|
351
|
-
move rows, but **not yet measured for a large pane**; measure pane-area scaling before committing
|
|
352
|
-
(§10). The design rule stands: **windowed viewport + windowed content pane + drawn rows.**
|
|
353
|
-
|
|
354
|
-
### 3.3 Compositor interaction
|
|
355
|
-
|
|
356
|
-
On Xorg+compositor and XWayland, child windows never reach the compositor — redirection captures
|
|
357
|
-
whole toplevel hierarchies (children are flattened into the toplevel's pixmap server-side). On
|
|
358
|
-
XQuartz likewise only toplevels get Quartz frames. So windowed regions cost the native window
|
|
359
|
-
system nothing anywhere; their price is X-server clip CPU. Also verified: XWayland creates
|
|
360
|
-
`wl_surface`s only for InputOutput children of root; and GLX **into child windows** is XQuartz's
|
|
361
|
-
historically buggiest path (Wine crashes, Mesa fix in 2.7.4) — relevant to `<glarea>`, which sits
|
|
362
|
-
exactly there. One majority-target upside worth naming: XWayland and composited Xorg are
|
|
363
|
-
standard xserver builds, so the Composite-based backing store verified on Xvfb is expected to
|
|
364
|
-
work there too (dix-level machinery; XWayland itself unprobed — §10).
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## 4. What 30 years of toolkits concluded
|
|
369
|
-
|
|
370
|
-
- **Xt/Motif**: window-per-widget → per-widget ConfigureWindow storms on every relayout (layout
|
|
371
|
-
was client-side anyway), clear-then-expose flicker. Motif's windowless _gadgets_ are the
|
|
372
|
-
cautionary inverse: the manager selected **all** events including MotionNotify and hit-tested
|
|
373
|
-
client-side — exactly react-x11's current shape — and by X11R5 "gadgets are worse than widgets
|
|
374
|
-
… from a performance perspective." The balance is empirical and era-dependent; Motif got it
|
|
375
|
-
wrong in both directions.
|
|
376
|
-
- **GTK 2.18 client-side windows (2009)**: kept the entire GdkWindow API — per-region event
|
|
377
|
-
masks, enter/leave, grabs, scroll-copy — as a _client-side abstraction_; the real window carries
|
|
378
|
-
the union of descendant interests. What broke was almost entirely native-handle consumers
|
|
379
|
-
(plus expose-handler timing assumptions and a long-broken win32 backend).
|
|
380
|
-
Larsson's flicker post is the canonical description of the scroll-copy tearing hazard the
|
|
381
|
-
scroll-blit branch handles. Original driver: offscreen rendering/transforms, which per-widget X
|
|
382
|
-
windows pin to the screen.
|
|
383
|
-
- **GTK4 (2020)**: no subwindows at all, even client-side. One GdkSurface per toplevel/popup;
|
|
384
|
-
cached per-widget render nodes; frame clock; pure client-side picking with synthesized
|
|
385
|
-
crossings. GTK4's considered answer to hover-on-1-of-1000 is "hit-test every motion
|
|
386
|
-
client-side, make picking cheap."
|
|
387
|
-
- **Qt 4.4 alien widgets (2008)**: native-off by default; multi-granularity opt-in
|
|
388
|
-
(env var / app / `WA_NativeWindow` / **implicit promotion on `winId()`** — asking for the handle
|
|
389
|
-
_is_ the opt-in). Native promotion is viral to ancestors (clipping/stacking need it) unless
|
|
390
|
-
explicitly stopped — a real API-design cost to remember. Where native reenters
|
|
391
|
-
(`createWindowContainer`), the docs read like our glarea caveats: "stacks on top … as an opaque
|
|
392
|
-
box," undefined stacking among containers.
|
|
393
|
-
- **AWT/Swing**: the mixing/"airspace" problem — heavyweights always above lightweights,
|
|
394
|
-
punching through menus and tooltips; took Sun 15 years to paper over via window shaping.
|
|
395
|
-
Structurally identical to `<glarea>` today.
|
|
396
|
-
- **Browsers/Flutter**: layers, not windows. Promotion criterion that survived: **different
|
|
397
|
-
update cadence, moves-without-repaint, or foreign content** — never "is interactive." Both
|
|
398
|
-
explicit hints (`will-change`, `RepaintBoundary`) and automatic triggers exist, with squashing
|
|
399
|
-
as the defense against layer explosion; Flutter auto-inserts boundaries around scrolling list
|
|
400
|
-
children. Flutter is the existence proof that event routing needs no windows at any scale.
|
|
401
|
-
|
|
402
|
-
**No toolkit that left window-per-widget ever went back.** And none ever adopted InputOnly
|
|
403
|
-
hit-region children for controls — that path is unexplored, not discredited.
|
|
404
|
-
|
|
405
|
-
---
|
|
406
|
-
|
|
407
|
-
## 5. The five concern axes
|
|
408
|
-
|
|
409
|
-
### 5.1 Local event masks
|
|
410
|
-
|
|
411
|
-
**Concern:** one hover-sensitive control in a 1000-control window forces PointerMotion on the
|
|
412
|
-
whole window and a hit test per motion.
|
|
413
|
-
|
|
414
|
-
**True today,** but decompose the cost:
|
|
415
|
-
|
|
416
|
-
- _Wire:_ 32 B/motion sample in, zero out (H1b). At 125–1000 Hz device rate, coalesced to frame
|
|
417
|
-
rate by ntk. Negligible locally; matters over SSH-forwarded X.
|
|
418
|
-
- _CPU:_ ~27 µs + 8–11 KB garbage per motion frame at 1000 controls — all in `hitTest`/`paintOrder`
|
|
419
|
-
churn. This is a **client-side data-structure problem**: `paintOrder()` re-allocates and
|
|
420
|
-
re-sorts per node per event; non-clipping containers don't prune; `_path` runs twice.
|
|
421
|
-
A cached paint-order array (invalidated on child-list/zIndex change), point-in-subtree-bounds
|
|
422
|
-
pruning (using bounds cached during `absolutize`), and a reused event object would cut this
|
|
423
|
-
10–50× with zero protocol change. GTK3's union-mask + client routing and GTK4/Flutter picking
|
|
424
|
-
are precedents that this is enough at any realistic scale.
|
|
425
|
-
- _What subwindows add on top:_ zero motion traffic at the server (mask off PointerMotion
|
|
426
|
-
entirely unless someone has `onMouseMove`), Enter/Leave computed by the server at ~6 ns/sibling,
|
|
427
|
-
the free one-level `child` field even without any child selecting events, per-region cursors as
|
|
428
|
-
a creation attribute, correct hover when content moves _under_ a stationary pointer (scroll —
|
|
429
|
-
the client model gets this stale today), and implicit-grab press/drag/release per control.
|
|
430
|
-
|
|
431
|
-
**Also true:** ntk's per-region event-mask machinery (lazy mask growth from listeners) already
|
|
432
|
-
exists and react-x11 defeats it by subscribing everything unconditionally
|
|
433
|
-
([events.js:84](../../src/events.js)). Handler-counted lazy subscription (the r3f rule `<glarea>`
|
|
434
|
-
already implements one level up) would drop `PointerMotion` from windows with no motion/hover
|
|
435
|
-
consumers at all — the cheapest win of the lot.
|
|
436
|
-
|
|
437
|
-
**Latent bug either way:** `_onMouseOut` clears the entire hover path on _any_ LeaveNotify
|
|
438
|
-
without checking `detail`/`mode` ([events.js:384](../../src/events.js)). Today every trigger —
|
|
439
|
-
pointer leaving the toplevel (the common case), crossings into `<glarea>`/nested `<window>`
|
|
440
|
-
children, Grab/Ungrab pseudo-crossings from popup grabs — happens to clear hover acceptably;
|
|
441
|
-
the first windowed region would break hover globally. Any subwindow work starts by making
|
|
442
|
-
crossing handling detail/mode-aware.
|
|
443
|
-
|
|
444
|
-
**Verdict:** fix client-side first; subwindow event masks are not the remedy for the CPU concern.
|
|
445
|
-
InputOnly regions (§8, option D) are the right vehicle _if_ the wire/cursor/scroll-hover benefits
|
|
446
|
-
are wanted, because they carry none of the rendering costs.
|
|
447
|
-
|
|
448
|
-
### 5.2 Damage area calculation
|
|
449
|
-
|
|
450
|
-
Damage narrowing already works (one coalesced rect, 31 ctx calls for a hover flip). What
|
|
451
|
-
subwindows would scope is the _walk_: per-region damage lists mean a region's invalidate touches
|
|
452
|
-
only its subtree, and the 4-rect cap stops being shared across unrelated corners of the UI. But
|
|
453
|
-
the same effect is available client-side by caching `_subtreeBounds` during `absolutize` (the
|
|
454
|
-
"recomputed on demand" design assumption — bounds asked only by the handful of invalidating nodes
|
|
455
|
-
— is falsified by measurement: 2158 recursive calls per 65×16 repaint, 55% of frame time).
|
|
456
|
-
|
|
457
|
-
What subwindows do **not** fix: the parent paints into a _pixmap_, and pixmaps have no children —
|
|
458
|
-
X's child-clipping applies only at the final CopyArea, so parent rasterization under windowed
|
|
459
|
-
children still happens unless the client culls it (today's `_outsideDamage`, retargeted at region
|
|
460
|
-
rects). A per-region window also brings per-region present (CopyArea) and — critical on the ntk
|
|
461
|
-
side — a per-Window frame clock and fence with **no cross-window frame transaction**: two regions
|
|
462
|
-
presenting independently can tear against each other. One ntk frame clock per toplevel is a
|
|
463
|
-
prerequisite (§9).
|
|
464
|
-
|
|
465
|
-
**Verdict:** cache subtree bounds client-side; per-region damage is a real but secondary benefit
|
|
466
|
-
that falls out of any region window added for other reasons (scroll), not a reason by itself.
|
|
467
|
-
|
|
468
|
-
### 5.3 Backing store invalidation
|
|
469
|
-
|
|
470
|
-
ntk already double-buffers per window and serves Expose from backing with no JS. Windowed regions
|
|
471
|
-
would each carry a backing pixmap — that is the memory axis: viewport-sized today vs
|
|
472
|
-
region-extent-sized after promotion. A 900×90 000 content pane is absurd (324 MB) regardless of
|
|
473
|
-
who holds the pixmap; the design must band the backing, opt the pane out (`backingStore:false` +
|
|
474
|
-
JS repaint of exposed strips), or size the pane viewport+overdraw and re-anchor.
|
|
475
|
-
|
|
476
|
-
**Who holds the pixmap is a per-server choice, and the majority target has the better option.**
|
|
477
|
-
On Composite servers (most users: Xorg+compositor, XWayland), setting `backing-store: WhenMapped`
|
|
478
|
-
on the content pane makes the _server_ retain the pane's clipped-away band — so scroll-by-move
|
|
479
|
-
generates **no Expose at all**: the server blits the surviving band and restores the revealed
|
|
480
|
-
strip from its own backing (this is exactly the behavior the in-process JS server showed as the
|
|
481
|
-
subwindow best case, and it is real on the majority target — occlude/reveal probe passed on
|
|
482
|
-
Xvfb). ntk's backing pixmap for that window then becomes redundant and should be dropped
|
|
483
|
-
(`backingStore:false`), or memory doubles for no benefit. On XQuartz (minority; no Composite)
|
|
484
|
-
the same window falls back to ntk's pixmap with Expose-strip repaints served from it. Both modes
|
|
485
|
-
cost one window-sized pixmap; the difference is which side owns it and whether scroll exposes
|
|
486
|
-
reach the client at all. Feature-detect via the screen's `backing-stores` field. Either way,
|
|
487
|
-
don't put backing store on small controls — on Xorg each opted-in window is a hidden full-window
|
|
488
|
-
redirect pixmap plus a Damage record.
|
|
489
|
-
|
|
490
|
-
There is also a fix available with no windows at all — but it starts in ntk, not react-x11: ntk
|
|
491
|
-
4.2.0 collapses every invalid-backing restore to a full-window synthetic `'draw'` (no rects reach
|
|
492
|
-
the renderer at all). Delivering the coalesced expose-rect union (§9.7) plus a react-x11 consumer
|
|
493
|
-
would restore partial granularity without any windows.
|
|
494
|
-
|
|
495
|
-
### 5.4 Layout engine cache
|
|
496
|
-
|
|
497
|
-
The key structural fact (verified in code): **a region's inner layout depends only on its
|
|
498
|
-
width/height, never its position** — position enters solely as the absolutize origin, and a
|
|
499
|
-
WindowNode lays out from `{0,0,w,h}`. So for a size-stable region, a parent-driven move is
|
|
500
|
-
exactly one ConfigureWindow with zero inner relayout and zero repaint (the backing rides along).
|
|
501
|
-
`_sizePinned` + the `_reflowed` contained-reflow machinery are the embryo of this boundary; a
|
|
502
|
-
windowed region is `_sizePinned` made real, enforced by the server instead of damage arithmetic.
|
|
503
|
-
|
|
504
|
-
The scroll pane is where this bites hardest today: scroll offset baked into `abs` makes every
|
|
505
|
-
wheel notch a full-tree relayout + absolutize (~4 embind crossings × N per notch). A windowed
|
|
506
|
-
scroll pane deletes that: scroll stops touching layout at all. Short of windows, the same cheap
|
|
507
|
-
observation — "scroll is a translation, not a reflow" — could be exploited client-side by storing
|
|
508
|
-
scroll offset as a paint/hit-test-time translation instead of re-absolutizing, at the cost of
|
|
509
|
-
threading an offset through paint and hitTest (a mini transform stack — the thing the flat `abs`
|
|
510
|
-
design deliberately avoids). That refactor is the client-side alternative to compare against.
|
|
511
|
-
|
|
512
|
-
When a parent resize _does_ change a region's size: two frames of latency (parent pass →
|
|
513
|
-
ConfigureWindow → child relayout) unless the inner pass runs synchronously in the same flush —
|
|
514
|
-
resize asynchrony between parent and children is literally GTK's stated top reason for leaving
|
|
515
|
-
subwindows ("asynchronously resize all child windows and sometimes get partial results drawn").
|
|
516
|
-
Regions should be size-stable or few.
|
|
517
|
-
|
|
518
|
-
### 5.5 Server-supported layout (gravity etc.)
|
|
519
|
-
|
|
520
|
-
Works (both servers, quartz-wm depends on it), already forwardable through ntk creation
|
|
521
|
-
attributes from JSX props, and nearly useless _as a layout engine_ for yoga: it expresses
|
|
522
|
-
corner/edge pinning of fixed-size children only — no stretch, no flex, no percentages, no
|
|
523
|
-
space-between; and yoga's default cross-axis is `stretch`. Its honest use is latency-masking
|
|
524
|
-
anchored panes during interactive resize (child snaps with the edge server-side, one frame
|
|
525
|
-
before the client's authoritative ConfigureWindow), plus `winGravity: Unmap` to suppress
|
|
526
|
-
stale-position jumps when relayout is coming anyway. **Under the project's goal hierarchy that
|
|
527
|
-
is not a dismissal — "latency mask" is the main goal during a resize gesture** (§0.1): resize is
|
|
528
|
-
continuous direct manipulation, the client is structurally one frame late, and the server moving
|
|
529
|
-
anchored windows instantly is "update instantly, catch up" in its purest form. It applies only
|
|
530
|
-
to windows that already exist for other reasons (a windowed scroll pane's panes and bars, capability
|
|
531
|
-
windows) and only to gravity-expressible positions — so it is a free rider on B, never a reason
|
|
532
|
-
to create windows. ntk gap: gravity is creation-time only (no ChangeWindowAttributes path
|
|
533
|
-
exposed; §9.10).
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
## 6. When a windowed region pays — ranked
|
|
538
|
-
|
|
539
|
-
| candidate | why | verdict |
|
|
540
|
-
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
541
|
-
| **Scrollview (viewport + content pane)** | server-side blit + strip expose deletes the whole scroll-blit gate stack, the O(N) safety walk, per-notch relayout; drawn rows ride along free; zero-Expose scroll with server backing store on Composite targets (the majority) | **the** case — prototype behind a flag (§8 B) |
|
|
542
|
-
| **`<glarea>` / video / foreign embed** | server capability the drawn path cannot express | already exists; keep implicit-by-capability |
|
|
543
|
-
| **`<popup>` / nested `<window>`** | WM interaction, bounds escape | already exists |
|
|
544
|
-
| InputOnly hit regions (cursor, hover-during-scroll, remote-X wire, drag semantics) | zero rendering cost; all event-locality wins | optional spike (§8 D) — no toolkit precedent either way |
|
|
545
|
-
| Large opaque panes with independent update cadence (dashboard tiles, terminal grid, animation layer) | per-region damage + present isolation | only after frame-clock sharing exists in ntk; measure first — client-side fixes may suffice |
|
|
546
|
-
| Hover-dense control grids | the motivating concern | **no** — fix the client-side hit test (§5.1) |
|
|
547
|
-
| Table/list rows | GTK's treeview-window-per-row is the canonical regret | **no** |
|
|
548
|
-
| Anything translucent, shadowed, rounded-antialiased, transform-animated | core X cannot blend child over parent | **structurally excluded** |
|
|
549
|
-
|
|
550
|
-
---
|
|
551
|
-
|
|
552
|
-
## 7. What breaks — the hazards checklist
|
|
553
|
-
|
|
554
|
-
Every item below is verified (code, spec, or measured), not speculative:
|
|
555
|
-
|
|
556
|
-
1. **Airspace**: a windowed region is always above all drawn content of its parent; drawn
|
|
557
|
-
tooltips/menus/overlays cannot paint over it. Mitigation: overlays are already `<popup>`s in
|
|
558
|
-
react-x11 — the airspace rule aligns with existing practice, but must be documented per region.
|
|
559
|
-
2. **Translucency/rounding**: opaque rectangles only (§2). Antialiased rounded corners against
|
|
560
|
-
the parent are impossible (SHAPE is binary).
|
|
561
|
-
3. **Hover detail/mode blindness**: `_onMouseOut` clears all hover on any LeaveNotify —
|
|
562
|
-
pre-existing bug, first windowed region trips it ([events.js:384](../../src/events.js)).
|
|
563
|
-
4. **ntk InputOnly BadMatch**: ntk unconditionally puts `bitGravity: 1` in every CreateWindow
|
|
564
|
-
value list (`ntk/lib/window.js:375-381`) — illegal for InputOnly; needs an ntk guard (plus a
|
|
565
|
-
`getContext` guard).
|
|
566
|
-
5. **Per-window frame clocks** (a _stability_ requirement under §0.1, not just perf — two
|
|
567
|
-
regions presenting independently in one gesture is visible tearing): N regions = N
|
|
568
|
-
fences/timers, ordering inversions possible
|
|
569
|
-
(move(A) → press(B) can invert since coalescing is per-window), storms double request counts.
|
|
570
|
-
Needs a shared per-toplevel frame clock in ntk.
|
|
571
|
-
6. **XID leak**: ntk never releases window IDs; node-x11 AllocID overflow is an unhandled TODO.
|
|
572
|
-
One-line ntk fix + optionally wire the existing xc-misc binding.
|
|
573
|
-
7. **16-bit geometry**: positions INT16 (±32767), sizes CARD16 — treat ±32767 as the envelope;
|
|
574
|
-
content panes for long lists must re-anchor;
|
|
575
|
-
glarea already has this bug latent (no clamp/unmap when scrolled far out — no guard exists).
|
|
576
|
-
8. **First-paint flash**: `attributes.backgroundColor` is dead in ntk 4.2.0 (no consumer);
|
|
577
|
-
region windows should set `backgroundPixel` (or background None to inherit stale pixels
|
|
578
|
-
rather than flash — the GTK2 anti-flicker rule).
|
|
579
|
-
9. **Resize asynchrony**: parent-size-tracking regions repaint one frame late (GTK's #1 reason
|
|
580
|
-
for leaving). Keep regions size-stable; consider `winGravity: Unmap` for the rest.
|
|
581
|
-
10. **XQuartz specifics** (minority target, but the dev machine — compatibility floor, not
|
|
582
|
-
design driver): no Composite → no server backing store for regions; child-GLX is the
|
|
583
|
-
historically buggy path; toplevel resize is expensive and crash-prone; XTEST off by default
|
|
584
|
-
(blocks input-injection tests on macOS — integration tests should target Xvfb/the JS server).
|
|
585
|
-
11. **Stacking bookkeeping**: regions must join `_restackWindowChildren`-style ordering
|
|
586
|
-
(including vs `<glarea>`, whose stacking is currently undefined), and scroll pane clipping of
|
|
587
|
-
a windowed region needs the server parent-child relationship to be the clip (viewport window),
|
|
588
|
-
not `overflow: hidden` arithmetic.
|
|
589
|
-
12. **Coordinate-space audit**: `_pressOutside`, popup anchoring (`_screenOrigin` composition),
|
|
590
|
-
DevTools `getClientRects`, ClickToComponent — everything reading `abs` as window-absolute
|
|
591
|
-
needs the extra origin composed when a region boundary intervenes.
|
|
592
|
-
13. **Cross-boundary bubbling and focus**: today capture/bubble stops dead at every window
|
|
593
|
-
boundary (`_path` breaks at the owning window, [events.js:130](../../src/events.js)) and
|
|
594
|
-
`_focusables` refuses to walk into `isWindow` children ([events.js:567](../../src/events.js)) —
|
|
595
|
-
a windowed scroll pane needs row clicks to bubble to ancestors outside the region and Tab to
|
|
596
|
-
traverse into it. The existing precedents are popup-style `focusManager` delegation and the
|
|
597
|
-
key path's tree-walking dispatch; both must be generalized to pointer events (with
|
|
598
|
-
child-local → owner coordinate translation) before any region ships.
|
|
599
|
-
|
|
600
|
-
---
|
|
601
|
-
|
|
602
|
-
## 8. API design: the manual → automatic spectrum
|
|
603
|
-
|
|
604
|
-
Ordered from least to most automatic, with verdicts:
|
|
605
|
-
|
|
606
|
-
**A. Client-side fixes only (no new API).** Cache `paintOrder`; cache subtree bounds in
|
|
607
|
-
`absolutize`; prune hit test by cached bounds; single `_path` per event; reused event objects;
|
|
608
|
-
handler-counted lazy event masks (stop selecting PointerMotion when nothing consumes it); use the
|
|
609
|
-
free `child` field once regions exist; expose-rect granularity from ntk instead of full-window.
|
|
610
|
-
**Do this regardless.** It attacks every _measured_ cost, needs no ntk release, and no design
|
|
611
|
-
risk. Expected effect: motion frame from ~27 µs to low-µs; hover-flip flush from ~69 µs to ~30 µs.
|
|
612
|
-
|
|
613
|
-
**B. Framework-internal promotion at known hotspots (invisible API).** The scroll pane becomes
|
|
614
|
-
`viewport window + content pane window + drawn rows` internally, feature-gated
|
|
615
|
-
(`REACT_X11_WINDOWED_SCROLL=1` during bring-up, later a scroll pane prop, eventually default where
|
|
616
|
-
safe). No user-facing "window" concept; identical JSX. This is Flutter's
|
|
617
|
-
`addRepaintBoundaries`-around-scrollables move, and it lands on the only region with a measured
|
|
618
|
-
structural win. Framed by the goals (§0.1): scroll is the most latency-sensitive gesture in the
|
|
619
|
-
toolkit, and this makes its screen response synchronous with the input event (ConfigureWindow
|
|
620
|
-
written in the wheel handler, before React) instead of one relayout + one paced frame later —
|
|
621
|
-
with idle time spent prepainting the overdraw band so revealed content is already backed.
|
|
622
|
-
On the deployment majority (Composite servers) it has a second tier for free:
|
|
623
|
-
give the content pane `backing-store: WhenMapped` (feature-detected from the screen's
|
|
624
|
-
`backing-stores` field, ntk backing dropped for that window) and scrolling stops generating
|
|
625
|
-
Expose entirely — the server owns both the blit and the strip restore; XQuartz falls back to
|
|
626
|
-
strip repaints from ntk's pixmap. Bring-up order: needs the ntk frame-clock/backing work (§9),
|
|
627
|
-
the detail/mode hover fix, cross-boundary bubbling/focus (§7.13), INT16 re-anchoring, and honest
|
|
628
|
-
fallbacks (small viewports and translucent-over-viewport layouts keep the drawn path — the
|
|
629
|
-
scroll-blit gates shrink to "is this scroll pane windowed or not").
|
|
630
|
-
|
|
631
|
-
Two things to say out loud about B. First, **it reverses a recorded decision**: NEXT_STEPS §4
|
|
632
|
-
states verbatim that a scroll pane is "a clip rect + translation on the render list, not a child
|
|
633
|
-
window (optionally optimized later with CopyArea scrolling inside the same window)" — the
|
|
634
|
-
CopyArea option being exactly the scroll-blit branch B would supersede. This document argues the
|
|
635
|
-
reversal on new measured evidence (server-side blit verified on both targets, per-notch relayout
|
|
636
|
-
cost, the gate stack); if B lands, amend NEXT_STEPS §4. Second, **the hermetic test suite cannot
|
|
637
|
-
exercise B's core mechanism**: the in-process JS server gives every window a retained raster, so
|
|
638
|
-
child moves generate no Expose there — the expose-strip half of the design is invisible to it,
|
|
639
|
-
XTEST is absent on this XQuartz, and there is currently zero test coverage for events/focus
|
|
640
|
-
inside nested windows. B's test plan is Xvfb-based integration tests (the only full-fidelity
|
|
641
|
-
target on this machine) plus extending the JS server with real expose-on-move semantics.
|
|
642
|
-
|
|
643
|
-
**C. Explicit region element for capability, not performance.** Keep real windows where the
|
|
644
|
-
server gives capability: `<glarea>` (exists), `<foreign windowId>` (NEXT_STEPS §4), possibly
|
|
645
|
-
`<surface>` for an opaque pane an app _knows_ updates independently — with documented constraints
|
|
646
|
-
(opaque, rectangular, above drawn siblings, clipped only by its parent window). Follow Qt's
|
|
647
|
-
implicit-promotion insight: asking for the native handle (a ref that needs an XID) _is_ the
|
|
648
|
-
opt-in signal. **Do not ship `<box windowed>` as a general perf knob** — the constraint list is
|
|
649
|
-
exactly the airspace/translucency/stacking catalogue every toolkit documents as its regret.
|
|
650
|
-
|
|
651
|
-
**D. InputOnly hit-region spike (orthogonal, optional).** A `hitWindow` internal experiment:
|
|
652
|
-
InputOnly children mirroring a few interactive regions, selecting Enter/Leave/Button, drawn tree
|
|
653
|
-
untouched. Buys per-region cursors as server attributes, hover-during-scroll correctness,
|
|
654
|
-
implicit-grab drag semantics per control, zero motion wire, `do_not_propagate` shielding — at the
|
|
655
|
-
price of ConfigureWindow-per-layout for those regions, XID lifecycle, and the detail/mode fix.
|
|
656
|
-
No rendering hazards at all. The perceived-performance lens (§0.1) adds one argument the CPU
|
|
657
|
-
lens missed: **a per-window cursor is applied by the server the instant the pointer crosses,
|
|
658
|
-
with the client not in the loop at all** — cursor feedback (I-beam over text, resize arrows on a
|
|
659
|
-
split handle) stays instant even while the client is mid-render or mid-GC, where today it rides
|
|
660
|
-
the motion→hover→`setCursor` path and stalls with the client. Worth a spike _after_ A if cursor
|
|
661
|
-
fidelity, hover-during-scroll, or remote-X matter; the CPU argument alone still does not
|
|
662
|
-
justify it.
|
|
663
|
-
|
|
664
|
-
**E. Fully automatic promotion (browser-style heuristics).** Rejected. Requires squashing
|
|
665
|
-
heuristics, memory-pressure valves, and years of tuning; the failure modes (layer explosion) are
|
|
666
|
-
documented at length by Chromium; and our candidate list (§6) is short enough that named hotspots
|
|
667
|
-
cover it. History: only browsers ever made this work, and nothing at react-x11's scale should try.
|
|
668
|
-
|
|
669
|
-
**Recommended sequence: A → B(prototype + measure) → C(as capabilities demand) → D(spike if
|
|
670
|
-
motivated) — E never.**
|
|
671
|
-
|
|
672
|
-
### 8.1 Concrete action plan (issue-ready)
|
|
673
|
-
|
|
674
|
-
Every item below is adoptable as code, API, or documentation without further research. Ordered by
|
|
675
|
-
dependency; acceptance criteria reference the benchmarks preserved out-of-tree (§10). Items are
|
|
676
|
-
phrased so they can be lifted directly into issue drafts (react-x11 / ntk / node-x11 as marked).
|
|
677
|
-
|
|
678
|
-
**Phase 0 — record the decisions (docs).**
|
|
679
|
-
|
|
680
|
-
- **[react-x11 docs]** Add a "when does an element get a real X window" section to
|
|
681
|
-
`docs/elements.md`: the promotion policy (opaque + rectangular + region granularity; real
|
|
682
|
-
windows for capability, never for "has handlers or a background"), the airspace constraints
|
|
683
|
-
(currently documented only in `docs/glx.md`), and the hazard list from §7. This makes the
|
|
684
|
-
policy user-visible instead of implicit in NEXT_STEPS.
|
|
685
|
-
- **[react-x11 docs]** Annotate NEXT_STEPS §4's scroll pane sentence with a pointer to this
|
|
686
|
-
document: the "clip rect + translation" decision is now conditional on the Phase-3 prototype's
|
|
687
|
-
results, and should be amended (not silently contradicted) if B lands.
|
|
688
|
-
|
|
689
|
-
**Phase 1 — client-side fixes, no ntk dependency (react-x11 code).**
|
|
690
|
-
|
|
691
|
-
1. **fix(events): respect crossing `detail`/`mode`** in `_onMouseOut`/hover handling
|
|
692
|
-
([events.js:384](../../src/events.js)) — drop Grab/Ungrab pseudo-crossings, treat
|
|
693
|
-
`detail=Inferior` as "still inside". Prerequisite for every windowed anything; arguably a live
|
|
694
|
-
bug for `<glarea>` + popup-grab edge cases now. Test: hover retention across a glarea/nested
|
|
695
|
-
`<window>` crossing.
|
|
696
|
-
2. **perf(hit-test): cache and prune** — cache `paintOrder()` per node (invalidate on child-list/
|
|
697
|
-
zIndex change), cache subtree bounds during `absolutize` (drop the per-call `_subtreeBounds`
|
|
698
|
-
recursion), prune `hitTest` descent by cached bounds, build `_path` once per motion, reuse the
|
|
699
|
-
synthetic event object. Acceptance: `hitbench.mjs` motion frame ≤5 µs and ≤1 KB alloc at 1000
|
|
700
|
-
controls (from 27 µs / 8–11 KB); hover-flip flush cull share <20% (from ~55%).
|
|
701
|
-
3. **perf(events): handler-counted lazy event masks** — stop subscribing `mousemove`/etc.
|
|
702
|
-
unconditionally in `EventManager.attach()`; select PointerMotion only while some node has a
|
|
703
|
-
motion/hover/cursor consumer (the r3f rule `<glarea>` already applies one level up). Note the
|
|
704
|
-
cursor dependency: `_updateCursor` rides the hover path, so `cursor` props count as consumers.
|
|
705
|
-
Acceptance: a protocol-bench scenario asserting zero MotionNotify delivery for an app with no
|
|
706
|
-
hover/motion/cursor usage.
|
|
707
|
-
4. **chore(windows): pass `backgroundPixel` at creation** (forwardable through ntk today) so
|
|
708
|
-
windows stop flashing undefined content before first paint.
|
|
709
|
-
4b. **perf(latency): immediate flush for discrete inputs** (filed as
|
|
710
|
-
[#141](https://github.com/sidorares/react-x11/issues/141)) — clicks, keypresses, and wheel
|
|
711
|
-
currently paint on the next paced frame (up to 16 ms + fence later); flush their damage as
|
|
712
|
-
soon as the discrete handler returns, keeping coalescing for continuous motion only. No
|
|
713
|
-
windows involved; probably the cheapest perceived-latency win in the stack (§0.1).
|
|
714
|
-
Acceptance: a caret blink / button `:active` flip reaches the wire in the same event-loop
|
|
715
|
-
turn as the input event (assert with the protocol tracer); motion-driven hover still
|
|
716
|
-
coalesces to frame rate.
|
|
717
|
-
|
|
718
|
-
**Phase 2 — ntk/node-x11 groundwork (file as issues now, accumulate into the next ntk cycle).** 5. **[ntk fix]** Release window XIDs on destroy (pixmaps/pictures/glyphsets already do); note
|
|
719
|
-
the 2d-context/present/clear GC ids also leak. **[node-x11 fix]** wire the existing
|
|
720
|
-
`xc-misc` GetXIDRange binding into `AllocID` (overflow TODO at `xcore.js:377-389`). 6. **[ntk feat]** Shared frame clock/fence per top-level for child windows (or a supported
|
|
721
|
-
`frameSync:false, coalesceEvents:false` child mode) — kills the measured 1000-fence storm
|
|
722
|
-
(protobench E vs E2) and the cross-window event-ordering inversion. 7. **[ntk fix]** InputOnly (`windowClass: 2`) support: omit `bitGravity`/background attributes
|
|
723
|
-
(BadMatch today), guard `getContext`, skip the StructureNotify base mask for micro-windows. 8. **[ntk feat]** `'draw'` with coalesced expose rects (today invalid-backing restores collapse
|
|
724
|
-
to full-window), plus a react-x11 consumer — partial restores without any windows. 9. **[ntk feat]** `serverBackingStore` creation option mapping to the X backing-store attribute
|
|
725
|
-
(deliberately not forwarded today, `window.js:112-121`), with feature detection from the
|
|
726
|
-
screen's `backing-stores` field — enables the zero-Expose scroll tier on Composite servers. 10. **[ntk feat, optional]** post-create `ChangeWindowAttributes` for winGravity/bitGravity
|
|
727
|
-
(TODO at `window.js:975`) — only if gravity latency-masking on anchored panes is pursued.
|
|
728
|
-
|
|
729
|
-
**Phase 3 — the windowed scroll pane prototype (react-x11, flagged, decision gate).** 11. **feat(scroll pane): `REACT_X11_WINDOWED_SCROLL=1`** — viewport window + content pane window + drawn rows; pane sized viewport+overdraw with INT16 re-anchoring; scrollbars drawn on the
|
|
730
|
-
viewport window; `serverBackingStore` tier on Composite servers, ntk-pixmap + expose-strip
|
|
731
|
-
fallback on XQuartz. Two goal-driven behaviors are part of the design, not polish:
|
|
732
|
-
**(latency)** the ConfigureWindow is written synchronously in the wheel/drag handler, before
|
|
733
|
-
any React or layout work — scroll response never waits on the frame clock or a render;
|
|
734
|
-
**(idle prep)** the overdraw band is prepainted during idle, and re-anchoring runs in idle
|
|
735
|
-
time with the swap prepared off-screen so no seam is ever visible on the input path.
|
|
736
|
-
Depends on items 1, 5–9. Test plan: Xvfb integration tests (the hermetic JS server cannot
|
|
737
|
-
see expose-on-move; XTEST is absent on this XQuartz); screenshot parity vs the drawn path.
|
|
738
|
-
Acceptance: **(latency)** wheel-to-ConfigureWindow-on-the-wire in the same event-loop turn,
|
|
739
|
-
measured with the protocol tracer, vs today's relayout + paced frame; pure-scroll frame
|
|
740
|
-
beats the scroll-blit path on the `protocol.js` bench metrics and on Xvfb frame times; zero
|
|
741
|
-
Expose per notch with the backing-store tier; **(stability)** no visible re-anchor seam, one
|
|
742
|
-
present per frame across viewport+bars (no cross-region tearing), hover changes from
|
|
743
|
-
pane-moves-under-pointer debounced to frame rate; no parity regressions. **Decision gate:**
|
|
744
|
-
if it wins, promote to a scroll pane prop and amend NEXT_STEPS §4; if not, this document's
|
|
745
|
-
§5/§3 records why not, and the scroll-blit chain stands.
|
|
746
|
-
|
|
747
|
-
**Phase 4 — optional, after Phase 3 evidence.** 12. **spike: InputOnly hit regions** (per-region cursors as server attributes, hover-during-
|
|
748
|
-
scroll correctness, remote-X wire silence, implicit-grab drags) — sweep script ready in
|
|
749
|
-
`windowed-regions-bench/bench1.js`, needs Xvfb+XTest. Adopt only if the measured benefits
|
|
750
|
-
show up in a real app scenario. 13. **API decisions to take deliberately, not by drift:** `<surface>` element (explicit opaque
|
|
751
|
-
pane) and ref-XID implicit promotion (Qt's `winId()` insight) stay out of the public API
|
|
752
|
-
until a concrete capability demands them; `<box windowed>` stays out, period.
|
|
753
|
-
|
|
754
|
-
---
|
|
755
|
-
|
|
756
|
-
## 9. Prerequisites in ntk / node-x11 (accumulate into the next ntk cycle)
|
|
757
|
-
|
|
758
|
-
(The same items appear inside §8.1's phased action plan with their react-x11 counterparts; this
|
|
759
|
-
section is the ntk-release shopping list.) Fixes independent of any decision here are marked ✱.
|
|
760
|
-
|
|
761
|
-
1. ✱ Release window XIDs on destroy (one line); optionally wire `xc-misc` GetXIDRange into
|
|
762
|
-
`AllocID` (binding exists, unwired; overflow TODO at `x11/lib/xcore.js:377-389`).
|
|
763
|
-
2. Shared frame clock / fence per top-level for child windows (or a supported
|
|
764
|
-
`frameSync:false, coalesceEvents:false` child mode + parent-side coalescing) — kills the
|
|
765
|
-
1000-fence storm and the cross-window ordering inversion.
|
|
766
|
-
3. InputOnly support: omit `bitGravity`/background attrs when `windowClass === 2`; guard
|
|
767
|
-
`getContext`.
|
|
768
|
-
4. ✱ Consume or remove the dead `backgroundColor` creation attribute (ntk-side; no consumer in
|
|
769
|
-
4.2.0). `backgroundPixel` itself is _already_ forwarded by ntk — the missing half is
|
|
770
|
-
react-x11 passing a real value at region-window creation so new windows don't flash
|
|
771
|
-
undefined content.
|
|
772
|
-
5. `ChangeWindowAttributes` exposure for winGravity/bitGravity post-create (TODO already at
|
|
773
|
-
`window.js:975`) — only if gravity latency-masking is wanted.
|
|
774
|
-
6. Banded/opt-out backing pixmap strategy for large content panes (or document
|
|
775
|
-
`backingStore:false` + expose-strip repaint as the supported mode).
|
|
776
|
-
7. ✱ Deliver ntk's coalesced expose-rect union to the renderer ('draw' with rects), so partial
|
|
777
|
-
restores stop being full-window repaints.
|
|
778
|
-
8. ✱ Ship `scrollRegion` (ntk#139) — the current chain's blit remains the right fallback for
|
|
779
|
-
non-windowed scroll panes.
|
|
780
|
-
9. A way to set the X `backing-store` attribute — ntk deliberately does **not** forward it
|
|
781
|
-
(its own `backingStore` creation option means client-side double buffering, and the comment
|
|
782
|
-
at `window.js:112-121` excludes the X attribute to avoid a silent collision). The
|
|
783
|
-
zero-Expose scroll tier on Composite servers (§5.3, §8 B) needs an explicit spelling, e.g.
|
|
784
|
-
`serverBackingStore: 'whenMapped'`, plus feature detection from the screen's
|
|
785
|
-
`backing-stores` field so it degrades cleanly on XQuartz.
|
|
786
|
-
10. ✱ **Done (ntk#148, 5.2.0).** `Window.frameInFlight()` — whether the last frame's fence is still
|
|
787
|
-
unanswered. Asked for by issue #141 (§0.1, §8.1 item 4b): a discrete input's response is
|
|
788
|
-
painted from its own handler rather than scheduled, and this is the gate that decides
|
|
789
|
-
between painting now and leaving it to the frame clock, so a burst still costs one
|
|
790
|
-
catch-up frame instead of one paint per event.
|
|
791
|
-
|
|
792
|
-
`_onMouseOut` detail/mode filtering is a react-x11-side prerequisite for **everything** above
|
|
793
|
-
(and is arguably a live bug for `<glarea>` + popup-grab edge cases today).
|
|
794
|
-
|
|
795
|
-
---
|
|
796
|
-
|
|
797
|
-
## 10. Measurement appendix
|
|
798
|
-
|
|
799
|
-
Scripts and raw research notes are preserved out-of-tree in the maintainer's local
|
|
800
|
-
`docs-extra/` research area (deliberately uncommitted): `docs-extra/windowed-regions-bench/`
|
|
801
|
-
holds the scripts, `docs-extra/windowed-regions-notes/` the eleven per-agent research reports
|
|
802
|
-
with full source URLs. The in-process-server scripts need no display; the copies reference
|
|
803
|
-
absolute paths from the research session and may need path tweaks to re-run:
|
|
804
|
-
|
|
805
|
-
- `hitbench/hitbench.mjs` — hit-test/motion/flush microbench (in-process server + real renderer)
|
|
806
|
-
- `protobench/protobench.mjs` — protocol-traffic scenarios (repo's `xcount.js` counting)
|
|
807
|
-
- `livebench/bench1.js bench2.js bench3.js` — live XQuartz window storms, RSS, InputOnly (node-x11)
|
|
808
|
-
- `bench.c bench2.c` — XQuartz-vs-Xvfb C benchmarks (storms are batched with a single trailing
|
|
809
|
-
XSync — same methodology as livebench, which is what makes the merged ranges legitimate;
|
|
810
|
-
XSync-per-op only for raise/resize/pick/scroll frames; gravity/backing probes, scroll-by-move)
|
|
811
|
-
- `winmem2.js mapmem.js mapscale.js holdrand.js motion.js` — Xorg heap accounting, map scaling,
|
|
812
|
-
miSpriteTrace pick cost (XTest, Xvfb)
|
|
813
|
-
- x11perf-1.6.1 (built locally) — window-op and Copy/Composite throughput on XQuartz
|
|
814
|
-
|
|
815
|
-
Key environment facts worth re-checking when numbers matter: XQuartz 2.8.6 here is a custom
|
|
816
|
-
build (X.Org 21.1.23, no XTEST, **no Composite**); two Xvfb servers were running during
|
|
817
|
-
measurement; the in-process JS X server gives every window a retained raster (universal backing
|
|
818
|
-
store) and implements neither gravity nor Composite — its subwindow numbers are lower bounds.
|
|
819
|
-
|
|
820
|
-
Follow-up measurements worth running before committing to B:
|
|
821
|
-
|
|
822
|
-
- windowed-scroll pane prototype vs scroll-blit vs full repaint, same content, frame times +
|
|
823
|
-
protocol counts — on XQuartz _and_ on a Composite target (Xvfb or a Linux box/VM with
|
|
824
|
-
Xorg+compositor), including the `backing-store: WhenMapped` zero-Expose tier, which is the
|
|
825
|
-
majority-user path (verified only as an occlude/reveal probe on Xvfb so far, not as a scroll
|
|
826
|
-
loop; XWayland entirely unprobed). Re-save the bench baseline at the ntk floor bump, per the
|
|
827
|
-
PR-chain convention;
|
|
828
|
-
- XQuartz pane-move cost vs pane pixel area (isolate from the grandchild-count effect — the
|
|
829
|
-
4.9 ms row conflates them, and the "drawn-rows pane is µs-scale" expectation rests on it);
|
|
830
|
-
- the un-measured layout axis: time a wheel-notch frame (full-tree relayout + absolutize) at
|
|
831
|
-
N≈1000 with the hitbench harness, and settle whether yoga's wasm `calculateLayout` early-outs
|
|
832
|
-
when the root size is re-set to identical values — it conditions the client-side
|
|
833
|
-
"scroll-as-translation" alternative's payoff (§5.4);
|
|
834
|
-
- Enter/Leave storm cost sweeping a grid of InputOnly regions (needs Xvfb + XTest, script ready
|
|
835
|
-
in `livebench/bench1.js`);
|
|
836
|
-
- hit-test bench re-run after the §8-A caching fixes to confirm the 10–50× expectation.
|
|
837
|
-
|
|
838
|
-
Not covered here, noted for completeness: **Present** (advertised by XQuartz, rootless fidelity
|
|
839
|
-
unknown) is the protocol's frame-transaction tool and the principled answer to §5.2's
|
|
840
|
-
cross-region tearing if regions ever multiply; **XInput2** changes the event-mask story (raw
|
|
841
|
-
motion, touch, smooth scroll) and its crossing semantics were not covered by this pass; the JS
|
|
842
|
-
server implements neither, so both need Xvfb work.
|
|
843
|
-
|
|
844
|
-
## 11. Primary sources
|
|
845
|
-
|
|
846
|
-
X11R7.7 protocol spec (event delivery, crossings, InputOnly, gravity, backing-store, encoding);
|
|
847
|
-
Composite v0.4, Shape v1.1, Render v0.11, XC-MISC specs; xorg-server master sources
|
|
848
|
-
(`windowstr.h`, `mivaltree.c`, `miwindow.c`, `compinit.c`, `dix/window.c`, `dix/dispatch.c`);
|
|
849
|
-
XQuartz rootless README + quartz-wm sources (InputOnly tracking window, SouthEastGravity growbox);
|
|
850
|
-
xwayland-window.c; Larsson's CSW posts + gtk-devel rationale mail; GTK3/GTK4 input & drawing
|
|
851
|
-
docs; Qt QWidget "Native vs Alien" docs + window-embedding blog; O'Reilly Motif ch.3 (gadgets)
|
|
852
|
-
and Java Swing ch.28 (mixing); Chromium GPU-compositing design doc; Keith Packard's smart
|
|
853
|
-
scheduler paper; XQuartz issues #308/#438/#466 and old-ticket 536 (child GLX). Full URL list in
|
|
854
|
-
the out-of-tree research notes (§10).
|