react-x11 2.44.0 → 2.44.1
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 +2 -2
- package/src/cocoa/panehost.js +38 -22
- package/src/cocoa/panewindow.js +43 -7
- package/src/index.d.ts +3 -2
- package/src/wayland/context2d.js +233 -59
- package/src/wayland/glyphatlas.js +43 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.44.
|
|
3
|
+
"version": "2.44.1",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"yoga-layout": "^3.2.1"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
|
-
"@windowkit/appkit": "^0.
|
|
112
|
+
"@windowkit/appkit": "^0.27.0",
|
|
113
113
|
"@windowkit/wayland": "^3.1.1",
|
|
114
114
|
"@windowkit/win32": "^0.0.9",
|
|
115
115
|
"dbus-native": "^0.16.0",
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -77,21 +77,30 @@ class PaneLayer {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Whether a pane's last frame is shown at its size
|
|
81
|
-
* frame of the layer's new size lands, or stretched
|
|
82
|
-
* the layer its new size the moment the window lays out, and the
|
|
83
|
-
* frame of that size comes a pane frame later: Core Animation
|
|
84
|
-
* the frame it has to the new bounds meanwhile, and a page's
|
|
85
|
-
* was drawn scaled a little at every step of a window dragged
|
|
86
|
-
* then at its size again. A pane that keeps up has its frame
|
|
87
|
-
* size instead, its edge carried over what it does not cover
|
|
88
|
-
* (`_edgeProps`); one that falls behind the budget is stretched
|
|
89
|
-
* the better picture for the long wait — a pane
|
|
90
|
-
* frames a second shows a squeezed page, not a page
|
|
91
|
-
* The budget is the window's live-resize
|
|
92
|
-
* (`createRoot({ cocoa: { resizeWait } })`), how long a frame
|
|
93
|
-
* size may take before the old one is shown in its place: 0
|
|
94
|
-
* always, as before.
|
|
80
|
+
* Whether a pane's last frame is shown at its size across its layer until
|
|
81
|
+
* a frame of the layer's new size lands, or stretched across it. The host
|
|
82
|
+
* gives the layer its new size the moment the window lays out, and the
|
|
83
|
+
* pane's frame of that size comes a pane frame later: Core Animation
|
|
84
|
+
* stretches the frame it has to the new bounds meanwhile, and a page's
|
|
85
|
+
* left column was drawn scaled a little at every step of a window dragged
|
|
86
|
+
* wider, and then at its size again. A pane that keeps up has its frame
|
|
87
|
+
* shown at its size instead, its edge carried over what it does not cover
|
|
88
|
+
* yet (`_edgeProps`); one that falls behind the budget is stretched
|
|
89
|
+
* across, which is the better picture for the long wait — a pane
|
|
90
|
+
* repainting at three frames a second shows a squeezed page, not a page
|
|
91
|
+
* and a smeared strip. The budget is the window's live-resize
|
|
92
|
+
* handshake's (`createRoot({ cocoa: { resizeWait } })`), how long a frame
|
|
93
|
+
* of a new size may take before the old one is shown in its place: 0
|
|
94
|
+
* stretches always, both ways, as before.
|
|
95
|
+
*
|
|
96
|
+
* Down the layer the frame is at its size whatever this says. A width
|
|
97
|
+
* moves where a page's lines break, and what comes is a page laid out
|
|
98
|
+
* again, which a squeezed one is nearer to than one cut off with a strip
|
|
99
|
+
* beside it. A height moves nothing a page lays out — its lines are as
|
|
100
|
+
* wide as they were, its blocks where they were — so what comes is the
|
|
101
|
+
* frame as it is with more of the page under it, and a frame stretched
|
|
102
|
+
* down was the page drawn too tall at every refresh of the wait, where
|
|
103
|
+
* at its size only the strip under it is not yet the page's.
|
|
95
104
|
*
|
|
96
105
|
* Behind is a pane whose frames come later than the budget, not one with
|
|
97
106
|
* a size waiting that long. `<Frame>` sends a pane the newest size once
|
|
@@ -125,8 +134,11 @@ class PaneLayer {
|
|
|
125
134
|
* carried over the rest (`contentsCenter`, the part of the contents that
|
|
126
135
|
* stretches while everything else keeps its size) — the last column
|
|
127
136
|
* across a window dragged wider, the last row down one dragged taller,
|
|
128
|
-
* and the corner pixel into the corner.
|
|
129
|
-
* the frame is stretched, and
|
|
137
|
+
* and the corner pixel into the corner. Across a pane that fell behind
|
|
138
|
+
* the frame is stretched, and down it is still at its size
|
|
139
|
+
* (`_anchoredNow`): the whole of each row stretched to the new width,
|
|
140
|
+
* cropped or with its last row carried down. Both rects are the whole
|
|
141
|
+
* when the frame is the layer's size, and at a budget of 0.
|
|
130
142
|
*
|
|
131
143
|
* Anchored by gravity alone, the strip the frame did not cover showed
|
|
132
144
|
* whatever was under the pane, which is the `<Frame>`'s background: in
|
|
@@ -147,15 +159,19 @@ class PaneLayer {
|
|
|
147
159
|
const rect = this._rect;
|
|
148
160
|
let crop = null;
|
|
149
161
|
let edge = null;
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
163
|
+
if (frame && rect && budget > 0) {
|
|
164
|
+
// across at its size while the pane keeps up, and down always; an
|
|
165
|
+
// axis stretched is shown whole, the frame's every column across
|
|
166
|
+
const across = this._anchoredNow();
|
|
167
|
+
const w = across ? Math.min(frame.width, rect.width) : frame.width;
|
|
152
168
|
const h = Math.min(frame.height, rect.height);
|
|
153
169
|
if (w < frame.width || h < frame.height) {
|
|
154
170
|
crop = [0, 0, w / frame.width, h / frame.height];
|
|
155
171
|
}
|
|
156
|
-
// in the shown part's own unit square; an axis that did not grow
|
|
157
|
-
//
|
|
158
|
-
const wider = rect.width > w;
|
|
172
|
+
// in the shown part's own unit square; an axis that did not grow, or
|
|
173
|
+
// is stretched, is all centre
|
|
174
|
+
const wider = across && rect.width > w;
|
|
159
175
|
const taller = rect.height > h;
|
|
160
176
|
if (wider || taller) {
|
|
161
177
|
edge = [
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -30,6 +30,12 @@ import { BackendContext2D } from '../backend/context2d.js';
|
|
|
30
30
|
// the pane drawing.
|
|
31
31
|
const ANSWER_TIMEOUT_MS = 250;
|
|
32
32
|
|
|
33
|
+
// Where a ring's slot with no buffer made yet ranks among the buffers a
|
|
34
|
+
// frame may take (`_takeBack`): after every one done with and let go of,
|
|
35
|
+
// ranked by their present's sequence number, and before one the
|
|
36
|
+
// WindowServer still reads, ranked from 2 ** 32 up.
|
|
37
|
+
const EMPTY_SLOT = 2 ** 32 - 1;
|
|
38
|
+
|
|
33
39
|
let nextPaneId = 1;
|
|
34
40
|
|
|
35
41
|
export class CocoaPaneWindow {
|
|
@@ -261,6 +267,8 @@ export class CocoaPaneWindow {
|
|
|
261
267
|
let done = false;
|
|
262
268
|
for (let i = 0; i < this._ring.length; i += 1) {
|
|
263
269
|
const s = this._ring[i];
|
|
270
|
+
// a slot with no buffer yet gets one, which nothing names
|
|
271
|
+
if (!s) return false;
|
|
264
272
|
if (i === this._shownIndex || !this._done(s)) continue;
|
|
265
273
|
if (!this._held(s)) return false;
|
|
266
274
|
done = true;
|
|
@@ -354,6 +362,7 @@ export class CocoaPaneWindow {
|
|
|
354
362
|
const release = this._native.releaseSurface;
|
|
355
363
|
if (typeof release !== 'function') return;
|
|
356
364
|
for (const s of ring) {
|
|
365
|
+
if (!s) continue;
|
|
357
366
|
if (s.presentedSeq > this._shownSeq) this._retired.push(s);
|
|
358
367
|
else release.call(this._native, s.handle);
|
|
359
368
|
}
|
|
@@ -407,6 +416,20 @@ export class CocoaPaneWindow {
|
|
|
407
416
|
for (const s of retired) release.call(this._native, s.handle);
|
|
408
417
|
}
|
|
409
418
|
|
|
419
|
+
/**
|
|
420
|
+
* A buffer of the ring, `w` by `h` device px, made the first time a frame
|
|
421
|
+
* takes its slot rather than with the ring. A drag resizes the pane a
|
|
422
|
+
* tick at a time and gives each size one frame, so a ring made whole was
|
|
423
|
+
* three buffers made and cleared for the one drawn into: Zen Garden 101
|
|
424
|
+
* in the browser example spent 4.4 ms of every 12 ms frame of a drag in
|
|
425
|
+
* `_ensureSurface`. Cleared where nothing is copied over it.
|
|
426
|
+
*/
|
|
427
|
+
_newBuffer(w, h, clear) {
|
|
428
|
+
const s = this._native.createSurfaceIOSurface(w, h, this.scale, true);
|
|
429
|
+
if (clear) this._native.ctxClearRect(s.handle, 0, 0, w, h);
|
|
430
|
+
return s;
|
|
431
|
+
}
|
|
432
|
+
|
|
410
433
|
_ensureSurface() {
|
|
411
434
|
const w = this.width;
|
|
412
435
|
const h = this.height;
|
|
@@ -417,12 +440,10 @@ export class CocoaPaneWindow {
|
|
|
417
440
|
) {
|
|
418
441
|
const hadSurface = Boolean(this._ring);
|
|
419
442
|
this._releaseRing();
|
|
420
|
-
this
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
this._ring.push(s);
|
|
425
|
-
}
|
|
443
|
+
// the buffer this frame draws into, and the rest of the ring as a
|
|
444
|
+
// frame first takes it (`_takeBack`)
|
|
445
|
+
this._ring = new Array(CocoaPaneWindow.RING).fill(null);
|
|
446
|
+
this._ring[0] = this._newBuffer(w, h, true);
|
|
426
447
|
this._drawIndex = 0;
|
|
427
448
|
this._shownIndex = -1;
|
|
428
449
|
this._native.surfaceLock(this._ring[0].handle);
|
|
@@ -478,6 +499,15 @@ export class CocoaPaneWindow {
|
|
|
478
499
|
for (let i = 0; i < ring.length; i += 1) {
|
|
479
500
|
const s = ring[i];
|
|
480
501
|
if (s === last) continue;
|
|
502
|
+
// a slot no buffer has been made for yet: after one let go of, which
|
|
503
|
+
// costs nothing to take, and before any the WindowServer still reads
|
|
504
|
+
if (!s) {
|
|
505
|
+
if (EMPTY_SLOT < rank) {
|
|
506
|
+
rank = EMPTY_SLOT;
|
|
507
|
+
pick = i;
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
481
511
|
// done and let go of, then done, then on its way, then on the layer:
|
|
482
512
|
// the oldest of the best — but of the done, the one the screen showed
|
|
483
513
|
// before the host's latest answers last
|
|
@@ -494,7 +524,13 @@ export class CocoaPaneWindow {
|
|
|
494
524
|
pick = i;
|
|
495
525
|
}
|
|
496
526
|
}
|
|
497
|
-
|
|
527
|
+
// what the frame just presented is copied over whole, below, and only a
|
|
528
|
+
// slot with none to copy is cleared
|
|
529
|
+
const back = (ring[pick] ??= this._newBuffer(
|
|
530
|
+
this._surfaceSize.width,
|
|
531
|
+
this._surfaceSize.height,
|
|
532
|
+
!last,
|
|
533
|
+
));
|
|
498
534
|
this._drawIndex = pick;
|
|
499
535
|
this._surface = back.handle;
|
|
500
536
|
// a different native surface owns the graphics state now — the context
|
package/src/index.d.ts
CHANGED
|
@@ -292,8 +292,9 @@ export interface RootOptions {
|
|
|
292
292
|
* the frame is painted on another thread (50 by default; 0 lets the edge
|
|
293
293
|
* move without waiting). A `<Frame>` pane's frames of a new size are held
|
|
294
294
|
* to it as well: within it, the pane's last frame stays at its size while
|
|
295
|
-
* the next comes, and past it, it is stretched to the pane's new
|
|
296
|
-
* every one is
|
|
295
|
+
* the next comes, and past it, it is stretched across to the pane's new
|
|
296
|
+
* width and stays at its size down it; at 0 every one is stretched both
|
|
297
|
+
* ways.
|
|
297
298
|
* `screenPoll` is how often the screen layout is re-read while a
|
|
298
299
|
* {@link useScreens} subscriber is mounted, in ms — 500 by default, 0 for
|
|
299
300
|
* never. macOS has no event for a display plugged in, unplugged or
|
package/src/wayland/context2d.js
CHANGED
|
@@ -92,6 +92,9 @@ const SWIZZLE_STRAIGHT = 2; // straight RGBA (ImageData)
|
|
|
92
92
|
/** floats per vertex: pos(2) uv(2) color(4) params(4) */
|
|
93
93
|
const STRIDE = 12;
|
|
94
94
|
const MAX_VERTS = 6 * 4096;
|
|
95
|
+
/** What a context's vertex array starts at: a surface that draws a handful
|
|
96
|
+
* of quads should not pay for the window's batch. */
|
|
97
|
+
const FIRST_VERTS = 6 * 64;
|
|
95
98
|
const TRI = [0, 1, 2, 0, 2, 3];
|
|
96
99
|
|
|
97
100
|
const VERT = `
|
|
@@ -265,6 +268,63 @@ function parseColorUncached(value) {
|
|
|
265
268
|
* use. Interpolated premultiplied, which is what keeps a fade to transparent
|
|
266
269
|
* from passing through grey.
|
|
267
270
|
*/
|
|
271
|
+
/**
|
|
272
|
+
* The textures made for a gradient or an image are found by the object, in
|
|
273
|
+
* a WeakMap — and a WeakMap cannot say when its key goes, so its entry went
|
|
274
|
+
* with the key and the texture stayed in the driver. A page that makes its
|
|
275
|
+
* gradients as it paints made a texture a paint, and every surface context
|
|
276
|
+
* left behind what it had uploaded. Each is registered here and deleted
|
|
277
|
+
* when its key is collected, unless the context that made it was destroyed
|
|
278
|
+
* first and deleted it then (`made` is that context's set, which is all the
|
|
279
|
+
* registration holds of it).
|
|
280
|
+
*/
|
|
281
|
+
const textureReaper = new FinalizationRegistry(({ gl, tex, made }) => {
|
|
282
|
+
if (!made.delete(tex)) return;
|
|
283
|
+
try {
|
|
284
|
+
gl.deleteTexture(tex);
|
|
285
|
+
} catch {
|
|
286
|
+
// the device went first
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* What the contexts on one device share (device.js): the program and its
|
|
292
|
+
* locations, the GL buffer a batch goes up through, and the glyph atlas.
|
|
293
|
+
* Each was a context's own, so every offscreen surface compiled and linked
|
|
294
|
+
* the program, allocated a buffer as large as the window's batch and
|
|
295
|
+
* rasterised every glyph it drew into an empty atlas — a few milliseconds a
|
|
296
|
+
* surface, which <Html> made at every width of a resize. Counted by the
|
|
297
|
+
* contexts that hold it; the last one destroyed deletes it.
|
|
298
|
+
*
|
|
299
|
+
* Not the vertex array: the device's owner is the only context with quads
|
|
300
|
+
* buffered, but `releaseDevice` lets a context keep a batch across foreign
|
|
301
|
+
* GL with no owner at all, and another context's quads written into a
|
|
302
|
+
* shared array would land on top of it. Each context keeps its own, grown
|
|
303
|
+
* as it draws. The GL buffer is filled from it immediately before each draw,
|
|
304
|
+
* so sharing that is safe.
|
|
305
|
+
*/
|
|
306
|
+
function sharedOf(device, gl) {
|
|
307
|
+
if (device.shared) return device.shared;
|
|
308
|
+
const atlas = new GlyphAtlas(gl);
|
|
309
|
+
const shared = {
|
|
310
|
+
refs: 0,
|
|
311
|
+
program: null,
|
|
312
|
+
loc: null,
|
|
313
|
+
buffer: null,
|
|
314
|
+
atlas,
|
|
315
|
+
/** the contexts with glyph quads buffered against the atlas */
|
|
316
|
+
glyphHolders: new Set(),
|
|
317
|
+
};
|
|
318
|
+
// Quads already buffered against the atlas must be drawn before it lays
|
|
319
|
+
// itself out again under them — every context's, owner or not: drawing
|
|
320
|
+
// one claims the device and puts its own state back first.
|
|
321
|
+
atlas.onBeforeGrow = () => {
|
|
322
|
+
for (const ctx of [...shared.glyphHolders]) ctx._flush();
|
|
323
|
+
};
|
|
324
|
+
device.shared = shared;
|
|
325
|
+
return shared;
|
|
326
|
+
}
|
|
327
|
+
|
|
268
328
|
class Gradient {
|
|
269
329
|
constructor(kind, a) {
|
|
270
330
|
this.kind = kind; // 'linear' | 'radial'
|
|
@@ -378,18 +438,16 @@ export class WaylandContext2D {
|
|
|
378
438
|
this.fontManager = fontManager;
|
|
379
439
|
/** @see ./device.js — the state this context shares with its siblings */
|
|
380
440
|
this._device = glDevice(gl);
|
|
441
|
+
this._shared = sharedOf(this._device, gl);
|
|
442
|
+
this._shared.refs++;
|
|
381
443
|
this._makeCurrent = makeCurrent ?? null;
|
|
382
444
|
// `TextLayout.draw` opens with `ctx.window.app.display.Render` and reads
|
|
383
445
|
// one constant from it. Satisfy the shape rather than fork the layout.
|
|
384
446
|
this.window = { app: { display: { Render: RENDER } } };
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
this.
|
|
389
|
-
// Path fills that go through the CPU rasteriser land here as A8 masks,
|
|
390
|
-
// drawn like glyphs; emptied every frame, since a mask is one draw.
|
|
391
|
-
this.masks = new GlyphAtlas(gl, { size: 256 });
|
|
392
|
-
this.masks.onBeforeGrow = () => this._flush();
|
|
447
|
+
/** the device's glyph atlas (`sharedOf`) */
|
|
448
|
+
this.atlas = this._shared.atlas;
|
|
449
|
+
/** this context's mask atlas, made the first time a fill needs one */
|
|
450
|
+
this._masks = null;
|
|
393
451
|
this._maskSeq = 0;
|
|
394
452
|
/** the non-rectangular clips in force, device space, outermost first */
|
|
395
453
|
this._clipPath = null;
|
|
@@ -426,7 +484,7 @@ export class WaylandContext2D {
|
|
|
426
484
|
this._width = 0;
|
|
427
485
|
this._height = 0;
|
|
428
486
|
|
|
429
|
-
this._verts = new Float32Array(
|
|
487
|
+
this._verts = new Float32Array(FIRST_VERTS * STRIDE);
|
|
430
488
|
this._n = 0;
|
|
431
489
|
this._mode = MODE_SOLID;
|
|
432
490
|
this._texture = null;
|
|
@@ -439,26 +497,69 @@ export class WaylandContext2D {
|
|
|
439
497
|
/** image object -> { tex, w, h } */
|
|
440
498
|
this._textures = new WeakMap();
|
|
441
499
|
this._gradTex = new WeakMap();
|
|
500
|
+
/** the textures the two maps above hold, for `destroy()` */
|
|
501
|
+
this._madeTextures = new Set();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Path fills that go through the CPU rasteriser land here as A8 masks,
|
|
506
|
+
* drawn like glyphs; emptied every frame, since a mask is one draw. A
|
|
507
|
+
* mask's key is this context's own count (`_maskSeq`), so the atlas is
|
|
508
|
+
* this context's — and only made once a fill needs it.
|
|
509
|
+
*/
|
|
510
|
+
get masks() {
|
|
511
|
+
if (!this._masks) {
|
|
512
|
+
this._masks = new GlyphAtlas(this.gl, { size: 256 });
|
|
513
|
+
this._masks.onBeforeGrow = () => this._flush();
|
|
514
|
+
}
|
|
515
|
+
return this._masks;
|
|
442
516
|
}
|
|
443
517
|
|
|
444
518
|
// ---- lifecycle --------------------------------------------------------
|
|
445
519
|
|
|
446
|
-
/**
|
|
520
|
+
/** Take the device's program and buffer, compiling and allocating them
|
|
521
|
+
* the first time. Context must be current. */
|
|
447
522
|
init() {
|
|
448
|
-
const gl = this.gl;
|
|
449
523
|
if (this._program) return;
|
|
524
|
+
const shared = this._shared;
|
|
525
|
+
if (!shared.program) this._build(shared);
|
|
526
|
+
this._program = shared.program;
|
|
527
|
+
this._loc = shared.loc;
|
|
528
|
+
this._buffer = shared.buffer;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Compile the program and allocate the vertex buffer the device shares. */
|
|
532
|
+
_build(shared) {
|
|
533
|
+
const gl = this.gl;
|
|
450
534
|
const vs = compile(gl, gl.VERTEX_SHADER, VERT, 'vertex');
|
|
451
|
-
|
|
535
|
+
let fs;
|
|
536
|
+
try {
|
|
537
|
+
fs = compile(gl, gl.FRAGMENT_SHADER, FRAG, 'fragment');
|
|
538
|
+
} catch (err) {
|
|
539
|
+
gl.deleteShader(vs);
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
452
542
|
const p = gl.createProgram();
|
|
453
543
|
gl.attachShader(p, vs);
|
|
454
544
|
gl.attachShader(p, fs);
|
|
455
545
|
gl.linkProgram(p);
|
|
456
|
-
|
|
457
|
-
|
|
546
|
+
const linked = gl.getProgramParameter(p, gl.LINK_STATUS);
|
|
547
|
+
const log = linked ? '' : gl.getProgramInfoLog(p);
|
|
548
|
+
// The shader objects are ours to delete once the program is linked.
|
|
549
|
+
// Deleting a program detaches its shaders and leaves them (GLES 3.0,
|
|
550
|
+
// 7.3), so every context — a window's, and each offscreen surface's —
|
|
551
|
+
// left two behind, and a resize that makes a surface a frame left two a
|
|
552
|
+
// frame. Deleted while attached, they are flagged and go with the
|
|
553
|
+
// program.
|
|
554
|
+
gl.deleteShader(vs);
|
|
555
|
+
gl.deleteShader(fs);
|
|
556
|
+
if (!linked) {
|
|
557
|
+
gl.deleteProgram(p);
|
|
558
|
+
throw new Error(`2d program failed to link: ${log}`);
|
|
458
559
|
}
|
|
459
|
-
|
|
560
|
+
shared.program = p;
|
|
460
561
|
const u = (n) => gl.getUniformLocation(p, n);
|
|
461
|
-
|
|
562
|
+
shared.loc = {
|
|
462
563
|
aPos: gl.getAttribLocation(p, 'aPos'),
|
|
463
564
|
aUV: gl.getAttribLocation(p, 'aUV'),
|
|
464
565
|
aColor: gl.getAttribLocation(p, 'aColor'),
|
|
@@ -471,11 +572,16 @@ export class WaylandContext2D {
|
|
|
471
572
|
uGradA: u('uGradA'),
|
|
472
573
|
uGradB: u('uGradB'),
|
|
473
574
|
};
|
|
474
|
-
|
|
475
|
-
gl.bindBuffer(gl.ARRAY_BUFFER,
|
|
575
|
+
shared.buffer = gl.createBuffer();
|
|
576
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, shared.buffer);
|
|
476
577
|
// The addon's bufferData takes a TypedArray, not the byte-length
|
|
477
|
-
// overload WebGL also accepts. Allocated once
|
|
478
|
-
|
|
578
|
+
// overload WebGL also accepts. Allocated once a device, as large as a
|
|
579
|
+
// batch can be; frames use bufferSubData.
|
|
580
|
+
gl.bufferData(
|
|
581
|
+
gl.ARRAY_BUFFER,
|
|
582
|
+
new Float32Array(MAX_VERTS * STRIDE),
|
|
583
|
+
gl.DYNAMIC_DRAW,
|
|
584
|
+
);
|
|
479
585
|
}
|
|
480
586
|
|
|
481
587
|
/** Where this context draws. A window's backing, or a surface's target. */
|
|
@@ -521,7 +627,7 @@ export class WaylandContext2D {
|
|
|
521
627
|
this._clipPath = null;
|
|
522
628
|
this._stencilling = false;
|
|
523
629
|
this._maskSeq = 0;
|
|
524
|
-
this.
|
|
630
|
+
this._masks?.reset();
|
|
525
631
|
this._applyState();
|
|
526
632
|
// scratch for paths and clips: a frame starts with it clean
|
|
527
633
|
const gl = this.gl;
|
|
@@ -1059,6 +1165,7 @@ export class WaylandContext2D {
|
|
|
1059
1165
|
const ramp = grad.ramp();
|
|
1060
1166
|
if (!tex) {
|
|
1061
1167
|
tex = gl.createTexture();
|
|
1168
|
+
this._keepTexture(grad, tex);
|
|
1062
1169
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1063
1170
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1064
1171
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
@@ -1631,8 +1738,9 @@ export class WaylandContext2D {
|
|
|
1631
1738
|
? data
|
|
1632
1739
|
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1633
1740
|
if (!entry || entry.w !== w || entry.h !== h) {
|
|
1634
|
-
if (entry)
|
|
1741
|
+
if (entry) this._dropTexture(entry.tex);
|
|
1635
1742
|
const tex = gl.createTexture();
|
|
1743
|
+
this._keepTexture(img, tex);
|
|
1636
1744
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1637
1745
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1638
1746
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
@@ -1792,13 +1900,12 @@ export class WaylandContext2D {
|
|
|
1792
1900
|
const size = run.size ?? font.size ?? 16;
|
|
1793
1901
|
const color = this._color(run.color ?? this.fillStyle);
|
|
1794
1902
|
if (color[3] === 0) continue;
|
|
1795
|
-
const
|
|
1903
|
+
const table = this.atlas.table(`${fontKey(font)}|${size}`);
|
|
1796
1904
|
const tex = this.atlas.bind();
|
|
1797
1905
|
this._setMode(MODE_GLYPH, tex, SWIZZLE_RGBA);
|
|
1906
|
+
this._shared.glyphHolders.add(this);
|
|
1798
1907
|
for (const g of run.glyphs) {
|
|
1799
|
-
const entry = this.atlas.
|
|
1800
|
-
font.rasterize(g.id, size),
|
|
1801
|
-
);
|
|
1908
|
+
const entry = this.atlas.glyph(table, font, g.id, size);
|
|
1802
1909
|
if (entry.empty) continue;
|
|
1803
1910
|
// `left`/`top` are the bitmap's offset from the glyph origin in the
|
|
1804
1911
|
// rasteriser's **y-down** pixel space: a cap-height glyph has a
|
|
@@ -1807,31 +1914,20 @@ export class WaylandContext2D {
|
|
|
1807
1914
|
const oy = Math.round(applyY(m, g.x, g.y)) + entry.top;
|
|
1808
1915
|
// the atlas may have grown mid-run; rebind so the batch names the
|
|
1809
1916
|
// new texture (the old quads were flushed by onBeforeGrow)
|
|
1810
|
-
if (this.atlas.texture !== this._texture)
|
|
1917
|
+
if (this.atlas.texture !== this._texture) {
|
|
1811
1918
|
this._setMode(MODE_GLYPH, this.atlas.bind(), SWIZZLE_RGBA);
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
[
|
|
1824
|
-
entry.u0,
|
|
1825
|
-
entry.v0,
|
|
1826
|
-
entry.u1,
|
|
1827
|
-
entry.v0,
|
|
1828
|
-
entry.u1,
|
|
1829
|
-
entry.v1,
|
|
1830
|
-
entry.u0,
|
|
1831
|
-
entry.v1,
|
|
1832
|
-
],
|
|
1919
|
+
this._shared.glyphHolders.add(this);
|
|
1920
|
+
}
|
|
1921
|
+
this._quadRect(
|
|
1922
|
+
ox,
|
|
1923
|
+
oy,
|
|
1924
|
+
ox + entry.w,
|
|
1925
|
+
oy + entry.h,
|
|
1926
|
+
entry.u0,
|
|
1927
|
+
entry.v0,
|
|
1928
|
+
entry.u1,
|
|
1929
|
+
entry.v1,
|
|
1833
1930
|
color,
|
|
1834
|
-
ZERO4,
|
|
1835
1931
|
);
|
|
1836
1932
|
this.shapeStats.glyphs++;
|
|
1837
1933
|
}
|
|
@@ -1992,7 +2088,7 @@ export class WaylandContext2D {
|
|
|
1992
2088
|
// `_take` is the only thing that can get it issued before someone reads
|
|
1993
2089
|
// the pixels it makes. A property compare in the common case.
|
|
1994
2090
|
this._claim();
|
|
1995
|
-
|
|
2091
|
+
this._room(6);
|
|
1996
2092
|
const v = this._verts;
|
|
1997
2093
|
const each = params.length === 16 ? 4 : 0;
|
|
1998
2094
|
let o = this._n * STRIDE;
|
|
@@ -2019,7 +2115,7 @@ export class WaylandContext2D {
|
|
|
2019
2115
|
/** One triangle, for the stencil pass. Colour and params are irrelevant. */
|
|
2020
2116
|
_tri(x0, y0, x1, y1, x2, y2) {
|
|
2021
2117
|
this._claim();
|
|
2022
|
-
|
|
2118
|
+
this._room(3);
|
|
2023
2119
|
const v = this._verts;
|
|
2024
2120
|
let o = this._n * STRIDE;
|
|
2025
2121
|
const pts = [x0, y0, x1, y1, x2, y2];
|
|
@@ -2031,11 +2127,61 @@ export class WaylandContext2D {
|
|
|
2031
2127
|
this._n += 3;
|
|
2032
2128
|
}
|
|
2033
2129
|
|
|
2130
|
+
/**
|
|
2131
|
+
* An axis-aligned quad, TL-TR-BR-BL, with no params: `_quad` for a glyph,
|
|
2132
|
+
* written straight into the batch rather than through two arrays of
|
|
2133
|
+
* corners made for it — a glyph's worth of garbage a glyph.
|
|
2134
|
+
*/
|
|
2135
|
+
_quadRect(x0, y0, x1, y1, u0, v0, u1, v1, color) {
|
|
2136
|
+
this._claim();
|
|
2137
|
+
this._room(6);
|
|
2138
|
+
const v = this._verts;
|
|
2139
|
+
let o = this._n * STRIDE;
|
|
2140
|
+
const r = color[0];
|
|
2141
|
+
const g = color[1];
|
|
2142
|
+
const b = color[2];
|
|
2143
|
+
const a = color[3];
|
|
2144
|
+
for (let k = 0; k < 6; k++) {
|
|
2145
|
+
const i = TRI[k];
|
|
2146
|
+
const right = i === 1 || i === 2;
|
|
2147
|
+
const bottom = i >= 2;
|
|
2148
|
+
v[o++] = right ? x1 : x0;
|
|
2149
|
+
v[o++] = bottom ? y1 : y0;
|
|
2150
|
+
v[o++] = right ? u1 : u0;
|
|
2151
|
+
v[o++] = bottom ? v1 : v0;
|
|
2152
|
+
v[o++] = r;
|
|
2153
|
+
v[o++] = g;
|
|
2154
|
+
v[o++] = b;
|
|
2155
|
+
v[o++] = a;
|
|
2156
|
+
v[o++] = 0;
|
|
2157
|
+
v[o++] = 0;
|
|
2158
|
+
v[o++] = 0;
|
|
2159
|
+
v[o++] = 0;
|
|
2160
|
+
}
|
|
2161
|
+
this._n += 6;
|
|
2162
|
+
this.shapeStats.quads++;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/** Room for `count` more vertices: the batch drawn where it is full, and
|
|
2166
|
+
* the array grown where it is smaller than a batch can be. */
|
|
2167
|
+
_room(count) {
|
|
2168
|
+
if (this._n + count > MAX_VERTS) this._flush();
|
|
2169
|
+
const need = (this._n + count) * STRIDE;
|
|
2170
|
+
if (need <= this._verts.length) return;
|
|
2171
|
+
const grown = new Float32Array(
|
|
2172
|
+
Math.min(MAX_VERTS * STRIDE, Math.max(need, this._verts.length * 2)),
|
|
2173
|
+
);
|
|
2174
|
+
grown.set(this._verts.subarray(0, this._n * STRIDE));
|
|
2175
|
+
this._verts = grown;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2034
2178
|
_flush() {
|
|
2035
2179
|
// Before the early return, not after: `clearRect` and `putImageData`
|
|
2036
2180
|
// flush, change the blend function and flush again, and the second
|
|
2037
2181
|
// flush must not be the one that re-takes the device and puts the
|
|
2038
2182
|
// blending back.
|
|
2183
|
+
// drawn or empty, nothing of this context's is left against the atlas
|
|
2184
|
+
this._shared.glyphHolders.delete(this);
|
|
2039
2185
|
this._claim();
|
|
2040
2186
|
if (this._n === 0) return;
|
|
2041
2187
|
const gl = this.gl;
|
|
@@ -2072,7 +2218,8 @@ export class WaylandContext2D {
|
|
|
2072
2218
|
// then it samples texels those glyphs never reached: garbled text,
|
|
2073
2219
|
// kept until that region is next repainted.
|
|
2074
2220
|
if (this._texture === this.atlas.texture) this.atlas.bind();
|
|
2075
|
-
else if (this._texture === this.
|
|
2221
|
+
else if (this._masks && this._texture === this._masks.texture)
|
|
2222
|
+
this._masks.bind();
|
|
2076
2223
|
gl.activeTexture(gl.TEXTURE0);
|
|
2077
2224
|
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
|
2078
2225
|
gl.uniform1i(this._loc.uTex, 0);
|
|
@@ -2127,14 +2274,41 @@ export class WaylandContext2D {
|
|
|
2127
2274
|
};
|
|
2128
2275
|
}
|
|
2129
2276
|
|
|
2277
|
+
/** A texture made for `key`: deleted when `key` is collected, or when
|
|
2278
|
+
* this context is destroyed, whichever comes first. */
|
|
2279
|
+
_keepTexture(key, tex) {
|
|
2280
|
+
this._madeTextures.add(tex);
|
|
2281
|
+
textureReaper.register(key, {
|
|
2282
|
+
gl: this.gl,
|
|
2283
|
+
tex,
|
|
2284
|
+
made: this._madeTextures,
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
/** A texture made for a key that is being given another. */
|
|
2289
|
+
_dropTexture(tex) {
|
|
2290
|
+
if (this._madeTextures.delete(tex)) this.gl.deleteTexture(tex);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2130
2293
|
destroy() {
|
|
2131
2294
|
// Nothing of this context's is installed any more, and the device
|
|
2132
2295
|
// record should not be the one thing still holding on to it.
|
|
2296
|
+
if (this._destroyed) return;
|
|
2297
|
+
this._destroyed = true;
|
|
2133
2298
|
this._release();
|
|
2134
|
-
this.
|
|
2135
|
-
this.
|
|
2136
|
-
|
|
2137
|
-
|
|
2299
|
+
for (const tex of this._madeTextures) this.gl.deleteTexture(tex);
|
|
2300
|
+
this._madeTextures.clear();
|
|
2301
|
+
this._masks?.destroy();
|
|
2302
|
+
this._masks = null;
|
|
2303
|
+
const shared = this._shared;
|
|
2304
|
+
shared.glyphHolders.delete(this);
|
|
2305
|
+
if (--shared.refs === 0) {
|
|
2306
|
+
// the last context on the device: what it shared goes with it
|
|
2307
|
+
shared.atlas.destroy();
|
|
2308
|
+
if (shared.buffer) this.gl.deleteBuffer(shared.buffer);
|
|
2309
|
+
if (shared.program) this.gl.deleteProgram(shared.program);
|
|
2310
|
+
if (this._device.shared === shared) this._device.shared = null;
|
|
2311
|
+
}
|
|
2138
2312
|
this._buffer = null;
|
|
2139
2313
|
this._program = null;
|
|
2140
2314
|
}
|
|
@@ -2328,9 +2502,9 @@ function compile(gl, type, src, what) {
|
|
|
2328
2502
|
gl.shaderSource(sh, src);
|
|
2329
2503
|
gl.compileShader(sh);
|
|
2330
2504
|
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
);
|
|
2505
|
+
const log = gl.getShaderInfoLog(sh);
|
|
2506
|
+
gl.deleteShader(sh);
|
|
2507
|
+
throw new Error(`${what} shader failed to compile: ${log}`);
|
|
2334
2508
|
}
|
|
2335
2509
|
return sh;
|
|
2336
2510
|
}
|
|
@@ -34,6 +34,8 @@ export class GlyphAtlas {
|
|
|
34
34
|
this.size = size;
|
|
35
35
|
/** key -> { x, y, w, h, left, top, u0, v0, u1, v1 } */
|
|
36
36
|
this.entries = new Map();
|
|
37
|
+
/** a face at a size -> (glyph id -> entry), for `glyph()` */
|
|
38
|
+
this._tables = new Map();
|
|
37
39
|
this._shelfY = PAD;
|
|
38
40
|
this._shelfH = 0;
|
|
39
41
|
this._shelfX = PAD;
|
|
@@ -58,17 +60,47 @@ export class GlyphAtlas {
|
|
|
58
60
|
get(key, raster) {
|
|
59
61
|
const hit = this.entries.get(key);
|
|
60
62
|
if (hit) return hit;
|
|
63
|
+
const entry = this._insert(raster());
|
|
64
|
+
if (entry.kept) this.entries.set(key, entry);
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The entries of one face at one size, by glyph id. A run of text looks
|
|
70
|
+
* its table up once, by the string that names the face and the size, and
|
|
71
|
+
* each glyph in it by number (`glyph()`): a string key built and hashed a
|
|
72
|
+
* glyph was most of what drawing text cost once the glyphs were in.
|
|
73
|
+
*
|
|
74
|
+
* A table is emptied in place when the atlas grows or resets, so one held
|
|
75
|
+
* across a `glyph()` that grew the atlas finds its glyphs missing rather
|
|
76
|
+
* than placed where they no longer are.
|
|
77
|
+
*/
|
|
78
|
+
table(key) {
|
|
79
|
+
let table = this._tables.get(key);
|
|
80
|
+
if (!table) this._tables.set(key, (table = new Map()));
|
|
81
|
+
return table;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One glyph of a `table()`, rasterised by `font` on a miss. */
|
|
85
|
+
glyph(table, font, id, size) {
|
|
86
|
+
const hit = table.get(id);
|
|
87
|
+
if (hit) return hit;
|
|
88
|
+
const entry = this._insert(font.rasterize(id, size));
|
|
89
|
+
if (entry.kept) table.set(id, entry);
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
61
92
|
|
|
62
|
-
|
|
93
|
+
/** Place a rasterised glyph, or say it has no ink. `kept` is whether the
|
|
94
|
+
* answer is worth remembering: a glyph the atlas had no room for is not,
|
|
95
|
+
* so the next frame asks again. */
|
|
96
|
+
_insert(g) {
|
|
63
97
|
// A space has no ink. Cache the absence so the miss is paid once.
|
|
64
98
|
if (!g || g.width <= 0 || g.height <= 0) {
|
|
65
|
-
|
|
66
|
-
this.entries.set(key, empty);
|
|
67
|
-
return empty;
|
|
99
|
+
return { empty: true, kept: true, left: g?.left ?? 0, top: g?.top ?? 0 };
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
const spot = this._place(g.width, g.height);
|
|
71
|
-
if (!spot) return { empty: true, left: g.left, top: g.top };
|
|
103
|
+
if (!spot) return { empty: true, kept: false, left: g.left, top: g.top };
|
|
72
104
|
|
|
73
105
|
for (let row = 0; row < g.height; row++) {
|
|
74
106
|
const src = row * g.width;
|
|
@@ -78,8 +110,9 @@ export class GlyphAtlas {
|
|
|
78
110
|
this._markDirty(spot.x, spot.y, g.width, g.height);
|
|
79
111
|
|
|
80
112
|
const inv = 1 / this.size;
|
|
81
|
-
|
|
113
|
+
return {
|
|
82
114
|
empty: false,
|
|
115
|
+
kept: true,
|
|
83
116
|
x: spot.x,
|
|
84
117
|
y: spot.y,
|
|
85
118
|
w: g.width,
|
|
@@ -91,8 +124,6 @@ export class GlyphAtlas {
|
|
|
91
124
|
u1: (spot.x + g.width) * inv,
|
|
92
125
|
v1: (spot.y + g.height) * inv,
|
|
93
126
|
};
|
|
94
|
-
this.entries.set(key, entry);
|
|
95
|
-
return entry;
|
|
96
127
|
}
|
|
97
128
|
|
|
98
129
|
_markDirty(x, y, w, h) {
|
|
@@ -139,6 +170,7 @@ export class GlyphAtlas {
|
|
|
139
170
|
this.size *= 2;
|
|
140
171
|
this._pixels = new Uint8Array(this.size * this.size);
|
|
141
172
|
this.entries.clear();
|
|
173
|
+
for (const table of this._tables.values()) table.clear();
|
|
142
174
|
this._shelfX = PAD;
|
|
143
175
|
this._shelfY = PAD;
|
|
144
176
|
this._shelfH = 0;
|
|
@@ -158,8 +190,9 @@ export class GlyphAtlas {
|
|
|
158
190
|
* the CPU copy until new entries overwrite them; nothing names them.
|
|
159
191
|
*/
|
|
160
192
|
reset() {
|
|
161
|
-
if (this.entries.size === 0) return;
|
|
193
|
+
if (this.entries.size === 0 && this._tables.size === 0) return;
|
|
162
194
|
this.entries.clear();
|
|
195
|
+
for (const table of this._tables.values()) table.clear();
|
|
163
196
|
this._shelfX = PAD;
|
|
164
197
|
this._shelfY = PAD;
|
|
165
198
|
this._shelfH = 0;
|
|
@@ -233,5 +266,6 @@ export class GlyphAtlas {
|
|
|
233
266
|
if (this.texture) this.gl.deleteTexture(this.texture);
|
|
234
267
|
this.texture = null;
|
|
235
268
|
this.entries.clear();
|
|
269
|
+
for (const table of this._tables.values()) table.clear();
|
|
236
270
|
}
|
|
237
271
|
}
|