react-x11 2.13.0 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +11 -8
  2. package/src/Reconciler.js +34 -21
  3. package/src/components/Select.js +8 -2
  4. package/src/events.js +8 -2
  5. package/src/glnodes.js +15 -3
  6. package/src/index.d.ts +5 -0
  7. package/src/nodes/boxpaint.js +9 -0
  8. package/src/nodes/preedit.js +64 -14
  9. package/src/ntk.js +10 -1
  10. package/src/scale.js +52 -22
  11. package/src/screencolor.js +104 -17
  12. package/src/types/system.d.ts +14 -1
  13. package/src/wayland/app.js +574 -0
  14. package/src/wayland/backendwindow.js +1199 -0
  15. package/src/wayland/clipboard.js +326 -0
  16. package/src/wayland/connection.js +485 -0
  17. package/src/wayland/context2d.js +2290 -0
  18. package/src/wayland/decorations.js +476 -0
  19. package/src/wayland/device.js +55 -0
  20. package/src/wayland/dmabuf.js +89 -0
  21. package/src/wayland/dnd.js +581 -0
  22. package/src/wayland/fdutil.js +108 -0
  23. package/src/wayland/framestyle.js +257 -0
  24. package/src/wayland/framewatch.js +190 -0
  25. package/src/wayland/glarea.js +372 -0
  26. package/src/wayland/glcontext.js +415 -0
  27. package/src/wayland/glyphatlas.js +237 -0
  28. package/src/wayland/input.js +417 -0
  29. package/src/wayland/keysymnames.js +35 -0
  30. package/src/wayland/layershell.js +363 -0
  31. package/src/wayland/outputs.js +601 -0
  32. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  33. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  34. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  35. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  36. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  37. package/src/wayland/protocols/index.json +131 -0
  38. package/src/wayland/protocols/kde-server-decoration.json +1 -0
  39. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  40. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  41. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  42. package/src/wayland/protocols/presentation-time.json +1 -0
  43. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  44. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  45. package/src/wayland/protocols/tablet-v2.json +1 -0
  46. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  47. package/src/wayland/protocols/viewporter.json +1 -0
  48. package/src/wayland/protocols/wayland.json +1 -0
  49. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  50. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  51. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  52. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  53. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  54. package/src/wayland/protocols/xdg-shell.json +1 -0
  55. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  56. package/src/wayland/readback.js +99 -0
  57. package/src/wayland/screencopy.js +584 -0
  58. package/src/wayland/seat.js +584 -0
  59. package/src/wayland/shm.js +226 -0
  60. package/src/wayland/ssd.js +234 -0
  61. package/src/wayland/surface.js +141 -0
  62. package/src/wayland/swapchain.js +411 -0
  63. package/src/wayland/tablet.js +522 -0
  64. package/src/wayland/target.js +269 -0
  65. package/src/wayland/text.js +113 -0
  66. package/src/wayland/textinput.js +671 -0
  67. package/src/wayland/touch.js +284 -0
  68. package/src/wayland/window.js +854 -0
  69. package/src/wayland/xkb.js +425 -0
  70. package/src/windowstate.js +41 -1
