@perspective-dev/viewer-charts 4.5.1 → 4.5.2
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/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/plugin.d.ts +15 -2
- package/dist/esm/render/scheduler.d.ts +23 -0
- package/dist/esm/webgl/context-manager.d.ts +100 -117
- package/dist/esm/webgl/context-pool.d.ts +40 -0
- package/dist/esm/webgl/gl-context.d.ts +54 -0
- package/package.json +1 -1
- package/src/ts/config.ts +21 -1
- package/src/ts/plugin/plugin.ts +76 -12
- package/src/ts/render/scheduler.ts +125 -42
- package/src/ts/webgl/context-manager.ts +195 -189
- package/src/ts/webgl/context-pool.ts +78 -0
- package/src/ts/webgl/gl-context.ts +191 -0
- package/src/ts/worker/renderer.worker.ts +62 -19
- package/src/ts/worker/session-host.ts +29 -0
|
@@ -177,6 +177,41 @@ export function deferIfDraining(
|
|
|
177
177
|
ops.push(op);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Drop every scheduler reference to `glManager` — called from
|
|
182
|
+
* `WebGLContextManager`'s owning renderer at teardown, *before*
|
|
183
|
+
* `glManager.destroy()` loses the GL context.
|
|
184
|
+
*
|
|
185
|
+
* Without this, a frame queued for the next RAF (an `Entry` in
|
|
186
|
+
* `pending`) or a `deferIfDraining` op still parked in `deferred`
|
|
187
|
+
* would survive the destroy and, on the next `drain()`, drive
|
|
188
|
+
* `fullRender` / `present` against a context-lost manager. In blit
|
|
189
|
+
* mode that surfaces as `endFrame`'s `transferToImageBitmap` throwing
|
|
190
|
+
* "Cannot transfer to ImageBitmap because WebGL context is lost" —
|
|
191
|
+
* logged as "scheduler: present failed". In direct mode it's a wasted
|
|
192
|
+
* paint against a dead context.
|
|
193
|
+
*
|
|
194
|
+
* Outstanding waiters for a destroyed manager are resolved (not
|
|
195
|
+
* rejected): the caller asked to tear the chart down, so its awaited
|
|
196
|
+
* `draw()` should observe a clean no-op rather than an error it would
|
|
197
|
+
* have to suppress. An in-flight `present()` (manager in `inFlight`)
|
|
198
|
+
* is left to unwind on its own — its `finally` clause will not find a
|
|
199
|
+
* `deferred` entry to flush, and the manager is already gone from
|
|
200
|
+
* `pending`, so it cannot re-enqueue.
|
|
201
|
+
*/
|
|
202
|
+
export function unregister(glManager: WebGLContextManager): void {
|
|
203
|
+
const entry = pending.get(glManager);
|
|
204
|
+
if (entry) {
|
|
205
|
+
pending.delete(glManager);
|
|
206
|
+
for (const w of entry.waiters) {
|
|
207
|
+
w.resolve();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
deferred.delete(glManager);
|
|
212
|
+
inFlight.delete(glManager);
|
|
213
|
+
}
|
|
214
|
+
|
|
180
215
|
/**
|
|
181
216
|
* Test-only: clear pending state. Production callers must not use
|
|
182
217
|
* this — outstanding waiters are silently dropped.
|
|
@@ -230,31 +265,39 @@ export function _resetForTest(): void {
|
|
|
230
265
|
async function drain(): Promise<void> {
|
|
231
266
|
const snapshot = Array.from(pending.values());
|
|
232
267
|
pending.clear();
|
|
233
|
-
|
|
268
|
+
|
|
269
|
+
// Group ready entries by the GL context behind each manager. Under
|
|
270
|
+
// pooled blit mode many managers share one context (one drawing
|
|
271
|
+
// buffer): their renders MUST serialize — interleaving two charts'
|
|
272
|
+
// paints into one canvas would corrupt the bitmap the first ships.
|
|
273
|
+
// In the default 1:1 mode every manager has its own backend, so
|
|
274
|
+
// every group has exactly one entry and all groups run in parallel,
|
|
275
|
+
// exactly as before pooling.
|
|
276
|
+
const groups = new Map<number, Entry[]>();
|
|
234
277
|
for (const entry of snapshot) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// browser's compositor only ever observes the canvas
|
|
242
|
-
// post-paint. In direct/in-process modes the visible
|
|
243
|
-
// canvas IS the GL canvas, and a clear-without-matching-
|
|
244
|
-
// paint in the previous task would otherwise present an
|
|
245
|
-
// empty frame to the user.
|
|
246
|
-
entry.glManager.applyPendingResize();
|
|
247
|
-
entry.fullRender();
|
|
248
|
-
ready.push(entry);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
console.error("scheduler: fullRender threw", err);
|
|
278
|
+
// The browser force-loses the oldest context when a page
|
|
279
|
+
// exceeds its per-agent WebGL context cap (~16). A frame queued
|
|
280
|
+
// before that eviction would paint + present against a dead
|
|
281
|
+
// context; skip it and settle its waiters cleanly rather than
|
|
282
|
+
// letting `endFrame`'s `transferToImageBitmap` throw.
|
|
283
|
+
if (entry.glManager.isContextLost()) {
|
|
251
284
|
for (const w of entry.waiters) {
|
|
252
|
-
w.
|
|
285
|
+
w.resolve();
|
|
253
286
|
}
|
|
287
|
+
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const key = entry.glManager.backendId;
|
|
292
|
+
const group = groups.get(key);
|
|
293
|
+
if (group) {
|
|
294
|
+
group.push(entry);
|
|
295
|
+
} else {
|
|
296
|
+
groups.set(key, [entry]);
|
|
254
297
|
}
|
|
255
298
|
}
|
|
256
299
|
|
|
257
|
-
await Promise.all(
|
|
300
|
+
await Promise.all(Array.from(groups.values()).map(presentGroup));
|
|
258
301
|
|
|
259
302
|
// Now (and only now) clear rafId. If new requests landed during
|
|
260
303
|
// this drain, schedule the next RAF.
|
|
@@ -264,16 +307,51 @@ async function drain(): Promise<void> {
|
|
|
264
307
|
}
|
|
265
308
|
}
|
|
266
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Render every entry sharing one GL context, sequentially: each chart
|
|
312
|
+
* gets the shared drawing buffer to itself for a full
|
|
313
|
+
* `beginFrame` → `fullRender` → `awaitGpuFence` → `endFrame` (present)
|
|
314
|
+
* cycle before the next chart touches it. Groups for *different*
|
|
315
|
+
* contexts run concurrently (via `Promise.all` in `drain`), so K pooled
|
|
316
|
+
* contexts give K-way parallelism and the 1:1 default keeps full
|
|
317
|
+
* cross-chart overlap.
|
|
318
|
+
*
|
|
319
|
+
* The synchronous prefix of each group (`beginFrame` + `fullRender`)
|
|
320
|
+
* runs before its first `awaitGpuFence` yields, so when groups run
|
|
321
|
+
* concurrently all first-chart draws are still submitted before any
|
|
322
|
+
* fence wait — preserving the GPU overlap the old two-phase drain had.
|
|
323
|
+
*/
|
|
324
|
+
async function presentGroup(entries: Entry[]): Promise<void> {
|
|
325
|
+
for (const entry of entries) {
|
|
326
|
+
await present(entry);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
267
330
|
async function present(entry: Entry): Promise<void> {
|
|
268
|
-
// Mark this glManager as in-flight *synchronously*, before the
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// any fence-wait yields and before any sibling message handler
|
|
273
|
-
// can run. Mutations posted by sibling handlers (resize, clear)
|
|
274
|
-
// route through `deferIfDraining` and queue into `deferred`
|
|
275
|
-
// until the `finally` block flushes them.
|
|
331
|
+
// Mark this glManager as in-flight *synchronously*, before the first
|
|
332
|
+
// await, so sibling message handlers' canvas mutations (resize,
|
|
333
|
+
// clear) route through `deferIfDraining` into `deferred` until the
|
|
334
|
+
// `finally` flushes them.
|
|
276
335
|
inFlight.add(entry.glManager);
|
|
336
|
+
try {
|
|
337
|
+
// Apply the dimension change and (in pooled mode) reset shared
|
|
338
|
+
// GL state in the same un-yielded step as the paint that fills
|
|
339
|
+
// the buffer. Pairing the canvas-clearing `canvas.width = N`
|
|
340
|
+
// with `_fullRender` keeps the compositor from ever observing a
|
|
341
|
+
// cleared-but-unpainted canvas (visible flicker in direct/in-
|
|
342
|
+
// process modes, where the visible canvas IS the GL canvas).
|
|
343
|
+
entry.glManager.beginFrame();
|
|
344
|
+
entry.fullRender();
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error("scheduler: fullRender threw", err);
|
|
347
|
+
for (const w of entry.waiters) {
|
|
348
|
+
w.reject(err);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
flushDeferred(entry.glManager);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
277
355
|
try {
|
|
278
356
|
await entry.glManager.awaitGpuFence();
|
|
279
357
|
entry.glManager.endFrame();
|
|
@@ -296,21 +374,26 @@ async function present(entry: Entry): Promise<void> {
|
|
|
296
374
|
w.reject(err);
|
|
297
375
|
}
|
|
298
376
|
} finally {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
377
|
+
flushDeferred(entry.glManager);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Re-open a glManager's canvas to mutations and flush any ops deferred
|
|
383
|
+
* during its present, in arrival order. Deferred ops may call
|
|
384
|
+
* `requestRender`; the resulting entry queues into `pending` and the
|
|
385
|
+
* drain's tail check picks it up for the next RAF.
|
|
386
|
+
*/
|
|
387
|
+
function flushDeferred(glManager: WebGLContextManager): void {
|
|
388
|
+
inFlight.delete(glManager);
|
|
389
|
+
const ops = deferred.get(glManager);
|
|
390
|
+
if (ops) {
|
|
391
|
+
deferred.delete(glManager);
|
|
392
|
+
for (const op of ops) {
|
|
393
|
+
try {
|
|
394
|
+
op();
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.error("scheduler: deferred op threw", err);
|
|
314
397
|
}
|
|
315
398
|
}
|
|
316
399
|
}
|