ntk 8.12.10 → 8.12.12
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/lib/glswapchain.js +60 -19
- package/lib/renderingcontext_cgl.js +18 -1
- package/lib/renderingcontext_gles.js +40 -13
- package/lib/window.js +10 -3
- package/package.json +1 -1
package/lib/glswapchain.js
CHANGED
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
// and every later frame in that buffer is a Present of a pixmap the server
|
|
13
13
|
// already has.
|
|
14
14
|
//
|
|
15
|
-
//
|
|
15
|
+
// Three rules shape the code. **A buffer belongs to the server until IdleNotify
|
|
16
16
|
// says otherwise** — drawing into one before then paints whatever is on
|
|
17
|
-
// screen — so a buffer is released back to GBM there and nowhere else.
|
|
18
|
-
//
|
|
17
|
+
// screen — so a buffer is released back to GBM there and nowhere else. **A
|
|
18
|
+
// pixmap must exist before it is presented**: a new buffer's import is
|
|
19
19
|
// confirmed by a round trip before the first Present names it, or the Present
|
|
20
20
|
// would fail with BadPixmap. A new buffer appears a handful of times per
|
|
21
21
|
// generation (and never again in steady state), so that round trip is a
|
|
22
|
-
// startup cost, not a per-frame one.
|
|
22
|
+
// startup cost, not a per-frame one. And **a surface is never destroyed while
|
|
23
|
+
// it is current** (`_destroySurface`): destroying one takes its GBM surface —
|
|
24
|
+
// the native window under the EGL surface — with it, while EGL still holds
|
|
25
|
+
// the EGL surface as current, and on NVIDIA's driver the next surface made
|
|
26
|
+
// current after that swaps nothing but EGL_BAD_SURFACE. A resize retires the
|
|
27
|
+
// generation being drawn into, so every resize of a window killed its GL.
|
|
23
28
|
|
|
24
29
|
import { GLError, glError } from './gl.js';
|
|
25
30
|
|
|
@@ -88,6 +93,9 @@ export class GLSwapchain {
|
|
|
88
93
|
this.onReady = null;
|
|
89
94
|
/** called once the first buffer has been imported, or it failed */
|
|
90
95
|
this.onValidated = null;
|
|
96
|
+
/** called with the error when the chain fails after it validated — the
|
|
97
|
+
* one failure `onValidated` has no way left to report */
|
|
98
|
+
this.onError = null;
|
|
91
99
|
this._validated = false;
|
|
92
100
|
|
|
93
101
|
this._eid = this.X.AllocID();
|
|
@@ -107,10 +115,44 @@ export class GLSwapchain {
|
|
|
107
115
|
const h = Math.max(1, Math.round(height));
|
|
108
116
|
const gen = this.generation;
|
|
109
117
|
if (gen && gen.width === w && gen.height === h) return gen.surface;
|
|
118
|
+
// The new generation first, and only then the old one's retirement: a
|
|
119
|
+
// driver that refuses the new size leaves the chain drawing where it
|
|
120
|
+
// was, and the old surface — current, as a rule, since this runs at the
|
|
121
|
+
// top of a frame — is let go by `_destroySurface`, which unbinds it.
|
|
122
|
+
// A generation that had to go linear keeps that layout: whatever refused
|
|
123
|
+
// the tiled buffer — the server, or EGL itself — has not changed since.
|
|
124
|
+
const surface = this._newGeneration(w, h, gen ? gen.linear : false);
|
|
110
125
|
this._retire(gen);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
return surface;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Destroy a generation's surface, unbinding it first if it is the one
|
|
131
|
+
* current on the connection.
|
|
132
|
+
*
|
|
133
|
+
* The currency is the connection's (`app._glCurrentSurface`, which the
|
|
134
|
+
* context sets whenever it binds): every direct surface on it shares one
|
|
135
|
+
* GPU context, and exactly one surface is current. Unbinding leaves no
|
|
136
|
+
* surface current and says so (`app._glCurrent`), so the next GL call of
|
|
137
|
+
* whichever context draws next binds its own, as it would after another
|
|
138
|
+
* surface stole currency.
|
|
139
|
+
*/
|
|
140
|
+
_destroySurface(surface) {
|
|
141
|
+
const app = this.window.app;
|
|
142
|
+
if (app && app._glCurrentSurface === surface) {
|
|
143
|
+
try {
|
|
144
|
+
this.gpu.makeCurrent(null);
|
|
145
|
+
} catch {
|
|
146
|
+
// nothing is current now either way
|
|
147
|
+
}
|
|
148
|
+
app._glCurrentSurface = null;
|
|
149
|
+
app._glCurrent = null;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
surface.destroy();
|
|
153
|
+
} catch {
|
|
154
|
+
// already gone
|
|
155
|
+
}
|
|
114
156
|
}
|
|
115
157
|
|
|
116
158
|
/** A GBM surface at this size, in the layout asked for. */
|
|
@@ -384,14 +426,20 @@ export class GLSwapchain {
|
|
|
384
426
|
_fail(err) {
|
|
385
427
|
if (this.error) return;
|
|
386
428
|
this.error = err;
|
|
387
|
-
|
|
429
|
+
// Before validation the failure is `ready`'s rejection. After it — a
|
|
430
|
+
// swap refused mid-life — nothing is waiting on `ready` any more, and a
|
|
431
|
+
// chain that only recorded the error left its window showing its last
|
|
432
|
+
// frame, or nothing, with no word to anyone.
|
|
433
|
+
if (this._validated) this.onError?.(err);
|
|
434
|
+
else this._validate(err);
|
|
388
435
|
}
|
|
389
436
|
|
|
390
437
|
_retire(gen) {
|
|
391
438
|
if (!gen || gen.retired) return;
|
|
392
439
|
gen.retired = true;
|
|
393
440
|
this.retired.push(gen);
|
|
394
|
-
|
|
441
|
+
// `surfaceFor` retires the old generation after making the new one
|
|
442
|
+
if (this.generation === gen) this.generation = null;
|
|
395
443
|
this._sweep(gen);
|
|
396
444
|
}
|
|
397
445
|
|
|
@@ -409,11 +457,7 @@ export class GLSwapchain {
|
|
|
409
457
|
if (gen.pixmaps.size || this.importing) return;
|
|
410
458
|
const index = this.retired.indexOf(gen);
|
|
411
459
|
if (index >= 0) this.retired.splice(index, 1);
|
|
412
|
-
|
|
413
|
-
gen.surface.destroy();
|
|
414
|
-
} catch {
|
|
415
|
-
// already gone
|
|
416
|
-
}
|
|
460
|
+
this._destroySurface(gen.surface);
|
|
417
461
|
}
|
|
418
462
|
|
|
419
463
|
_freePixmap(pixmap) {
|
|
@@ -429,14 +473,11 @@ export class GLSwapchain {
|
|
|
429
473
|
this.destroyed = true;
|
|
430
474
|
this.onReady = null;
|
|
431
475
|
this.onValidated = null;
|
|
476
|
+
this.onError = null;
|
|
432
477
|
for (const gen of this._generations()) {
|
|
433
478
|
for (const entry of gen.pixmaps.values()) this._freePixmap(entry.pixmap);
|
|
434
479
|
gen.pixmaps.clear();
|
|
435
|
-
|
|
436
|
-
gen.surface.destroy();
|
|
437
|
-
} catch {
|
|
438
|
-
// already gone
|
|
439
|
-
}
|
|
480
|
+
this._destroySurface(gen.surface);
|
|
440
481
|
}
|
|
441
482
|
this.generation = null;
|
|
442
483
|
this.retired.length = 0;
|
|
@@ -416,7 +416,24 @@ does not make it retroactive. Either:
|
|
|
416
416
|
_fail(err) {
|
|
417
417
|
if (this.error) return;
|
|
418
418
|
this.error = err;
|
|
419
|
-
this
|
|
419
|
+
// before `ready` settled this is its rejection; after, the one word the
|
|
420
|
+
// failure gets is `onError` (the GLES flavor's contract)
|
|
421
|
+
if (this._settleReady) this._settleReady(err);
|
|
422
|
+
else this._errorHandler?.(err);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Called once, with the coded error `error` then holds, when the context
|
|
427
|
+
* fails after `ready` resolved — a flush refused, or a surface that could
|
|
428
|
+
* not be exported again after the window came back. A failure before that
|
|
429
|
+
* is `ready`'s rejection, and only that.
|
|
430
|
+
*/
|
|
431
|
+
set onError(fn) {
|
|
432
|
+
this._errorHandler = fn;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
get onError() {
|
|
436
|
+
return this._errorHandler;
|
|
420
437
|
}
|
|
421
438
|
|
|
422
439
|
destroy() {
|
|
@@ -167,6 +167,12 @@ does not make it retroactive. Either:
|
|
|
167
167
|
this.ready.catch(() => {});
|
|
168
168
|
this.swapchain.onReady = () => this._onFrameAvailable();
|
|
169
169
|
this._frameWanted = null;
|
|
170
|
+
// …and a failure after `ready` resolved, which nothing awaits any more
|
|
171
|
+
this._errorHandler = null;
|
|
172
|
+
this.swapchain.onError = (err) => {
|
|
173
|
+
this.error = err;
|
|
174
|
+
this._errorHandler?.(err);
|
|
175
|
+
};
|
|
170
176
|
|
|
171
177
|
// GL entry points and constants, bound so that whichever surface this
|
|
172
178
|
// context owns is the current one when they run
|
|
@@ -208,24 +214,28 @@ does not make it retroactive. Either:
|
|
|
208
214
|
*/
|
|
209
215
|
makeCurrent() {
|
|
210
216
|
if (this.error || this._destroyed || this.window._destroyed) return this;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this._width = width;
|
|
215
|
-
this._height = height;
|
|
216
|
-
this._surface = null; // a new size is a new generation
|
|
217
|
-
}
|
|
217
|
+
// a new size is a new generation, which `_bind` asks the chain for
|
|
218
|
+
this._width = this.window.width;
|
|
219
|
+
this._height = this.window.height;
|
|
218
220
|
this._bind();
|
|
219
221
|
return this;
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
_bind() {
|
|
223
225
|
if (this.error || this._destroyed) return;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// Asked of the chain every time rather than cached here: the chain also
|
|
227
|
+
// replaces its generation on its own — the linear retry after the server
|
|
228
|
+
// refuses a buffer — and a surface cached across that is one the chain
|
|
229
|
+
// has destroyed. At the same size it is a comparison and a return.
|
|
230
|
+
this._surface = this.swapchain.surfaceFor(
|
|
231
|
+
this._width ?? this.window.width,
|
|
232
|
+
this._height ?? this.window.height
|
|
233
|
+
);
|
|
227
234
|
this.gpu.makeCurrent(this._surface);
|
|
228
235
|
this.app._glCurrent = this;
|
|
236
|
+
// which surface is current, for the chain: it never destroys that one
|
|
237
|
+
// without unbinding it first (GLSwapchain#_destroySurface)
|
|
238
|
+
this.app._glCurrentSurface = this._surface;
|
|
229
239
|
}
|
|
230
240
|
|
|
231
241
|
/**
|
|
@@ -252,6 +262,20 @@ does not make it retroactive. Either:
|
|
|
252
262
|
this._frameWanted?.();
|
|
253
263
|
}
|
|
254
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Called once, with the coded error `error` then holds, when the context
|
|
267
|
+
* fails after `ready` resolved — the driver refusing a swap mid-life. A
|
|
268
|
+
* failure before that is `ready`'s rejection, and only that. Either way
|
|
269
|
+
* the context draws nothing more, and a caller with a fallback shows it.
|
|
270
|
+
*/
|
|
271
|
+
set onError(fn) {
|
|
272
|
+
this._errorHandler = fn;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get onError() {
|
|
276
|
+
return this._errorHandler;
|
|
277
|
+
}
|
|
278
|
+
|
|
255
279
|
/**
|
|
256
280
|
* Show the frame just drawn.
|
|
257
281
|
*
|
|
@@ -266,9 +290,8 @@ does not make it retroactive. Either:
|
|
|
266
290
|
// A resize seen only now still gets picked up: the next frame binds a
|
|
267
291
|
// generation at the new size. Checked after the swap so the frame that was
|
|
268
292
|
// drawn at the old size is the one that goes out.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
293
|
+
this._width = this.window.width;
|
|
294
|
+
this._height = this.window.height;
|
|
272
295
|
return sent;
|
|
273
296
|
}
|
|
274
297
|
|
|
@@ -288,10 +311,14 @@ does not make it retroactive. Either:
|
|
|
288
311
|
destroy() {
|
|
289
312
|
if (this._destroyed) return;
|
|
290
313
|
this._destroyed = true;
|
|
314
|
+
this._errorHandler = null;
|
|
315
|
+
// unbinds this context's surface first where it is the current one
|
|
316
|
+
// (GLSwapchain#_destroySurface)
|
|
291
317
|
this.swapchain.destroy();
|
|
292
318
|
this.window._setGenericEventSink(0, null);
|
|
293
319
|
if (this.app._glCurrent === this) {
|
|
294
320
|
this.app._glCurrent = null;
|
|
321
|
+
this.app._glCurrentSurface = null;
|
|
295
322
|
try {
|
|
296
323
|
this.gpu.makeCurrent(null);
|
|
297
324
|
} catch {
|
package/lib/window.js
CHANGED
|
@@ -1560,6 +1560,7 @@ export default class Window extends Drawable {
|
|
|
1560
1560
|
|
|
1561
1561
|
_runFrame() {
|
|
1562
1562
|
const f = this._frame;
|
|
1563
|
+
const startedAt = performance.now();
|
|
1563
1564
|
if (
|
|
1564
1565
|
!f.pending.size &&
|
|
1565
1566
|
!f.rafCbs.length &&
|
|
@@ -1601,7 +1602,7 @@ export default class Window extends Drawable {
|
|
|
1601
1602
|
// that drew nothing has to fall back to the timer, or nothing would ever
|
|
1602
1603
|
// run the next frame.
|
|
1603
1604
|
if (!this._clockOnPresent()) this._armFence();
|
|
1604
|
-
this._armTimer();
|
|
1605
|
+
this._armTimer(startedAt);
|
|
1605
1606
|
}
|
|
1606
1607
|
|
|
1607
1608
|
/**
|
|
@@ -2156,8 +2157,14 @@ export default class Window extends Drawable {
|
|
|
2156
2157
|
* and arming one would only cap the rate the display just set. An explicit
|
|
2157
2158
|
* `frameInterval` is honoured in both, since a cap the caller asked for
|
|
2158
2159
|
* applies whatever ends the frame.
|
|
2160
|
+
*
|
|
2161
|
+
* The period runs from the start of the frame (`startedAt`), not from its
|
|
2162
|
+
* end: a frame's own work is part of its period, not a pause owed after it.
|
|
2163
|
+
* Counted from the end, a window whose frames took 3 ms drew one every 19
|
|
2164
|
+
* under a 16.7 ms interval — a GL map panned by an animation, whose window
|
|
2165
|
+
* is on the fence, ran at 51 frames a second on a 60 Hz panel.
|
|
2159
2166
|
*/
|
|
2160
|
-
_armTimer() {
|
|
2167
|
+
_armTimer(startedAt = performance.now()) {
|
|
2161
2168
|
const f = this._frame;
|
|
2162
2169
|
if (f.timer) return;
|
|
2163
2170
|
let period = this.frameInterval;
|
|
@@ -2169,7 +2176,7 @@ export default class Window extends Drawable {
|
|
|
2169
2176
|
f.timer = setTimeout(() => {
|
|
2170
2177
|
f.timer = null;
|
|
2171
2178
|
if (f.pending.size || f.rafCbs.length || f.needsRedraw) this._scheduleFrame();
|
|
2172
|
-
}, period);
|
|
2179
|
+
}, Math.max(0, period - (performance.now() - startedAt)));
|
|
2173
2180
|
if (typeof f.timer.unref === 'function') f.timer.unref();
|
|
2174
2181
|
}
|
|
2175
2182
|
|