@@ -0,0 +1,411 @@
1
+ // The swap chain behind the Wayland direct rendering context: GPU buffers in,
2
+ // `wl_buffer`s out, and the bookkeeping that keeps the two in step.
3
+ //
4
+ // A frame's life:
5
+ //
6
+ // draw into the backing target (target.js)
7
+ // blit the changed rectangles into the swapchain's back buffer
8
+ // surface.swap() -> dma-buf fd (first time only)
9
+ // -> zwp_linux_dmabuf_v1 -> a wl_buffer for that buffer
10
+ // -> attach + damage_buffer + commit
11
+ // <- wl_buffer.release -> the buffer is ours again
12
+ //
13
+ // This is ntk's `GLSwapchain` with three substitutions — `PixmapFromBuffer`
14
+ // becomes the dmabuf import, `Present.Pixmap` becomes attach/damage/commit,
15
+ // and `PresentIdleNotify` becomes `wl_buffer.release`.
16
+ //
17
+ // The rule that shapes it survives the port unchanged: **a buffer belongs to
18
+ // the compositor until it says otherwise** — drawing into one before then
19
+ // paints whatever is on screen — so a buffer goes back to GBM on
20
+ // `wl_buffer.release` and nowhere else.
21
+ //
22
+ // ## Buffer age, and why painting happens elsewhere
23
+ //
24
+ // An shm client owns one piece of memory and can repaint exactly the rows it
25
+ // touched. A swapchain rotates buffers, so the buffer a frame lands in last
26
+ // showed the frame before last (or earlier), and a partial repaint into it
27
+ // leaves stale pixels everywhere it did not touch. react-x11's renderer is
28
+ // built around partial repaints, so the frame is *not* painted into the
29
+ // swapchain buffer: it is painted into a persistent target and copied across.
30
+ // What has to be copied is everything that changed since *this buffer* was
31
+ // last shown — which is tracked per buffer below — and what has to be told
32
+ // to the compositor is the same set, so it re-composites no more than it
33
+ // must.
34
+ //
35
+ // There is one wrinkle: GBM names the buffer only *after* `swap()`, and the
36
+ // copy has to happen before. So the copy uses `blitHint()`: the union of
37
+ // every known buffer's debt, which is at least what the next one needs; and
38
+ // while the chain is still meeting new buffers — after creation or a resize —
39
+ // everything, since a buffer never seen has no known debt. Steady state is
40
+ // reached once a full rotation has passed without a new buffer, and from then
41
+ // on the copy is the tight union.
42
+ //
43
+ // ## What x11-dri 0.8.0 changed here
44
+ //
45
+ // This file used to carry a `Generation` class that retired the whole GBM
46
+ // surface on every resize and swept it as buffers came back, because there
47
+ // was no in-place resize (node-x11-dri#20). `Surface.resize()` replaced all
48
+ // of it: the surface keeps its identity and its GL objects, only the buffers
49
+ // change, and resizing to the size it already has is free.
50
+ //
51
+ // The same release fixed a hazard this code was exposed to. Buffer `key`s
52
+ // are unique only *within* a generation and GEM handles are recycled, so a
53
+ // cache keyed on `key` alone can hand a stale entry to a live buffer, and a
54
+ // late `release(key)` can free the wrong one. Keys are namespaced by
55
+ // `generation` and releases pass `{ key, generation }`.
56
+
57
+ import { importDmabuf, DmabufRefused } from './dmabuf.js';
58
+
59
+ /** How many frames may be in the compositor's hands at once. */
60
+ const DEFAULT_MAX_IN_FLIGHT = 2;
61
+
62
+ /**
63
+ * Damage sets are kept small: past a handful of rectangles the bookkeeping
64
+ * costs more than the full-surface copy it is trying to avoid, and the
65
+ * compositor clips to the surface anyway.
66
+ */
67
+ const MAX_RECTS = 8;
68
+
69
+ export class WaylandSwapchain {
70
+ /**
71
+ * @param {object} opts
72
+ * @param {object} opts.surface the `wl_surface` presented to
73
+ * @param {object} opts.dmabuf a bound `zwp_linux_dmabuf_v1`
74
+ * @param {object} opts.gpu an x11-dri `Gpu`
75
+ * @param {object} opts.dri the x11-dri module
76
+ * @param {number} opts.format a DRM fourcc, matching the Gpu's
77
+ * @param {object} [opts.policy]
78
+ */
79
+ constructor({ surface, dmabuf, gpu, dri, format, policy = {} }) {
80
+ this.surface = surface;
81
+ this.dmabuf = dmabuf;
82
+ this.gpu = gpu;
83
+ this.dri = dri;
84
+ this.format = format;
85
+ this.policy = {
86
+ maxInFlight: DEFAULT_MAX_IN_FLIGHT,
87
+ linearFallback: true,
88
+ ...policy,
89
+ };
90
+
91
+ /** the one GBM swapchain, resized in place */
92
+ this.gbm = null;
93
+ /** `${generation}:${key}` -> { wlBuffer, busy, damage, key, generation } */
94
+ this.buffers = new Map();
95
+ this.width = 0;
96
+ this.height = 0;
97
+ /** set once the compositor has refused a tiled buffer */
98
+ this.linear = false;
99
+ /** swaps since a buffer was last seen for the first time */
100
+ this._swapsSinceNew = 0;
101
+
102
+ this.inFlight = 0;
103
+ this.importing = false;
104
+ this.starved = false;
105
+ this.destroyed = false;
106
+ this.error = null;
107
+ this.onReady = null;
108
+ this.onError = null;
109
+ }
110
+
111
+ /**
112
+ * Point the chain at a size.
113
+ *
114
+ * @returns the GBM surface to draw into
115
+ */
116
+ surfaceFor(width, height) {
117
+ const w = Math.max(1, Math.round(width));
118
+ const h = Math.max(1, Math.round(height));
119
+ if (!this.gbm) return this._create(w, h);
120
+ if (w === this.width && h === this.height) return this.gbm;
121
+
122
+ // In place: the handle stays valid, the context keeps its GL objects and
123
+ // stays current. Buffers from the old generation are gone, so their
124
+ // `wl_buffer`s go with them — the compositor may still be showing one, so
125
+ // the wl_buffer is destroyed and it will simply never be attached again.
126
+ try {
127
+ this.gbm.resize(w, h);
128
+ } catch (err) {
129
+ this._fail(
130
+ new Error(
131
+ `could not resize the GPU surface to ${w}x${h}: ${err.message}`,
132
+ { cause: err },
133
+ ),
134
+ );
135
+ return this.gbm;
136
+ }
137
+ this._dropBuffers();
138
+ this.width = w;
139
+ this.height = h;
140
+ this.inFlight = 0;
141
+ this._swapsSinceNew = 0;
142
+ return this.gbm;
143
+ }
144
+
145
+ _create(w, h) {
146
+ const use = this.linear
147
+ ? this.dri.GBM_USE.RENDERING | this.dri.GBM_USE.LINEAR
148
+ : undefined;
149
+ try {
150
+ this.gbm =
151
+ use === undefined
152
+ ? this.gpu.createSurface(w, h)
153
+ : this.gpu.createSurface(w, h, use);
154
+ } catch (err) {
155
+ throw new Error(
156
+ `could not create a ${w}x${h} GPU surface: ${err.message}`,
157
+ { cause: err },
158
+ );
159
+ }
160
+ this.width = w;
161
+ this.height = h;
162
+ this._swapsSinceNew = 0;
163
+ return this.gbm;
164
+ }
165
+
166
+ /** Can a frame be drawn and presented right now? */
167
+ canRender() {
168
+ if (this.destroyed || this.error) return false;
169
+ if (this.importing) return false;
170
+ return this.inFlight < this.policy.maxInFlight;
171
+ }
172
+
173
+ /** Whether every buffer of the chain has been seen at least once. */
174
+ get steady() {
175
+ return this.buffers.size > 0 && this._swapsSinceNew >= this.buffers.size;
176
+ }
177
+
178
+ /**
179
+ * What the next buffer might need copied into it, given what this frame
180
+ * changed: `'all'`, or a list of rectangles.
181
+ */
182
+ blitHint(frameDamage) {
183
+ if (!this.steady) return 'all';
184
+ let acc =
185
+ frameDamage === 'all' || !frameDamage
186
+ ? (frameDamage ?? null)
187
+ : [...frameDamage];
188
+ if (acc === 'all') return 'all';
189
+ for (const entry of this.buffers.values()) {
190
+ acc = unionDamage(acc, entry.damage);
191
+ if (acc === 'all') return 'all';
192
+ }
193
+ return acc ?? [];
194
+ }
195
+
196
+ /**
197
+ * Show what was just drawn.
198
+ *
199
+ * Returns false when the frame could not go out. The caller must still
200
+ * commit something — see `WaylandGLContext.endFrame`, which owns that rule.
201
+ */
202
+ async swap(damage) {
203
+ if (this.destroyed || this.error || this.importing) return false;
204
+ if (!this.gbm) return false;
205
+
206
+ let out;
207
+ try {
208
+ out = this.gbm.swap();
209
+ } catch (err) {
210
+ this._fail(
211
+ new Error(`GPU buffer swap failed: ${err.message}`, { cause: err }),
212
+ );
213
+ return false;
214
+ }
215
+ if (!out) {
216
+ // every buffer is still on the compositor's side of the fence
217
+ this.starved = true;
218
+ return false;
219
+ }
220
+ this.starved = false;
221
+
222
+ const generation = this.gbm.generation;
223
+ const id = `${generation}:${out.key}`;
224
+ const known = this.buffers.get(id);
225
+ if (known) {
226
+ this._swapsSinceNew++;
227
+ this._present(known, damage);
228
+ return true;
229
+ }
230
+ this._swapsSinceNew = 0;
231
+ return await this._import(id, out, generation, damage);
232
+ }
233
+
234
+ /**
235
+ * A buffer the compositor has not seen: hand over its descriptor, and only
236
+ * attach once it has taken it.
237
+ */
238
+ async _import(id, out, generation, damage) {
239
+ this.importing = true;
240
+ let wlBuffer;
241
+ try {
242
+ wlBuffer = await importDmabuf(this.dmabuf, out, this.format);
243
+ } catch (err) {
244
+ this.importing = false;
245
+ if (this.destroyed) return false;
246
+ return await this._importFailed(err);
247
+ }
248
+ this.importing = false;
249
+ if (this.destroyed) {
250
+ wlBuffer.$.destroy?.();
251
+ return false;
252
+ }
253
+
254
+ // A resize while the import was in flight retires this buffer before it
255
+ // was ever shown. Its GBM buffer is already gone with the old generation;
256
+ // only the wl_buffer needs freeing.
257
+ if (this.gbm.generation !== generation) {
258
+ wlBuffer.$.destroy?.();
259
+ this.onReady?.();
260
+ return false;
261
+ }
262
+
263
+ const entry = {
264
+ wlBuffer,
265
+ busy: false,
266
+ damage: null,
267
+ key: out.key,
268
+ generation,
269
+ };
270
+ wlBuffer.on('release', () => this._released(id, entry));
271
+ this.buffers.set(id, entry);
272
+ this._present(entry, damage);
273
+ this.onReady?.();
274
+ return true;
275
+ }
276
+
277
+ /**
278
+ * The compositor would not take the buffer. Almost always this means client
279
+ * and compositor are on different DRM devices; a linear buffer fixes it, at
280
+ * the cost of some bandwidth. Worth one retry before giving up.
281
+ */
282
+ async _importFailed(err) {
283
+ if (
284
+ err instanceof DmabufRefused &&
285
+ this.policy.linearFallback &&
286
+ !this.linear
287
+ ) {
288
+ const { width, height } = this;
289
+ this.linear = true;
290
+ this._dropBuffers();
291
+ try {
292
+ this.gbm.destroy();
293
+ } catch {
294
+ /* already gone */
295
+ }
296
+ this.gbm = null;
297
+ try {
298
+ this._create(width, height);
299
+ } catch (retryErr) {
300
+ this._fail(retryErr);
301
+ return false;
302
+ }
303
+ this.onReady?.();
304
+ return false;
305
+ }
306
+ this._fail(
307
+ new Error(
308
+ `${err.message}. Client and compositor are probably on different DRM devices; ` +
309
+ "name the compositor's device with glPolicy: { devicePath: '/dev/dri/renderD###' } or REACT_X11_GL_DEVICE.",
310
+ { cause: err },
311
+ ),
312
+ );
313
+ return false;
314
+ }
315
+
316
+ /**
317
+ * Attach, damage and commit — synchronously, through the library's `$`
318
+ * path: three writes, no promises, and nothing for the frame to wait on.
319
+ *
320
+ * Damage is accumulated per buffer: what this buffer must repaint is
321
+ * everything that changed since it was last on screen, which is the union
322
+ * of every frame's damage in between.
323
+ */
324
+ _present(entry, damage) {
325
+ for (const other of this.buffers.values()) {
326
+ if (other === entry) continue;
327
+ other.damage = unionDamage(other.damage, damage);
328
+ }
329
+ const owed = unionDamage(entry.damage, damage);
330
+ entry.damage = null;
331
+
332
+ entry.busy = true;
333
+ this.inFlight++;
334
+
335
+ const s = this.surface.$;
336
+ s.attach(entry.wlBuffer.id, 0, 0);
337
+ if (owed === 'all' || !owed) {
338
+ // `damage_buffer` takes buffer coordinates, which is what a GL frame is
339
+ // drawn in — the older `damage` request is in surface coordinates and
340
+ // would need the scale factor applied.
341
+ s.damage_buffer(0, 0, this.width, this.height);
342
+ } else {
343
+ for (const r of owed)
344
+ s.damage_buffer(
345
+ r.x | 0,
346
+ r.y | 0,
347
+ Math.ceil(r.width),
348
+ Math.ceil(r.height),
349
+ );
350
+ }
351
+ // The caller commits: a frame request and the configure ack ride the
352
+ // same commit, and the caller owns those.
353
+ }
354
+
355
+ _released(id, entry) {
356
+ if (!entry.busy) return;
357
+ entry.busy = false;
358
+ this.inFlight = Math.max(0, this.inFlight - 1);
359
+ // Pass the reference, not the key: keys are unique only within a
360
+ // generation and GEM handles are recycled, so a bare key arriving late
361
+ // would free whichever live buffer inherited that handle. `release`
362
+ // answers false for a buffer whose generation is gone, which is the
363
+ // normal outcome after a resize and not an error.
364
+ try {
365
+ this.gbm?.release({ key: entry.key, generation: entry.generation });
366
+ } catch {
367
+ /* the surface may already be gone */
368
+ }
369
+ if (this.starved || this.inFlight < this.policy.maxInFlight)
370
+ this.onReady?.();
371
+ }
372
+
373
+ /** Forget every wl_buffer — after a resize, or on the way out. */
374
+ _dropBuffers() {
375
+ for (const entry of this.buffers.values()) {
376
+ try {
377
+ entry.wlBuffer.$.destroy?.();
378
+ } catch {
379
+ /* connection gone */
380
+ }
381
+ }
382
+ this.buffers.clear();
383
+ }
384
+
385
+ _fail(err) {
386
+ if (this.error) return;
387
+ this.error = err;
388
+ this.onError?.(err);
389
+ }
390
+
391
+ destroy() {
392
+ if (this.destroyed) return;
393
+ this.destroyed = true;
394
+ this._dropBuffers();
395
+ try {
396
+ this.gbm?.destroy();
397
+ } catch {
398
+ /* already gone */
399
+ }
400
+ this.gbm = null;
401
+ }
402
+ }
403
+
404
+ /** Merge damage, where `'all'` means the whole buffer and `null` means none. */
405
+ export function unionDamage(a, b) {
406
+ if (a === 'all' || b === 'all') return 'all';
407
+ if (!a) return b ? [...b] : null;
408
+ if (!b) return a;
409
+ const merged = [...a, ...b];
410
+ return merged.length > MAX_RECTS ? 'all' : merged;
411
+ }