ntk 8.12.10 → 8.12.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.12.10",
3
+ "version": "8.12.11",
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",