react-x11 2.13.0 → 2.14.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/package.json +11 -8
- package/src/Reconciler.js +34 -21
- package/src/components/Select.js +8 -2
- package/src/events.js +8 -2
- package/src/index.d.ts +5 -0
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/wayland/app.js +560 -0
- package/src/wayland/backendwindow.js +1123 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +482 -0
- package/src/wayland/context2d.js +2133 -0
- package/src/wayland/decorations.js +476 -0
- package/src/wayland/dmabuf.js +89 -0
- package/src/wayland/dnd.js +581 -0
- package/src/wayland/fdutil.js +108 -0
- package/src/wayland/framestyle.js +257 -0
- package/src/wayland/glarea.js +371 -0
- package/src/wayland/glcontext.js +415 -0
- package/src/wayland/glyphatlas.js +237 -0
- package/src/wayland/input.js +417 -0
- package/src/wayland/keysymnames.js +35 -0
- package/src/wayland/layershell.js +363 -0
- package/src/wayland/outputs.js +601 -0
- package/src/wayland/protocols/cursor-shape-v1.json +1 -0
- package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
- package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
- package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
- package/src/wayland/protocols/fractional-scale-v1.json +1 -0
- package/src/wayland/protocols/index.json +127 -0
- package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
- package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
- package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
- package/src/wayland/protocols/presentation-time.json +1 -0
- package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
- package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
- package/src/wayland/protocols/tablet-v2.json +1 -0
- package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
- package/src/wayland/protocols/viewporter.json +1 -0
- package/src/wayland/protocols/wayland.json +1 -0
- package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
- package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-activation-v1.json +1 -0
- package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-shell.json +1 -0
- package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
- package/src/wayland/readback.js +99 -0
- package/src/wayland/screencopy.js +584 -0
- package/src/wayland/seat.js +584 -0
- package/src/wayland/shm.js +226 -0
- package/src/wayland/ssd.js +106 -0
- package/src/wayland/surface.js +123 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +263 -0
- package/src/wayland/text.js +113 -0
- package/src/wayland/textinput.js +671 -0
- package/src/wayland/touch.js +284 -0
- package/src/wayland/window.js +827 -0
- package/src/wayland/xkb.js +425 -0
|
@@ -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
|
+
}
|