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.
@@ -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
- // Two rules shape the code. **A buffer belongs to the server until IdleNotify
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. And
18
- // **a pixmap must exist before it is presented**: a new buffer's import is
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
- // a generation that had to go linear keeps that layout: whatever refused
112
- // the tiled buffer — the server, or EGL itself — has not changed since
113
- return this._newGeneration(w, h, gen ? gen.linear : false);
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
- this._validate(err);
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
- this.generation = null;
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
- try {
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
- try {
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._settleReady?.(err);
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
- const width = this.window.width;
212
- const height = this.window.height;
213
- if (width !== this._width || height !== this._height) {
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
- if (!this._surface) {
225
- this._surface = this.swapchain.surfaceFor(this._width ?? this.window.width, this._height ?? this.window.height);
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
- if (this.window.width !== this._width || this.window.height !== this._height) {
270
- this._surface = null;
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.12.10",
3
+ "version": "8.12.12",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",