@perspective-dev/viewer-charts 4.5.1 → 5.0.0
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/charts/candlestick/candlestick.d.ts +8 -10
- package/dist/esm/charts/chart-base.d.ts +58 -4
- package/dist/esm/charts/chart.d.ts +45 -9
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +5 -2
- package/dist/esm/charts/series/series-build.d.ts +11 -0
- package/dist/esm/charts/series/series-interact.d.ts +10 -0
- package/dist/esm/charts/series/series-render.d.ts +17 -1
- package/dist/esm/charts/series/series.d.ts +53 -10
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/data/view-reader.d.ts +10 -1
- package/dist/esm/event-detail.d.ts +7 -0
- package/dist/esm/layout/ticks.d.ts +5 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/charts.d.ts +6 -3
- package/dist/esm/plugin/plugin.d.ts +46 -4
- package/dist/esm/render/scheduler.d.ts +34 -8
- package/dist/esm/transport/protocol.d.ts +31 -2
- package/dist/esm/transport/renderer-transport.d.ts +74 -3
- 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/dist/esm/webgl/instanced-attrs.d.ts +7 -1
- package/dist/esm/worker/renderer.worker.d.ts +12 -0
- package/package.json +1 -1
- package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
- package/src/ts/charts/candlestick/candlestick.ts +12 -17
- package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
- package/src/ts/charts/chart-base.ts +85 -5
- package/src/ts/charts/chart.ts +47 -9
- package/src/ts/charts/common/category-axis-resolver.ts +5 -1
- package/src/ts/charts/common/tree-data.ts +61 -16
- package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
- package/src/ts/charts/series/glyphs/draw-areas.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
- package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
- package/src/ts/charts/series/series-build.ts +21 -3
- package/src/ts/charts/series/series-interact.ts +86 -13
- package/src/ts/charts/series/series-render.ts +660 -17
- package/src/ts/charts/series/series.ts +96 -26
- package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
- package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
- package/src/ts/charts/treemap/treemap-interact.ts +2 -5
- package/src/ts/charts/treemap/treemap-render.ts +3 -1
- package/src/ts/charts/treemap/treemap.ts +6 -1
- package/src/ts/config.ts +21 -1
- package/src/ts/data/view-reader.ts +22 -2
- package/src/ts/event-detail.ts +8 -0
- package/src/ts/index.ts +13 -7
- package/src/ts/interaction/raw-event-forwarder.ts +7 -0
- package/src/ts/interaction/zoom-controller.ts +8 -0
- package/src/ts/layout/ticks.ts +9 -0
- package/src/ts/plugin/charts.ts +20 -4
- package/src/ts/plugin/plugin.ts +157 -23
- package/src/ts/render/scheduler.ts +164 -55
- package/src/ts/transport/protocol.ts +33 -0
- package/src/ts/transport/renderer-transport.ts +185 -9
- 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/webgl/instanced-attrs.ts +15 -1
- package/src/ts/worker/dispatch.ts +4 -1
- package/src/ts/worker/renderer.worker.ts +94 -22
- package/src/ts/worker/session-host.ts +29 -0
package/src/ts/plugin/plugin.ts
CHANGED
|
@@ -125,6 +125,22 @@ const GLOBAL_STYLES = (() => {
|
|
|
125
125
|
return [sheet];
|
|
126
126
|
})();
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Process-global GL presentation strategy, shared by *every* chart-type
|
|
130
|
+
* plugin in this renderer. Seeded from the build-time {@link RENDER_BLIT_MODE}
|
|
131
|
+
* and overridable at runtime via
|
|
132
|
+
* {@link HTMLPerspectiveViewerWebGLPluginElement.setBlitMode}.
|
|
133
|
+
*
|
|
134
|
+
* Module scope (not per-instance) on purpose: blit-vs-direct is a
|
|
135
|
+
* whole-renderer decision — in `"blit"` mode the worker shares a pool of
|
|
136
|
+
* GL contexts across all charts ([webgl/context-pool.ts]), so a page
|
|
137
|
+
* can't sensibly mix strategies per chart. Every per-chart-type subclass
|
|
138
|
+
* registered in [index.ts] reads this one value when it builds its
|
|
139
|
+
* renderer, so setting it once (before the first chart renders) applies
|
|
140
|
+
* to all of them.
|
|
141
|
+
*/
|
|
142
|
+
let BLIT_MODE: "direct" | "blit" = RENDER_BLIT_MODE;
|
|
143
|
+
|
|
128
144
|
export class HTMLPerspectiveViewerWebGLPluginElement
|
|
129
145
|
extends HTMLElement
|
|
130
146
|
implements IPerspectiveViewerPlugin
|
|
@@ -138,8 +154,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
138
154
|
private _renderer: RendererTransport | null = null;
|
|
139
155
|
private _rendererPromise: Promise<RendererTransport> | null = null;
|
|
140
156
|
private _rawEventForwarder: RawEventForwarder | null = null;
|
|
141
|
-
private _generation = 0;
|
|
142
|
-
private _renderBlitMode: "direct" | "blit" = RENDER_BLIT_MODE;
|
|
143
157
|
private _resetClickAbort: AbortController | null = null;
|
|
144
158
|
|
|
145
159
|
/**
|
|
@@ -257,13 +271,40 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
257
271
|
return this._rendererPromise;
|
|
258
272
|
}
|
|
259
273
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
274
|
+
// `_buildRenderer` is async — it awaits `getClient`, `getTable`,
|
|
275
|
+
// and a full worker handshake — so a `disconnectedCallback`
|
|
276
|
+
// (toggle / chart-type switch) frequently lands *before* this
|
|
277
|
+
// `.then` runs. At that point `this._renderer` is still null,
|
|
278
|
+
// so `delete()`'s `if (this._renderer)` teardown is skipped and
|
|
279
|
+
// the freshly-built transport (and its WebGL context) would be
|
|
280
|
+
// assigned to a detached element and leak — one context per
|
|
281
|
+
// raced toggle, until the browser evicts the oldest.
|
|
282
|
+
//
|
|
283
|
+
// `delete()` sets `_rendererPromise = null`, so promise identity
|
|
284
|
+
// is the dispose signal: if `this._rendererPromise` no longer
|
|
285
|
+
// points at *this* build when it resolves, the element was
|
|
286
|
+
// deleted (or a reconnect started a newer build) and we destroy
|
|
287
|
+
// the orphan instead of adopting it. Promise identity is the
|
|
288
|
+
// right token: a rapid draw→update must NOT tear down the
|
|
289
|
+
// single in-flight build it shares via this memoized promise.
|
|
290
|
+
const p: Promise<RendererTransport> = this._buildRenderer(view).then(
|
|
291
|
+
(r) => {
|
|
292
|
+
if (this._rendererPromise !== p) {
|
|
293
|
+
r.destroy();
|
|
294
|
+
throw new Error("renderer disposed during init");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this._renderer = r;
|
|
298
|
+
this._setupInteraction(r);
|
|
299
|
+
return r;
|
|
300
|
+
},
|
|
301
|
+
);
|
|
265
302
|
|
|
266
|
-
|
|
303
|
+
// Swallow the dispose rejection so it doesn't surface as an
|
|
304
|
+
// unhandled rejection; `_drawImpl` catches it and bails.
|
|
305
|
+
p.catch(() => {});
|
|
306
|
+
this._rendererPromise = p;
|
|
307
|
+
return p;
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
/**
|
|
@@ -310,11 +351,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
310
351
|
|
|
311
352
|
private async _buildRenderer(view: View): Promise<RendererTransport> {
|
|
312
353
|
const viewer = this.parentElement as HTMLPerspectiveViewerElement;
|
|
313
|
-
|
|
354
|
+
// This chart's own panel (its `slot`). Used to scope the client/table
|
|
355
|
+
// to THIS panel rather than the host's active/seed panel (the bare
|
|
356
|
+
// getClient()/getTable() resolve element-wide in a multi-panel viewer),
|
|
357
|
+
// and to tag the dispatched interaction events with their source panel.
|
|
358
|
+
const panel = this.getAttribute("slot") ?? undefined;
|
|
359
|
+
const client = await viewer.getClient({ panel });
|
|
314
360
|
const viewer_class = customElements.get("perspective-viewer");
|
|
315
361
|
const clientWasm = viewer_class.get_wasm_module();
|
|
316
362
|
const clientWorkerURL = viewer_class.get_worker_url();
|
|
317
|
-
const table = await viewer?.getTable?.();
|
|
363
|
+
const table = await viewer?.getTable?.({ panel });
|
|
318
364
|
const tableName: string | undefined = table
|
|
319
365
|
? await table.get_name()
|
|
320
366
|
: undefined;
|
|
@@ -327,6 +373,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
327
373
|
client,
|
|
328
374
|
view,
|
|
329
375
|
tableName,
|
|
376
|
+
panel,
|
|
330
377
|
clientWorkerURL,
|
|
331
378
|
clientWasm,
|
|
332
379
|
chartTag: this._chartType.tag,
|
|
@@ -350,15 +397,28 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
350
397
|
},
|
|
351
398
|
pluginConfig: this._pluginConfig,
|
|
352
399
|
defaultChartType: this._chartType.default_chart_type,
|
|
353
|
-
renderBlitMode:
|
|
400
|
+
renderBlitMode: BLIT_MODE,
|
|
354
401
|
});
|
|
355
402
|
|
|
356
403
|
return transport;
|
|
357
404
|
}
|
|
358
405
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Select the GL presentation strategy for *all* chart-type plugins
|
|
408
|
+
* in this renderer. Static + process-global: the value is shared by
|
|
409
|
+
* every per-chart-type subclass, so it must be set once before the
|
|
410
|
+
* charts that should use it build their renderers (a renderer reads
|
|
411
|
+
* {@link BLIT_MODE} at construction in `_buildRenderer`; charts
|
|
412
|
+
* already built keep their mode until torn down and rebuilt).
|
|
413
|
+
*
|
|
414
|
+
* - `"direct"` — each chart owns a GL context 1:1 with its visible
|
|
415
|
+
* canvas (lowest latency; bounded by the browser's ~16-context
|
|
416
|
+
* cap).
|
|
417
|
+
* - `"blit"` — charts render off-screen and share a pool of GL
|
|
418
|
+
* contexts ([webgl/context-pool.ts]), so a page can exceed the cap.
|
|
419
|
+
*/
|
|
420
|
+
static setBlitMode(mode: "direct" | "blit") {
|
|
421
|
+
BLIT_MODE = mode;
|
|
362
422
|
}
|
|
363
423
|
|
|
364
424
|
get_static_config(): PluginStaticConfig {
|
|
@@ -497,22 +557,36 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
497
557
|
return this._drawImpl(view);
|
|
498
558
|
}
|
|
499
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Shared body of `draw` / `update`. No re-entrancy guard: the host
|
|
562
|
+
* serializes every rendering call (`draw`, `update`, `render`,
|
|
563
|
+
* `resize`) and `delete` on its per-renderer draw lock, so this
|
|
564
|
+
* method never overlaps itself or a teardown. The one exception —
|
|
565
|
+
* an external DOM disconnect `delete()`ing the element mid-draw —
|
|
566
|
+
* is absorbed by `RendererTransport`, whose post-`destroy()`
|
|
567
|
+
* requests settle immediately instead of pending forever.
|
|
568
|
+
*/
|
|
500
569
|
private async _drawImpl(view: View): Promise<void> {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
570
|
+
let renderer: RendererTransport;
|
|
571
|
+
try {
|
|
572
|
+
renderer = await this._ensureRenderer(view);
|
|
573
|
+
} catch {
|
|
574
|
+
// Renderer was disposed mid-init (element disconnected
|
|
575
|
+
// during the async build) — nothing to draw.
|
|
504
576
|
return;
|
|
505
577
|
}
|
|
506
578
|
|
|
507
579
|
renderer.setView(view);
|
|
508
580
|
renderer.setBufferMaxCapacity(this._chartType.max_cells);
|
|
581
|
+
const panel = this.getAttribute("slot") ?? undefined;
|
|
509
582
|
const viewer = this
|
|
510
583
|
.parentElement as HTMLPerspectiveViewerElement | null;
|
|
511
|
-
|
|
512
|
-
if (
|
|
584
|
+
|
|
585
|
+
if (viewer === null) {
|
|
513
586
|
return;
|
|
514
587
|
}
|
|
515
588
|
|
|
589
|
+
const viewerConfig = await viewer.getViewConfig({ panel });
|
|
516
590
|
await renderer.loadAndRender({
|
|
517
591
|
viewerConfig: {
|
|
518
592
|
group_by: viewerConfig?.group_by ?? [],
|
|
@@ -524,17 +598,78 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
524
598
|
}
|
|
525
599
|
|
|
526
600
|
async clear(): Promise<void> {
|
|
527
|
-
this._generation++;
|
|
528
601
|
this._renderer?.clear();
|
|
529
602
|
}
|
|
530
603
|
|
|
531
604
|
async resize(): Promise<void> {
|
|
532
|
-
|
|
605
|
+
// Hidden (an unslotted tab-stack panel, a `display: none` host):
|
|
606
|
+
// the 0×0 rect a hidden element reports would resize the worker
|
|
607
|
+
// canvas to zero — CLEARING the retained frame, so the next
|
|
608
|
+
// activation repaints from blank (the two-stage tab-switch
|
|
609
|
+
// artifact). Skip instead and keep the last frame for an instant,
|
|
610
|
+
// single-blit reveal — mirroring the datagrid's guard. The host's
|
|
611
|
+
// activation nudge is also a `resize()`, so a hidden panel's nudge
|
|
612
|
+
// is free by the same check.
|
|
613
|
+
if (!this.isConnected || this.offsetParent == null) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// AWAITED to the resized frame's PRESENT (the worker's
|
|
618
|
+
// `resizeAck`) — the host's presize protocol style-overrides
|
|
619
|
+
// this element to its target box and holds the layout commit on
|
|
620
|
+
// this promise, so resolving at message-post would commit the
|
|
621
|
+
// settings-pane layout against the old-dimensions bitmap (the
|
|
622
|
+
// aspect-ratio warp the datagrid's synchronous resize never
|
|
623
|
+
// shows).
|
|
624
|
+
await this._renderer?.resize();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* OPTIONAL host presize protocol: render at the TARGET element box
|
|
629
|
+
* `(width, height)` — the box the host's pending layout commit will
|
|
630
|
+
* produce — holding the resulting frame offscreen, so nothing on
|
|
631
|
+
* screen changes during the round-trip. Resolves, once the resized
|
|
632
|
+
* frame is staged, to a present closure; the host calls it in the
|
|
633
|
+
* same task as the layout commit, landing geometry and pixels in
|
|
634
|
+
* one paint. Plugins without this method get the host's held
|
|
635
|
+
* style-override presize path instead.
|
|
636
|
+
*/
|
|
637
|
+
async presize(width: number, height: number): Promise<(() => void) | void> {
|
|
638
|
+
if (
|
|
639
|
+
!this.isConnected ||
|
|
640
|
+
this.offsetParent == null ||
|
|
641
|
+
!this._renderer ||
|
|
642
|
+
!this._glCanvas
|
|
643
|
+
) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Target GL-canvas box = its current box shifted by the element's
|
|
648
|
+
// box delta — the chrome between the element edge and the canvas
|
|
649
|
+
// is constant across a resize.
|
|
650
|
+
const hostRect = this.getBoundingClientRect();
|
|
651
|
+
const glRect = this._glCanvas.getBoundingClientRect();
|
|
652
|
+
return await this._renderer.presize(
|
|
653
|
+
Math.max(0, glRect.width + (width - hostRect.width)),
|
|
654
|
+
Math.max(0, glRect.height + (height - hostRect.height)),
|
|
655
|
+
);
|
|
533
656
|
}
|
|
534
657
|
|
|
535
658
|
restyle() {
|
|
536
659
|
this._renderer?.invalidateTheme();
|
|
537
|
-
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Clear any active selection state (pinned tooltip) WITHOUT emitting
|
|
664
|
+
* selection events — the host calls this when a global filter
|
|
665
|
+
* contributed by this panel's selection is removed from the global
|
|
666
|
+
* filter bar, so the pin can't outlive the filter it produced. The
|
|
667
|
+
* transport's `_lastInsertConfig` (remove-set memory) is deliberately
|
|
668
|
+
* RETAINED so a subsequent selection still replaces any leftover
|
|
669
|
+
* clauses rather than accumulating.
|
|
670
|
+
*/
|
|
671
|
+
async deselect(): Promise<void> {
|
|
672
|
+
this._renderer?.deselect();
|
|
538
673
|
}
|
|
539
674
|
|
|
540
675
|
async render(view: View): Promise<Blob> {
|
|
@@ -563,7 +698,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
563
698
|
}
|
|
564
699
|
|
|
565
700
|
delete() {
|
|
566
|
-
this._generation++;
|
|
567
701
|
if (this._rawEventForwarder) {
|
|
568
702
|
this._rawEventForwarder.detach();
|
|
569
703
|
this._rawEventForwarder = null;
|
|
@@ -16,8 +16,8 @@ import type { WebGLContextManager } from "../webgl/context-manager";
|
|
|
16
16
|
* Module-level render scheduler. The single entry point for driving a
|
|
17
17
|
* chart frame. Every render-triggering caller — upload chunks, zoom /
|
|
18
18
|
* pan, resize, theme invalidation, host-driven redraws — calls
|
|
19
|
-
* `requestRender(glManager, fullRender)` and awaits the
|
|
20
|
-
* promise.
|
|
19
|
+
* `requestRender(glManager, fullRender, render2D)` and awaits the
|
|
20
|
+
* returned promise.
|
|
21
21
|
*
|
|
22
22
|
* ## Guarantees
|
|
23
23
|
*
|
|
@@ -51,9 +51,12 @@ import type { WebGLContextManager } from "../webgl/context-manager";
|
|
|
51
51
|
* begins, letting per-context GPU work overlap.
|
|
52
52
|
*
|
|
53
53
|
* - **Phase 2 (parallel):** `Promise.all(snapshot.map(present))`
|
|
54
|
-
* where `present` does `await awaitGpuFence();
|
|
55
|
-
* resolve waiters`.
|
|
56
|
-
*
|
|
54
|
+
* where `present` does `await awaitGpuFence(); render2D();
|
|
55
|
+
* endFrame(); resolve waiters`. The `render2D()` step draws the 2D
|
|
56
|
+
* layers (gridlines + chrome) only after the GPU fence, so the 2D
|
|
57
|
+
* placeholder canvases don't present a frame ahead of the GL frame
|
|
58
|
+
* on resize. Each entry's waiters resolve as soon as its own
|
|
59
|
+
* present completes — independent of other entries.
|
|
57
60
|
*
|
|
58
61
|
* ## Failure modes
|
|
59
62
|
*
|
|
@@ -78,6 +81,15 @@ import type { WebGLContextManager } from "../webgl/context-manager";
|
|
|
78
81
|
interface Entry {
|
|
79
82
|
glManager: WebGLContextManager;
|
|
80
83
|
fullRender: () => void;
|
|
84
|
+
/**
|
|
85
|
+
* The chart's 2D-canvas draws (gridlines + chrome), split out of
|
|
86
|
+
* `fullRender` so they run *after* `awaitGpuFence` — see
|
|
87
|
+
* {@link present}. Deferring the 2D work past the GPU fence keeps
|
|
88
|
+
* the 2D placeholder canvases from pushing a resized frame to the
|
|
89
|
+
* compositor a frame ahead of the GL present (the GL/2D "distortion"
|
|
90
|
+
* on resize). Populated as captured closures during `fullRender`.
|
|
91
|
+
*/
|
|
92
|
+
render2D: () => void;
|
|
81
93
|
waiters: PromiseWithResolvers<void>[];
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -105,25 +117,30 @@ const inFlight = new Set<WebGLContextManager>();
|
|
|
105
117
|
const deferred = new Map<WebGLContextManager, (() => void)[]>();
|
|
106
118
|
|
|
107
119
|
/**
|
|
108
|
-
* Request a coalesced render of `glManager`
|
|
109
|
-
*
|
|
110
|
-
* Phase 2
|
|
120
|
+
* Request a coalesced render of `glManager`. `fullRender` submits the
|
|
121
|
+
* GL commands (Phase 1, synchronous); `render2D` draws the 2D layers
|
|
122
|
+
* (gridlines + chrome) and runs in Phase 2 *after* `awaitGpuFence`, so
|
|
123
|
+
* the 2D canvases don't present ahead of the GL frame. Returns a
|
|
124
|
+
* promise that resolves when this entry's Phase 2 (`awaitGpuFence` +
|
|
125
|
+
* `render2D` + `endFrame`) completes.
|
|
111
126
|
*
|
|
112
127
|
* If a request is already pending for the same glManager, the new
|
|
113
|
-
* call's `fullRender`
|
|
114
|
-
* wins; closures read chart state lazily so this is
|
|
115
|
-
* no-op, but keeps the
|
|
116
|
-
* resolves alongside the existing waiters.
|
|
128
|
+
* call's `fullRender` / `render2D` closures replace the prior ones
|
|
129
|
+
* (latest call wins; closures read chart state lazily so this is
|
|
130
|
+
* functionally a no-op, but keeps the closures fresh) and the returned
|
|
131
|
+
* promise resolves alongside the existing waiters.
|
|
117
132
|
*/
|
|
118
133
|
export function requestRender(
|
|
119
134
|
glManager: WebGLContextManager,
|
|
120
135
|
fullRender: () => void,
|
|
136
|
+
render2D: () => void,
|
|
121
137
|
): Promise<void> {
|
|
122
138
|
let entry = pending.get(glManager);
|
|
123
139
|
if (entry) {
|
|
124
140
|
entry.fullRender = fullRender;
|
|
141
|
+
entry.render2D = render2D;
|
|
125
142
|
} else {
|
|
126
|
-
entry = { glManager, fullRender, waiters: [] };
|
|
143
|
+
entry = { glManager, fullRender, render2D, waiters: [] };
|
|
127
144
|
pending.set(glManager, entry);
|
|
128
145
|
}
|
|
129
146
|
|
|
@@ -177,6 +194,41 @@ export function deferIfDraining(
|
|
|
177
194
|
ops.push(op);
|
|
178
195
|
}
|
|
179
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Drop every scheduler reference to `glManager` — called from
|
|
199
|
+
* `WebGLContextManager`'s owning renderer at teardown, *before*
|
|
200
|
+
* `glManager.destroy()` loses the GL context.
|
|
201
|
+
*
|
|
202
|
+
* Without this, a frame queued for the next RAF (an `Entry` in
|
|
203
|
+
* `pending`) or a `deferIfDraining` op still parked in `deferred`
|
|
204
|
+
* would survive the destroy and, on the next `drain()`, drive
|
|
205
|
+
* `fullRender` / `present` against a context-lost manager. In blit
|
|
206
|
+
* mode that surfaces as `endFrame`'s `transferToImageBitmap` throwing
|
|
207
|
+
* "Cannot transfer to ImageBitmap because WebGL context is lost" —
|
|
208
|
+
* logged as "scheduler: present failed". In direct mode it's a wasted
|
|
209
|
+
* paint against a dead context.
|
|
210
|
+
*
|
|
211
|
+
* Outstanding waiters for a destroyed manager are resolved (not
|
|
212
|
+
* rejected): the caller asked to tear the chart down, so its awaited
|
|
213
|
+
* `draw()` should observe a clean no-op rather than an error it would
|
|
214
|
+
* have to suppress. An in-flight `present()` (manager in `inFlight`)
|
|
215
|
+
* is left to unwind on its own — its `finally` clause will not find a
|
|
216
|
+
* `deferred` entry to flush, and the manager is already gone from
|
|
217
|
+
* `pending`, so it cannot re-enqueue.
|
|
218
|
+
*/
|
|
219
|
+
export function unregister(glManager: WebGLContextManager): void {
|
|
220
|
+
const entry = pending.get(glManager);
|
|
221
|
+
if (entry) {
|
|
222
|
+
pending.delete(glManager);
|
|
223
|
+
for (const w of entry.waiters) {
|
|
224
|
+
w.resolve();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
deferred.delete(glManager);
|
|
229
|
+
inFlight.delete(glManager);
|
|
230
|
+
}
|
|
231
|
+
|
|
180
232
|
/**
|
|
181
233
|
* Test-only: clear pending state. Production callers must not use
|
|
182
234
|
* this — outstanding waiters are silently dropped.
|
|
@@ -230,31 +282,39 @@ export function _resetForTest(): void {
|
|
|
230
282
|
async function drain(): Promise<void> {
|
|
231
283
|
const snapshot = Array.from(pending.values());
|
|
232
284
|
pending.clear();
|
|
233
|
-
|
|
285
|
+
|
|
286
|
+
// Group ready entries by the GL context behind each manager. Under
|
|
287
|
+
// pooled blit mode many managers share one context (one drawing
|
|
288
|
+
// buffer): their renders MUST serialize — interleaving two charts'
|
|
289
|
+
// paints into one canvas would corrupt the bitmap the first ships.
|
|
290
|
+
// In the default 1:1 mode every manager has its own backend, so
|
|
291
|
+
// every group has exactly one entry and all groups run in parallel,
|
|
292
|
+
// exactly as before pooling.
|
|
293
|
+
const groups = new Map<number, Entry[]>();
|
|
234
294
|
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);
|
|
295
|
+
// The browser force-loses the oldest context when a page
|
|
296
|
+
// exceeds its per-agent WebGL context cap (~16). A frame queued
|
|
297
|
+
// before that eviction would paint + present against a dead
|
|
298
|
+
// context; skip it and settle its waiters cleanly rather than
|
|
299
|
+
// letting `endFrame`'s `transferToImageBitmap` throw.
|
|
300
|
+
if (entry.glManager.isContextLost()) {
|
|
251
301
|
for (const w of entry.waiters) {
|
|
252
|
-
w.
|
|
302
|
+
w.resolve();
|
|
253
303
|
}
|
|
304
|
+
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const key = entry.glManager.backendId;
|
|
309
|
+
const group = groups.get(key);
|
|
310
|
+
if (group) {
|
|
311
|
+
group.push(entry);
|
|
312
|
+
} else {
|
|
313
|
+
groups.set(key, [entry]);
|
|
254
314
|
}
|
|
255
315
|
}
|
|
256
316
|
|
|
257
|
-
await Promise.all(
|
|
317
|
+
await Promise.all(Array.from(groups.values()).map(presentGroup));
|
|
258
318
|
|
|
259
319
|
// Now (and only now) clear rafId. If new requests landed during
|
|
260
320
|
// this drain, schedule the next RAF.
|
|
@@ -264,18 +324,62 @@ async function drain(): Promise<void> {
|
|
|
264
324
|
}
|
|
265
325
|
}
|
|
266
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Render every entry sharing one GL context, sequentially: each chart
|
|
329
|
+
* gets the shared drawing buffer to itself for a full
|
|
330
|
+
* `beginFrame` → `fullRender` → `awaitGpuFence` → `endFrame` (present)
|
|
331
|
+
* cycle before the next chart touches it. Groups for *different*
|
|
332
|
+
* contexts run concurrently (via `Promise.all` in `drain`), so K pooled
|
|
333
|
+
* contexts give K-way parallelism and the 1:1 default keeps full
|
|
334
|
+
* cross-chart overlap.
|
|
335
|
+
*
|
|
336
|
+
* The synchronous prefix of each group (`beginFrame` + `fullRender`)
|
|
337
|
+
* runs before its first `awaitGpuFence` yields, so when groups run
|
|
338
|
+
* concurrently all first-chart draws are still submitted before any
|
|
339
|
+
* fence wait — preserving the GPU overlap the old two-phase drain had.
|
|
340
|
+
*/
|
|
341
|
+
async function presentGroup(entries: Entry[]): Promise<void> {
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
await present(entry);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
267
347
|
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.
|
|
348
|
+
// Mark this glManager as in-flight *synchronously*, before the first
|
|
349
|
+
// await, so sibling message handlers' canvas mutations (resize,
|
|
350
|
+
// clear) route through `deferIfDraining` into `deferred` until the
|
|
351
|
+
// `finally` flushes them.
|
|
276
352
|
inFlight.add(entry.glManager);
|
|
353
|
+
try {
|
|
354
|
+
// Apply the dimension change and (in pooled mode) reset shared
|
|
355
|
+
// GL state in the same un-yielded step as the paint that fills
|
|
356
|
+
// the buffer. Pairing the canvas-clearing `canvas.width = N`
|
|
357
|
+
// with `_fullRender` keeps the compositor from ever observing a
|
|
358
|
+
// cleared-but-unpainted canvas (visible flicker in direct/in-
|
|
359
|
+
// process modes, where the visible canvas IS the GL canvas).
|
|
360
|
+
entry.glManager.beginFrame();
|
|
361
|
+
entry.fullRender();
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.error("scheduler: fullRender threw", err);
|
|
364
|
+
for (const w of entry.waiters) {
|
|
365
|
+
w.reject(err);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
flushDeferred(entry.glManager);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
277
372
|
try {
|
|
278
373
|
await entry.glManager.awaitGpuFence();
|
|
374
|
+
// Draw the 2D layers (gridlines + chrome) only now that the GL
|
|
375
|
+
// frame's GPU work has completed. Issuing the 2D-canvas commands
|
|
376
|
+
// here — rather than inline in `fullRender` (Phase 1) — keeps the
|
|
377
|
+
// 2D placeholder canvases from pushing their resized frame to the
|
|
378
|
+
// compositor during the fence-wait yields, a frame ahead of the
|
|
379
|
+
// GL present. That early push is the visible GL/2D misalignment
|
|
380
|
+
// on resize. Failures fall through to the same present-failed
|
|
381
|
+
// path as the fence.
|
|
382
|
+
entry.render2D();
|
|
279
383
|
entry.glManager.endFrame();
|
|
280
384
|
for (const w of entry.waiters) {
|
|
281
385
|
w.resolve();
|
|
@@ -296,21 +400,26 @@ async function present(entry: Entry): Promise<void> {
|
|
|
296
400
|
w.reject(err);
|
|
297
401
|
}
|
|
298
402
|
} finally {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
403
|
+
flushDeferred(entry.glManager);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Re-open a glManager's canvas to mutations and flush any ops deferred
|
|
409
|
+
* during its present, in arrival order. Deferred ops may call
|
|
410
|
+
* `requestRender`; the resulting entry queues into `pending` and the
|
|
411
|
+
* drain's tail check picks it up for the next RAF.
|
|
412
|
+
*/
|
|
413
|
+
function flushDeferred(glManager: WebGLContextManager): void {
|
|
414
|
+
inFlight.delete(glManager);
|
|
415
|
+
const ops = deferred.get(glManager);
|
|
416
|
+
if (ops) {
|
|
417
|
+
deferred.delete(glManager);
|
|
418
|
+
for (const op of ops) {
|
|
419
|
+
try {
|
|
420
|
+
op();
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error("scheduler: deferred op threw", err);
|
|
314
423
|
}
|
|
315
424
|
}
|
|
316
425
|
}
|
|
@@ -31,6 +31,7 @@ export type ControlMsg =
|
|
|
31
31
|
| SetBufferMaxCapacityMsg
|
|
32
32
|
| LoadAndRenderMsg
|
|
33
33
|
| RedrawMsg
|
|
34
|
+
| DeselectMsg
|
|
34
35
|
| ResizeMsg
|
|
35
36
|
| ClearMsg
|
|
36
37
|
| InvalidateThemeMsg
|
|
@@ -53,6 +54,7 @@ export type WorkerMsg =
|
|
|
53
54
|
| UserClickMsg
|
|
54
55
|
| UserSelectMsg
|
|
55
56
|
| LoadAndRenderAckMsg
|
|
57
|
+
| ResizeAckMsg
|
|
56
58
|
| FrameBitmapMsg
|
|
57
59
|
| ErrorMsg;
|
|
58
60
|
|
|
@@ -300,13 +302,44 @@ export interface RedrawMsg {
|
|
|
300
302
|
kind: "redraw";
|
|
301
303
|
}
|
|
302
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Host → worker: silently clear the chart's selection state (pinned
|
|
307
|
+
* tooltip) — no `userSelect` reply, so the host filter-bar removal that
|
|
308
|
+
* triggered it can't echo back into the host's filter set. The worker's
|
|
309
|
+
* `TooltipController.dismiss()` posts `dismissTooltip` to tear down the
|
|
310
|
+
* host-side pinned artifact.
|
|
311
|
+
*/
|
|
312
|
+
export interface DeselectMsg {
|
|
313
|
+
kind: "deselect";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Host → worker: resize the chart to a new CSS box. Replied with
|
|
318
|
+
* {@link ResizeAckMsg} AFTER the resized frame PRESENTS — the host's
|
|
319
|
+
* `plugin.resize()` promise is the viewer's presize contract ("the
|
|
320
|
+
* plugin has repainted at the target box when this resolves"), so an
|
|
321
|
+
* ack at message-receipt would let the settings-pane layout commit
|
|
322
|
+
* against the old-dimensions bitmap (the aspect-ratio warp).
|
|
323
|
+
*/
|
|
303
324
|
export interface ResizeMsg {
|
|
304
325
|
kind: "resize";
|
|
326
|
+
msgId: number;
|
|
305
327
|
cssWidth: number;
|
|
306
328
|
cssHeight: number;
|
|
307
329
|
dpr: number;
|
|
308
330
|
}
|
|
309
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Worker → host reply to a `ResizeMsg`, posted after the resized
|
|
334
|
+
* frame's present completes. Always sent — present failures and
|
|
335
|
+
* torn-down charts included — so the host's awaited promise resolves
|
|
336
|
+
* deterministically.
|
|
337
|
+
*/
|
|
338
|
+
export interface ResizeAckMsg {
|
|
339
|
+
kind: "resizeAck";
|
|
340
|
+
msgId: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
310
343
|
export interface ClearMsg {
|
|
311
344
|
kind: "clear";
|
|
312
345
|
}
|