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,415 @@
|
|
|
1
|
+
// Tier D: a GLES rendering context whose frames reach the compositor as
|
|
2
|
+
// dma-buf, with no pixels on the socket and no CPU rasterisation anywhere.
|
|
3
|
+
//
|
|
4
|
+
// The addon this leans on is misnamed by history. `x11-dri`'s `Gpu` is GBM +
|
|
5
|
+
// EGL + GLES on a DRM render node and never touches X — the X part of the
|
|
6
|
+
// package is the *presentation* half, DRI3 and Present, which is exactly the
|
|
7
|
+
// half Wayland replaces. So the accelerated backend costs a swapchain and
|
|
8
|
+
// nothing else; the GL context, the device selection and the buffer export
|
|
9
|
+
// are already there and already shared with the X11 backend.
|
|
10
|
+
//
|
|
11
|
+
// A frame here has three parts, and the middle one is the renderer's:
|
|
12
|
+
//
|
|
13
|
+
// beginFrame() bind the backing target at the window's buffer size
|
|
14
|
+
// …the 2d context paints into it, partially, as it would into a pixmap…
|
|
15
|
+
// endFrame() copy what changed into the swapchain buffer, swap, and
|
|
16
|
+
// commit — with the configure ack and the frame request in
|
|
17
|
+
// that same commit
|
|
18
|
+
//
|
|
19
|
+
// The backing target (target.js) is what makes partial repaints correct on a
|
|
20
|
+
// rotating swapchain; see swapchain.js for why the copy is sized the way it
|
|
21
|
+
// is. It also means the window's EGL config needs no stencil bits of its own
|
|
22
|
+
// — the target carries them — though they are still requested, cheaply, for
|
|
23
|
+
// anything that draws straight at the swapchain (a `<glarea>`).
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { WaylandSwapchain } from './swapchain.js';
|
|
28
|
+
import { GLTarget } from './target.js';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One GPU context per pixel format, not one per surface.
|
|
34
|
+
*
|
|
35
|
+
* An EGL context is expensive — a device, a GBM device, a display, a config
|
|
36
|
+
* scan — and every surface in an app wants the same one; sharing it also
|
|
37
|
+
* means textures and programs are shared between surfaces, as they are
|
|
38
|
+
* between canvases in a browser tab. The cost is that exactly one surface is
|
|
39
|
+
* current at a time, which `beginFrame` takes care of.
|
|
40
|
+
*
|
|
41
|
+
* Kept per x11-dri module, which is one object in practice and a fresh fake
|
|
42
|
+
* in each test.
|
|
43
|
+
*/
|
|
44
|
+
const gpuCaches = new WeakMap();
|
|
45
|
+
|
|
46
|
+
function cacheFor(dri) {
|
|
47
|
+
let cache = gpuCaches.get(dri);
|
|
48
|
+
if (!cache) gpuCaches.set(dri, (cache = new Map()));
|
|
49
|
+
return cache;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `/dev/dri/card*`, in numeric order. */
|
|
53
|
+
export function listCardNodes(readdir = fs.readdirSync) {
|
|
54
|
+
let names;
|
|
55
|
+
try {
|
|
56
|
+
names = readdir('/dev/dri');
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return names
|
|
61
|
+
.map((name) => /^card(\d+)$/.exec(name))
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.sort((a, b) => Number(a[1]) - Number(b[1]))
|
|
64
|
+
.map((m) => `/dev/dri/${m[0]}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The DRM nodes to try for the shared context, in order; `undefined` is
|
|
69
|
+
* x11-dri's own choice, the first render node.
|
|
70
|
+
*
|
|
71
|
+
* A named device is the only one tried: asking for a device and getting
|
|
72
|
+
* another would be a worse surprise than the error. Otherwise the render
|
|
73
|
+
* node, as ever. A machine with no render node at all — a GitHub-hosted
|
|
74
|
+
* runner, a Hyper-V or VirtualBox guest — can still have a card node Mesa
|
|
75
|
+
* renders on in software, through kms_swrast, so those are tried in turn.
|
|
76
|
+
* With neither, x11-dri's default once more, so its own reason is the one
|
|
77
|
+
* reported.
|
|
78
|
+
*/
|
|
79
|
+
export function gpuCandidates({ requested, renderNodes, cardNodes }) {
|
|
80
|
+
if (requested) return [requested];
|
|
81
|
+
if (renderNodes.length) return [undefined];
|
|
82
|
+
return cardNodes.length ? cardNodes : [undefined];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function requireDri() {
|
|
86
|
+
try {
|
|
87
|
+
return require('x11-dri');
|
|
88
|
+
} catch (err) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'the accelerated backend needs the optional x11-dri addon, which is not installed',
|
|
91
|
+
{ cause: err },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The GPU context for a pixel format, made once and shared.
|
|
98
|
+
*
|
|
99
|
+
* The device is `devicePath` (`glPolicy.devicePath`), else
|
|
100
|
+
* `REACT_X11_GL_DEVICE`, else whatever {@link gpuCandidates} finds. The
|
|
101
|
+
* cache is keyed by what was asked for rather than what was found, so the
|
|
102
|
+
* app's context and every window's resolve to the same one.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [seams] for the tests: the environment, and the card nodes
|
|
105
|
+
*/
|
|
106
|
+
export function sharedGpu(
|
|
107
|
+
dri,
|
|
108
|
+
{ format, depthSize, stencilSize, devicePath },
|
|
109
|
+
{ env = process.env, cardNodes = listCardNodes } = {},
|
|
110
|
+
) {
|
|
111
|
+
const requested = devicePath || env.REACT_X11_GL_DEVICE || null;
|
|
112
|
+
const cache = cacheFor(dri);
|
|
113
|
+
const key = `${format}|${depthSize}|${stencilSize}|${requested ?? ''}`;
|
|
114
|
+
const existing = cache.get(key);
|
|
115
|
+
if (existing) return existing;
|
|
116
|
+
const candidates = gpuCandidates({
|
|
117
|
+
requested,
|
|
118
|
+
renderNodes: requested ? [] : dri.listRenderNodes(),
|
|
119
|
+
cardNodes: requested ? [] : cardNodes(),
|
|
120
|
+
});
|
|
121
|
+
const failures = [];
|
|
122
|
+
for (const path of candidates) {
|
|
123
|
+
try {
|
|
124
|
+
const gpu = new dri.Gpu({
|
|
125
|
+
format,
|
|
126
|
+
depthSize,
|
|
127
|
+
stencilSize,
|
|
128
|
+
...(path ? { devicePath: path } : {}),
|
|
129
|
+
});
|
|
130
|
+
cache.set(key, gpu);
|
|
131
|
+
return gpu;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
failures.push({ path, err });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const tried = failures
|
|
137
|
+
.map(
|
|
138
|
+
({ path, err }) => `${path ?? 'the default render node'}: ${err.message}`,
|
|
139
|
+
)
|
|
140
|
+
.join('; ');
|
|
141
|
+
throw new Error(`could not create a GPU context on ${tried}`, {
|
|
142
|
+
cause: failures.at(-1).err,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class WaylandGLContext {
|
|
147
|
+
/**
|
|
148
|
+
* Build the context with nothing to await.
|
|
149
|
+
*
|
|
150
|
+
* Only `zwp_linux_dmabuf_v1` had to be bound asynchronously, and an app
|
|
151
|
+
* binds it once at startup; the GPU context itself never needed the
|
|
152
|
+
* compositor. React's commit phase cannot await, and that is where windows
|
|
153
|
+
* are made.
|
|
154
|
+
*/
|
|
155
|
+
static createSync({ conn, window, dmabuf, policy = {} }) {
|
|
156
|
+
const dri = requireDri();
|
|
157
|
+
const probe = dri.probe();
|
|
158
|
+
if (probe.gbm !== true || probe.egl !== true || probe.gles !== true) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`this machine cannot render on the GPU: gbm=${probe.gbm}, egl=${probe.egl}, gles=${probe.gles}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (!dmabuf) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
'this compositor does not advertise zwp_linux_dmabuf_v1, so GPU buffers cannot be handed to it',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
// Alpha by default: client-side decorations want rounded corners, and a
|
|
169
|
+
// fully opaque window says so with `set_opaque_region` instead, which is
|
|
170
|
+
// the cheaper signal anyway.
|
|
171
|
+
const alpha = policy.alpha ?? true;
|
|
172
|
+
const format = alpha ? dri.FORMAT.ARGB8888 : dri.FORMAT.XRGB8888;
|
|
173
|
+
const gpu = sharedGpu(dri, {
|
|
174
|
+
format,
|
|
175
|
+
depthSize: policy.depthSize ?? 16,
|
|
176
|
+
stencilSize: policy.stencilSize ?? 8,
|
|
177
|
+
devicePath: policy.devicePath,
|
|
178
|
+
});
|
|
179
|
+
const chain = new WaylandSwapchain({
|
|
180
|
+
surface: window.surface,
|
|
181
|
+
dmabuf,
|
|
182
|
+
gpu,
|
|
183
|
+
dri,
|
|
184
|
+
format,
|
|
185
|
+
policy,
|
|
186
|
+
});
|
|
187
|
+
return new WaylandGLContext({
|
|
188
|
+
conn,
|
|
189
|
+
window,
|
|
190
|
+
dri,
|
|
191
|
+
gpu,
|
|
192
|
+
chain,
|
|
193
|
+
format,
|
|
194
|
+
policy,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The asynchronous form, for scripts that are not inside a React commit. */
|
|
199
|
+
static async create({ conn, window, policy = {} }) {
|
|
200
|
+
const dmabuf = await conn.bind('zwp_linux_dmabuf_v1');
|
|
201
|
+
return WaylandGLContext.createSync({ conn, window, dmabuf, policy });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
constructor({ conn, window, dri, gpu, chain, format, policy = {} }) {
|
|
205
|
+
this.conn = conn;
|
|
206
|
+
this.window = window;
|
|
207
|
+
this.dri = dri;
|
|
208
|
+
this.gpu = gpu;
|
|
209
|
+
this.chain = chain;
|
|
210
|
+
this.format = format;
|
|
211
|
+
this.policy = policy;
|
|
212
|
+
/** The GL entry points — WebGL-shaped and camelCase. */
|
|
213
|
+
this.gl = gpu.gl;
|
|
214
|
+
this.destroyed = false;
|
|
215
|
+
/** the persistent pixels the renderer paints into */
|
|
216
|
+
this.backing = null;
|
|
217
|
+
this._surface = null;
|
|
218
|
+
this._inFrame = false;
|
|
219
|
+
this.lastFramePresented = false;
|
|
220
|
+
|
|
221
|
+
chain.onError = (err) => {
|
|
222
|
+
this.destroyed = true;
|
|
223
|
+
window.emit('error', err);
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** What the driver actually gave us, which can exceed what was asked for. */
|
|
228
|
+
get glVersion() {
|
|
229
|
+
return this.gpu.glVersion;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get features() {
|
|
233
|
+
return this.gpu.features;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get width() {
|
|
237
|
+
return this.backing?.width ?? 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
get height() {
|
|
241
|
+
return this.backing?.height ?? 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Make the context current, size the swapchain and the backing target to
|
|
246
|
+
* the window's buffer size, and bind the target for drawing.
|
|
247
|
+
*
|
|
248
|
+
* The size is taken from the window rather than passed in because the
|
|
249
|
+
* compositor owns it: a configure may have landed between two frames, and
|
|
250
|
+
* drawing at the old size would produce a buffer the compositor stretches.
|
|
251
|
+
*
|
|
252
|
+
* @returns {{width:number, height:number, resized:boolean}|null}
|
|
253
|
+
*/
|
|
254
|
+
beginFrame() {
|
|
255
|
+
if (this.destroyed) return null;
|
|
256
|
+
const w = this.window.bufferWidth;
|
|
257
|
+
const h = this.window.bufferHeight;
|
|
258
|
+
const surface = this.chain.surfaceFor(w, h);
|
|
259
|
+
this._surface = surface;
|
|
260
|
+
this.gpu.makeCurrent(surface);
|
|
261
|
+
let resized = false;
|
|
262
|
+
if (!this.backing) {
|
|
263
|
+
this.backing = new GLTarget(this.gl, {
|
|
264
|
+
width: w,
|
|
265
|
+
height: h,
|
|
266
|
+
stencil: true,
|
|
267
|
+
});
|
|
268
|
+
resized = true;
|
|
269
|
+
} else if (this.backing.width !== w || this.backing.height !== h) {
|
|
270
|
+
// Contents are dropped: a resize relayouts and repaints everything
|
|
271
|
+
// upstairs (listeners.js: `invalidate(true, null, 'resize')`).
|
|
272
|
+
this.backing.resize(w, h);
|
|
273
|
+
resized = true;
|
|
274
|
+
}
|
|
275
|
+
this.backing.bind();
|
|
276
|
+
this._inFrame = true;
|
|
277
|
+
return { width: w, height: h, resized };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Route GL draws at the backing target again — for a `<glarea>` that bound
|
|
282
|
+
* its own framebuffer in between, or anything else that touched the
|
|
283
|
+
* binding.
|
|
284
|
+
*/
|
|
285
|
+
bindBacking() {
|
|
286
|
+
this.backing?.bind();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Finish the frame, hand it over, and hand back the frame clock.
|
|
291
|
+
*
|
|
292
|
+
* Pacing is owned here rather than by the caller because the two have to be
|
|
293
|
+
* atomic and the failure is silent if they are not. A `wl_surface.frame`
|
|
294
|
+
* request is only *delivered* by a commit, so a loop that asks for the
|
|
295
|
+
* callback and then discovers it has nothing to present — every buffer
|
|
296
|
+
* still held by the compositor, which is a normal thing to happen — waits
|
|
297
|
+
* forever on a callback the compositor was never told to schedule. That
|
|
298
|
+
* deadlock is what this method exists to make unrepresentable: the request
|
|
299
|
+
* goes out with the commit, and when there is no frame to show, an empty
|
|
300
|
+
* commit delivers it anyway so the clock keeps ticking and the next frame
|
|
301
|
+
* gets its turn.
|
|
302
|
+
*
|
|
303
|
+
* @param {Array<{x,y,width,height}>|'all'} [damage] what the frame changed,
|
|
304
|
+
* in buffer coordinates. Defaults to everything.
|
|
305
|
+
* @returns {Promise<number>} the frame callback's timestamp, once the
|
|
306
|
+
* compositor says it is time to draw again. An async function cannot
|
|
307
|
+
* hand back a promise unsettled — returning `vsync` makes the caller's
|
|
308
|
+
* `await` wait for it — which is what keeps one present in flight in
|
|
309
|
+
* `_present`, and why a connection closing mid-frame rejects there.
|
|
310
|
+
*/
|
|
311
|
+
async endFrame(damage = 'all') {
|
|
312
|
+
// Always a promise for the frame clock, even when there is no frame:
|
|
313
|
+
// callers chain off it unconditionally.
|
|
314
|
+
if (this.destroyed || !this._inFrame)
|
|
315
|
+
return Promise.resolve(Promise.resolve(0));
|
|
316
|
+
this._inFrame = false;
|
|
317
|
+
const gl = this.gl;
|
|
318
|
+
const win = this.window;
|
|
319
|
+
|
|
320
|
+
// The copy: backing -> the swapchain's back buffer, sized by what the
|
|
321
|
+
// next buffer might be missing. Then the GPU work is settled (optionally
|
|
322
|
+
// through a fence rather than a stall) before the swap hands it over.
|
|
323
|
+
const rects = this.chain.blitHint(damage);
|
|
324
|
+
this.backing.blitTo(
|
|
325
|
+
null,
|
|
326
|
+
rects === 'all' ? null : rects,
|
|
327
|
+
this.chain.width,
|
|
328
|
+
this.chain.height,
|
|
329
|
+
);
|
|
330
|
+
await this._settleGpu();
|
|
331
|
+
|
|
332
|
+
// Queued before anything that commits, so one commit carries the frame
|
|
333
|
+
// request, the configure ack, and the buffer.
|
|
334
|
+
const vsync = win.scheduleFrame();
|
|
335
|
+
win.ackPending();
|
|
336
|
+
// Another window's frame may have run in the await above and made its
|
|
337
|
+
// own surface current. eglSwapBuffers on a surface that is not current
|
|
338
|
+
// is EGL_BAD_SURFACE (0x300d): the second window of examples/windows.jsx.
|
|
339
|
+
this.gpu.makeCurrent(this._surface);
|
|
340
|
+
const presented = await this.chain.swap(damage);
|
|
341
|
+
// `swap` attached and damaged but did not commit; and when nothing went
|
|
342
|
+
// out — every buffer busy — the frame request still needs a commit to be
|
|
343
|
+
// delivered. Either way, exactly one commit.
|
|
344
|
+
if (!this.destroyed) win.surface.$.commit();
|
|
345
|
+
if (presented) win.mapped = true;
|
|
346
|
+
this.lastFramePresented = presented;
|
|
347
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
348
|
+
return vsync;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Wait for the GPU to finish this frame *without* blocking the thread.
|
|
353
|
+
*
|
|
354
|
+
* `eglSwapBuffers` inside `Surface.swap()` waits for the frame's commands
|
|
355
|
+
* to complete, and that wait is a hard stall of the JS thread — measured at
|
|
356
|
+
* ~10ms per frame on this machine's virtio-gpu, and size-independent, so it
|
|
357
|
+
* is round-trip latency to the host rather than fill rate. A stalled thread
|
|
358
|
+
* is a thread not reading input or protocol events.
|
|
359
|
+
*
|
|
360
|
+
* A fence turns that stall into something the event loop can interleave
|
|
361
|
+
* with: signal after the draw commands, then poll with a zero timeout,
|
|
362
|
+
* yielding between polls so input and Wayland events get their turn. The
|
|
363
|
+
* work takes just as long — this trades a blocked thread for a busy one
|
|
364
|
+
* (measured: −0.9% frame rate) — so it is off by default and
|
|
365
|
+
* `glPolicy: { fence: true }` turns it on where responsiveness matters more
|
|
366
|
+
* than a core.
|
|
367
|
+
*/
|
|
368
|
+
async _settleGpu() {
|
|
369
|
+
if (!this.policy?.fence) return;
|
|
370
|
+
const gl = this.gl;
|
|
371
|
+
if (!this.gpu.features?.sync) return;
|
|
372
|
+
let sync;
|
|
373
|
+
try {
|
|
374
|
+
sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
375
|
+
} catch {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!sync) return;
|
|
379
|
+
gl.flush();
|
|
380
|
+
try {
|
|
381
|
+
for (let spins = 0; spins < 10000; spins++) {
|
|
382
|
+
const r = gl.clientWaitSync(sync, 0, 0);
|
|
383
|
+
if (r === gl.ALREADY_SIGNALED || r === gl.CONDITION_SATISFIED) return;
|
|
384
|
+
if (r !== gl.TIMEOUT_EXPIRED) return;
|
|
385
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
386
|
+
}
|
|
387
|
+
} finally {
|
|
388
|
+
gl.deleteSync(sync);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
canRender() {
|
|
393
|
+
return this.chain.canRender();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
set onReady(fn) {
|
|
397
|
+
this.chain.onReady = fn;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
destroy() {
|
|
401
|
+
if (this.destroyed) return;
|
|
402
|
+
this.destroyed = true;
|
|
403
|
+
if (this._surface) {
|
|
404
|
+
try {
|
|
405
|
+
this.gpu.makeCurrent(this._surface);
|
|
406
|
+
this.backing?.destroy();
|
|
407
|
+
} catch {
|
|
408
|
+
/* context already gone */
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.backing = null;
|
|
412
|
+
this.chain.destroy();
|
|
413
|
+
// The Gpu is shared; it outlives any one context.
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// The client-side glyph cache — the thing XRender's `GlyphSet` used to be.
|
|
2
|
+
//
|
|
3
|
+
// On X11 a glyph is uploaded once into a server-resident glyph set and then
|
|
4
|
+
// named by id in a `CompositeGlyphs` request; the server owns the pixels and
|
|
5
|
+
// the client never sees them again. There is no such thing here. A Wayland
|
|
6
|
+
// client rasterises its own glyphs and composites them itself, so the cache
|
|
7
|
+
// has to live on this side of the socket — and for Tier D specifically, in
|
|
8
|
+
// GPU memory, as a texture that quads sample from.
|
|
9
|
+
//
|
|
10
|
+
// The rasteriser is ntk's own (`Font.rasterize` -> A8 coverage), which is the
|
|
11
|
+
// part of the text stack most worth keeping: shaping, bidi and line breaking
|
|
12
|
+
// stay exactly as they are on X11, and only the final composite changes.
|
|
13
|
+
//
|
|
14
|
+
// Packing is a shelf: glyphs arrive in no useful order and are never freed
|
|
15
|
+
// individually, so the classic row-by-row placement wastes little and costs
|
|
16
|
+
// nothing to maintain. When a shelf will not take the glyph the atlas grows
|
|
17
|
+
// by doubling, which invalidates every entry — the context is told first, so
|
|
18
|
+
// quads already buffered against the old layout are drawn before it changes
|
|
19
|
+
// under them (see `onBeforeGrow`).
|
|
20
|
+
//
|
|
21
|
+
// Uploads are deferred and partial: new glyphs land in a CPU copy and the
|
|
22
|
+
// dirty rectangle goes up with one `texSubImage2D` when the texture is next
|
|
23
|
+
// bound. The first version of this file re-uploaded the whole atlas per new
|
|
24
|
+
// glyph, which at 4096² is 16MB per character of a paragraph's first paint.
|
|
25
|
+
|
|
26
|
+
const INITIAL_SIZE = 512;
|
|
27
|
+
const MAX_SIZE = 4096;
|
|
28
|
+
/** Keep glyphs from bleeding into each other under linear filtering. */
|
|
29
|
+
const PAD = 1;
|
|
30
|
+
|
|
31
|
+
export class GlyphAtlas {
|
|
32
|
+
constructor(gl, { size = INITIAL_SIZE } = {}) {
|
|
33
|
+
this.gl = gl;
|
|
34
|
+
this.size = size;
|
|
35
|
+
/** key -> { x, y, w, h, left, top, u0, v0, u1, v1 } */
|
|
36
|
+
this.entries = new Map();
|
|
37
|
+
this._shelfY = PAD;
|
|
38
|
+
this._shelfH = 0;
|
|
39
|
+
this._shelfX = PAD;
|
|
40
|
+
this._pixels = new Uint8Array(size * size);
|
|
41
|
+
this.texture = null;
|
|
42
|
+
/** dirty rectangle of the CPU copy not yet uploaded, or null */
|
|
43
|
+
this._dirty = null;
|
|
44
|
+
this._fresh = true; // the GPU texture needs a full upload
|
|
45
|
+
this.generation = 0;
|
|
46
|
+
/** called before the atlas grows, so pending quads can be flushed */
|
|
47
|
+
this.onBeforeGrow = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find or make room for one rasterised glyph.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} key identifies the glyph *and* the size it was rasterised
|
|
54
|
+
* at — a 12px 'a' and a 24px 'a' are different pixels and share nothing.
|
|
55
|
+
* @param {() => {width,height,left,top,data}} raster called only on a miss,
|
|
56
|
+
* so a hit never touches the font machinery
|
|
57
|
+
*/
|
|
58
|
+
get(key, raster) {
|
|
59
|
+
const hit = this.entries.get(key);
|
|
60
|
+
if (hit) return hit;
|
|
61
|
+
|
|
62
|
+
const g = raster();
|
|
63
|
+
// A space has no ink. Cache the absence so the miss is paid once.
|
|
64
|
+
if (!g || g.width <= 0 || g.height <= 0) {
|
|
65
|
+
const empty = { empty: true, left: g?.left ?? 0, top: g?.top ?? 0 };
|
|
66
|
+
this.entries.set(key, empty);
|
|
67
|
+
return empty;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const spot = this._place(g.width, g.height);
|
|
71
|
+
if (!spot) return { empty: true, left: g.left, top: g.top };
|
|
72
|
+
|
|
73
|
+
for (let row = 0; row < g.height; row++) {
|
|
74
|
+
const src = row * g.width;
|
|
75
|
+
const dst = (spot.y + row) * this.size + spot.x;
|
|
76
|
+
this._pixels.set(g.data.subarray(src, src + g.width), dst);
|
|
77
|
+
}
|
|
78
|
+
this._markDirty(spot.x, spot.y, g.width, g.height);
|
|
79
|
+
|
|
80
|
+
const inv = 1 / this.size;
|
|
81
|
+
const entry = {
|
|
82
|
+
empty: false,
|
|
83
|
+
x: spot.x,
|
|
84
|
+
y: spot.y,
|
|
85
|
+
w: g.width,
|
|
86
|
+
h: g.height,
|
|
87
|
+
left: g.left,
|
|
88
|
+
top: g.top,
|
|
89
|
+
u0: spot.x * inv,
|
|
90
|
+
v0: spot.y * inv,
|
|
91
|
+
u1: (spot.x + g.width) * inv,
|
|
92
|
+
v1: (spot.y + g.height) * inv,
|
|
93
|
+
};
|
|
94
|
+
this.entries.set(key, entry);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_markDirty(x, y, w, h) {
|
|
99
|
+
const d = this._dirty;
|
|
100
|
+
if (!d) {
|
|
101
|
+
this._dirty = { x0: x, y0: y, x1: x + w, y1: y + h };
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
d.x0 = Math.min(d.x0, x);
|
|
105
|
+
d.y0 = Math.min(d.y0, y);
|
|
106
|
+
d.x1 = Math.max(d.x1, x + w);
|
|
107
|
+
d.y1 = Math.max(d.y1, y + h);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_place(w, h) {
|
|
111
|
+
if (w + PAD * 2 > this.size || h + PAD * 2 > this.size) {
|
|
112
|
+
return this._grow() ? this._place(w, h) : null;
|
|
113
|
+
}
|
|
114
|
+
if (this._shelfX + w + PAD > this.size) {
|
|
115
|
+
this._shelfY += this._shelfH + PAD;
|
|
116
|
+
this._shelfH = 0;
|
|
117
|
+
this._shelfX = PAD;
|
|
118
|
+
}
|
|
119
|
+
if (this._shelfY + h + PAD > this.size) {
|
|
120
|
+
return this._grow() ? this._place(w, h) : null;
|
|
121
|
+
}
|
|
122
|
+
const spot = { x: this._shelfX, y: this._shelfY };
|
|
123
|
+
this._shelfX += w + PAD;
|
|
124
|
+
if (h > this._shelfH) this._shelfH = h;
|
|
125
|
+
return spot;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Double the atlas and start over.
|
|
130
|
+
*
|
|
131
|
+
* Entries are dropped rather than copied: they only remember where their
|
|
132
|
+
* pixels went, and a repack at this frequency is not worth keeping a second
|
|
133
|
+
* copy of every glyph. Anyone holding an entry across this call has a
|
|
134
|
+
* stale one, which is why `onBeforeGrow` exists.
|
|
135
|
+
*/
|
|
136
|
+
_grow() {
|
|
137
|
+
if (this.size >= MAX_SIZE) return false;
|
|
138
|
+
this.onBeforeGrow?.();
|
|
139
|
+
this.size *= 2;
|
|
140
|
+
this._pixels = new Uint8Array(this.size * this.size);
|
|
141
|
+
this.entries.clear();
|
|
142
|
+
this._shelfX = PAD;
|
|
143
|
+
this._shelfY = PAD;
|
|
144
|
+
this._shelfH = 0;
|
|
145
|
+
this._dirty = null;
|
|
146
|
+
this._fresh = true;
|
|
147
|
+
this.generation++;
|
|
148
|
+
if (this.texture) {
|
|
149
|
+
this.gl.deleteTexture(this.texture);
|
|
150
|
+
this.texture = null;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Forget every entry, keeping the texture and its size — for an atlas of
|
|
157
|
+
* transient masks, emptied at the start of each frame. Old pixels stay in
|
|
158
|
+
* the CPU copy until new entries overwrite them; nothing names them.
|
|
159
|
+
*/
|
|
160
|
+
reset() {
|
|
161
|
+
if (this.entries.size === 0) return;
|
|
162
|
+
this.entries.clear();
|
|
163
|
+
this._shelfX = PAD;
|
|
164
|
+
this._shelfY = PAD;
|
|
165
|
+
this._shelfH = 0;
|
|
166
|
+
this._dirty = null;
|
|
167
|
+
this.generation++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Make the atlas current on the active texture unit, uploading anything new. */
|
|
171
|
+
bind() {
|
|
172
|
+
const gl = this.gl;
|
|
173
|
+
if (!this.texture) {
|
|
174
|
+
this.texture = gl.createTexture();
|
|
175
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
176
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
177
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
178
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
179
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
180
|
+
this._fresh = true;
|
|
181
|
+
} else {
|
|
182
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
183
|
+
}
|
|
184
|
+
// One byte of coverage per texel, as R8 — GLES 3's own single-channel
|
|
185
|
+
// format — rather than the legacy ALPHA, which drivers emulate. On
|
|
186
|
+
// virgl the emulation let an upload into the mask atlas reach what the
|
|
187
|
+
// glyph atlas sampled: text drawn after a frame's coverage masks showed
|
|
188
|
+
// pieces of them (a select's chevron where a title's "R" belonged).
|
|
189
|
+
// A8 rows are tightly packed and almost never a multiple of 4.
|
|
190
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
191
|
+
if (this._fresh) {
|
|
192
|
+
gl.texImage2D(
|
|
193
|
+
gl.TEXTURE_2D,
|
|
194
|
+
0,
|
|
195
|
+
gl.R8,
|
|
196
|
+
this.size,
|
|
197
|
+
this.size,
|
|
198
|
+
0,
|
|
199
|
+
gl.RED,
|
|
200
|
+
gl.UNSIGNED_BYTE,
|
|
201
|
+
this._pixels,
|
|
202
|
+
);
|
|
203
|
+
this._fresh = false;
|
|
204
|
+
this._dirty = null;
|
|
205
|
+
} else if (this._dirty) {
|
|
206
|
+
const { x0, y0, x1, y1 } = this._dirty;
|
|
207
|
+
const w = x1 - x0;
|
|
208
|
+
const h = y1 - y0;
|
|
209
|
+
// texSubImage2D wants the rows contiguous; a sub-rectangle of the CPU
|
|
210
|
+
// copy is not, so gather them. Small — a frame's worth of new glyphs.
|
|
211
|
+
const rows = new Uint8Array(w * h);
|
|
212
|
+
for (let r = 0; r < h; r++) {
|
|
213
|
+
const src = (y0 + r) * this.size + x0;
|
|
214
|
+
rows.set(this._pixels.subarray(src, src + w), r * w);
|
|
215
|
+
}
|
|
216
|
+
gl.texSubImage2D(
|
|
217
|
+
gl.TEXTURE_2D,
|
|
218
|
+
0,
|
|
219
|
+
x0,
|
|
220
|
+
y0,
|
|
221
|
+
w,
|
|
222
|
+
h,
|
|
223
|
+
gl.RED,
|
|
224
|
+
gl.UNSIGNED_BYTE,
|
|
225
|
+
rows,
|
|
226
|
+
);
|
|
227
|
+
this._dirty = null;
|
|
228
|
+
}
|
|
229
|
+
return this.texture;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
destroy() {
|
|
233
|
+
if (this.texture) this.gl.deleteTexture(this.texture);
|
|
234
|
+
this.texture = null;
|
|
235
|
+
this.entries.clear();
|
|
236
|
+
}
|
|
237
|
+
}
|