@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.
Files changed (70) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +2 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  3. package/dist/esm/charts/candlestick/candlestick.d.ts +8 -10
  4. package/dist/esm/charts/chart-base.d.ts +58 -4
  5. package/dist/esm/charts/chart.d.ts +45 -9
  6. package/dist/esm/charts/series/glyphs/draw-areas.d.ts +3 -2
  7. package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
  8. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +3 -2
  9. package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +5 -2
  10. package/dist/esm/charts/series/series-build.d.ts +11 -0
  11. package/dist/esm/charts/series/series-interact.d.ts +10 -0
  12. package/dist/esm/charts/series/series-render.d.ts +17 -1
  13. package/dist/esm/charts/series/series.d.ts +53 -10
  14. package/dist/esm/config.d.ts +19 -0
  15. package/dist/esm/data/view-reader.d.ts +10 -1
  16. package/dist/esm/event-detail.d.ts +7 -0
  17. package/dist/esm/layout/ticks.d.ts +5 -0
  18. package/dist/esm/perspective-viewer-charts.js +2 -2
  19. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  20. package/dist/esm/plugin/charts.d.ts +6 -3
  21. package/dist/esm/plugin/plugin.d.ts +46 -4
  22. package/dist/esm/render/scheduler.d.ts +34 -8
  23. package/dist/esm/transport/protocol.d.ts +31 -2
  24. package/dist/esm/transport/renderer-transport.d.ts +74 -3
  25. package/dist/esm/webgl/context-manager.d.ts +100 -117
  26. package/dist/esm/webgl/context-pool.d.ts +40 -0
  27. package/dist/esm/webgl/gl-context.d.ts +54 -0
  28. package/dist/esm/webgl/instanced-attrs.d.ts +7 -1
  29. package/dist/esm/worker/renderer.worker.d.ts +12 -0
  30. package/package.json +1 -1
  31. package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
  32. package/src/ts/charts/candlestick/candlestick.ts +12 -17
  33. package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
  34. package/src/ts/charts/chart-base.ts +85 -5
  35. package/src/ts/charts/chart.ts +47 -9
  36. package/src/ts/charts/common/category-axis-resolver.ts +5 -1
  37. package/src/ts/charts/common/tree-data.ts +61 -16
  38. package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
  39. package/src/ts/charts/series/glyphs/draw-areas.ts +10 -1
  40. package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
  41. package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
  42. package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
  43. package/src/ts/charts/series/series-build.ts +21 -3
  44. package/src/ts/charts/series/series-interact.ts +86 -13
  45. package/src/ts/charts/series/series-render.ts +660 -17
  46. package/src/ts/charts/series/series.ts +96 -26
  47. package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
  48. package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
  49. package/src/ts/charts/treemap/treemap-interact.ts +2 -5
  50. package/src/ts/charts/treemap/treemap-render.ts +3 -1
  51. package/src/ts/charts/treemap/treemap.ts +6 -1
  52. package/src/ts/config.ts +21 -1
  53. package/src/ts/data/view-reader.ts +22 -2
  54. package/src/ts/event-detail.ts +8 -0
  55. package/src/ts/index.ts +13 -7
  56. package/src/ts/interaction/raw-event-forwarder.ts +7 -0
  57. package/src/ts/interaction/zoom-controller.ts +8 -0
  58. package/src/ts/layout/ticks.ts +9 -0
  59. package/src/ts/plugin/charts.ts +20 -4
  60. package/src/ts/plugin/plugin.ts +157 -23
  61. package/src/ts/render/scheduler.ts +164 -55
  62. package/src/ts/transport/protocol.ts +33 -0
  63. package/src/ts/transport/renderer-transport.ts +185 -9
  64. package/src/ts/webgl/context-manager.ts +195 -189
  65. package/src/ts/webgl/context-pool.ts +78 -0
  66. package/src/ts/webgl/gl-context.ts +191 -0
  67. package/src/ts/webgl/instanced-attrs.ts +15 -1
  68. package/src/ts/worker/dispatch.ts +4 -1
  69. package/src/ts/worker/renderer.worker.ts +94 -22
  70. package/src/ts/worker/session-host.ts +29 -0
@@ -30,9 +30,12 @@ export interface ChartTypeConfig {
30
30
  * Per-chart-type overrides for `DEFAULT_PLUGIN_CONFIG`. Used when a
31
31
  * field's sensible default differs by chart family — currently
32
32
  * `include_zero` (true for Y Bar / Y Area / X Bar, false for line
33
- * / scatter / cartesian / financial). Applied at schema generation
34
- * and at `restore({})` so the effective default matches the
35
- * surfaced UI default.
33
+ * / scatter / cartesian / financial) and `facet_mode` ("overlay"
34
+ * for the band-pipeline families — series / financial — whose
35
+ * historical split_by rendering is the single stacked/colored
36
+ * plot; "grid" elsewhere). Applied at schema generation and at
37
+ * `restore({})` so the effective default matches the surfaced UI
38
+ * default.
36
39
  */
37
40
  plugin_field_defaults?: Partial<PluginConfig>;
38
41
  }
@@ -10,8 +10,6 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
10
10
  private _renderer;
11
11
  private _rendererPromise;
12
12
  private _rawEventForwarder;
13
- private _generation;
14
- private _renderBlitMode;
15
13
  private _resetClickAbort;
16
14
  /**
17
15
  * Plugin-scoped global config. Seeded lazily from
@@ -64,7 +62,21 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
64
62
  */
65
63
  private _setupInteraction;
66
64
  private _buildRenderer;
67
- setBlitMode(mode: "direct" | "blit"): void;
65
+ /**
66
+ * Select the GL presentation strategy for *all* chart-type plugins
67
+ * in this renderer. Static + process-global: the value is shared by
68
+ * every per-chart-type subclass, so it must be set once before the
69
+ * charts that should use it build their renderers (a renderer reads
70
+ * {@link BLIT_MODE} at construction in `_buildRenderer`; charts
71
+ * already built keep their mode until torn down and rebuilt).
72
+ *
73
+ * - `"direct"` — each chart owns a GL context 1:1 with its visible
74
+ * canvas (lowest latency; bounded by the browser's ~16-context
75
+ * cap).
76
+ * - `"blit"` — charts render off-screen and share a pool of GL
77
+ * contexts ([webgl/context-pool.ts]), so a page can exceed the cap.
78
+ */
79
+ static setBlitMode(mode: "direct" | "blit"): void;
68
80
  get_static_config(): PluginStaticConfig;
69
81
  column_config_schema(column_type: string, _group: string | undefined, _column_name: string, current_value: Record<string, unknown> | null, _viewer_config?: {
70
82
  group_by?: string[];
@@ -84,10 +96,40 @@ export declare class HTMLPerspectiveViewerWebGLPluginElement extends HTMLElement
84
96
  };
85
97
  draw(view: View): Promise<void>;
86
98
  update(view: View): Promise<void>;
99
+ /**
100
+ * Shared body of `draw` / `update`. No re-entrancy guard: the host
101
+ * serializes every rendering call (`draw`, `update`, `render`,
102
+ * `resize`) and `delete` on its per-renderer draw lock, so this
103
+ * method never overlaps itself or a teardown. The one exception —
104
+ * an external DOM disconnect `delete()`ing the element mid-draw —
105
+ * is absorbed by `RendererTransport`, whose post-`destroy()`
106
+ * requests settle immediately instead of pending forever.
107
+ */
87
108
  private _drawImpl;
88
109
  clear(): Promise<void>;
89
110
  resize(): Promise<void>;
90
- restyle(): number;
111
+ /**
112
+ * OPTIONAL host presize protocol: render at the TARGET element box
113
+ * `(width, height)` — the box the host's pending layout commit will
114
+ * produce — holding the resulting frame offscreen, so nothing on
115
+ * screen changes during the round-trip. Resolves, once the resized
116
+ * frame is staged, to a present closure; the host calls it in the
117
+ * same task as the layout commit, landing geometry and pixels in
118
+ * one paint. Plugins without this method get the host's held
119
+ * style-override presize path instead.
120
+ */
121
+ presize(width: number, height: number): Promise<(() => void) | void>;
122
+ restyle(): void;
123
+ /**
124
+ * Clear any active selection state (pinned tooltip) WITHOUT emitting
125
+ * selection events — the host calls this when a global filter
126
+ * contributed by this panel's selection is removed from the global
127
+ * filter bar, so the pin can't outlive the filter it produced. The
128
+ * transport's `_lastInsertConfig` (remove-set memory) is deliberately
129
+ * RETAINED so a subsequent selection still replaces any leftover
130
+ * clauses rather than accumulating.
131
+ */
132
+ deselect(): Promise<void>;
91
133
  render(view: View): Promise<Blob>;
92
134
  restore(config: any, columns_config?: Record<string, any>): void;
93
135
  delete(): void;
@@ -1,16 +1,19 @@
1
1
  import type { WebGLContextManager } from "../webgl/context-manager";
2
2
  /**
3
- * Request a coalesced render of `glManager` whose body is
4
- * `fullRender`. Returns a promise that resolves when this entry's
5
- * Phase 2 (`awaitGpuFence` + `endFrame`) completes.
3
+ * Request a coalesced render of `glManager`. `fullRender` submits the
4
+ * GL commands (Phase 1, synchronous); `render2D` draws the 2D layers
5
+ * (gridlines + chrome) and runs in Phase 2 *after* `awaitGpuFence`, so
6
+ * the 2D canvases don't present ahead of the GL frame. Returns a
7
+ * promise that resolves when this entry's Phase 2 (`awaitGpuFence` +
8
+ * `render2D` + `endFrame`) completes.
6
9
  *
7
10
  * If a request is already pending for the same glManager, the new
8
- * call's `fullRender` closure replaces the prior one (latest call
9
- * wins; closures read chart state lazily so this is functionally a
10
- * no-op, but keeps the closure fresh) and the returned promise
11
- * resolves alongside the existing waiters.
11
+ * call's `fullRender` / `render2D` closures replace the prior ones
12
+ * (latest call wins; closures read chart state lazily so this is
13
+ * functionally a no-op, but keeps the closures fresh) and the returned
14
+ * promise resolves alongside the existing waiters.
12
15
  */
13
- export declare function requestRender(glManager: WebGLContextManager, fullRender: () => void): Promise<void>;
16
+ export declare function requestRender(glManager: WebGLContextManager, fullRender: () => void, render2D: () => void): Promise<void>;
14
17
  /**
15
18
  * Run `op` synchronously if no drain `present()` is currently active
16
19
  * for `glManager`. Otherwise queue `op` to run as soon as that
@@ -34,6 +37,29 @@ export declare function requestRender(glManager: WebGLContextManager, fullRender
34
37
  * picks it up for the next RAF.
35
38
  */
36
39
  export declare function deferIfDraining(glManager: WebGLContextManager, op: () => void): void;
40
+ /**
41
+ * Drop every scheduler reference to `glManager` — called from
42
+ * `WebGLContextManager`'s owning renderer at teardown, *before*
43
+ * `glManager.destroy()` loses the GL context.
44
+ *
45
+ * Without this, a frame queued for the next RAF (an `Entry` in
46
+ * `pending`) or a `deferIfDraining` op still parked in `deferred`
47
+ * would survive the destroy and, on the next `drain()`, drive
48
+ * `fullRender` / `present` against a context-lost manager. In blit
49
+ * mode that surfaces as `endFrame`'s `transferToImageBitmap` throwing
50
+ * "Cannot transfer to ImageBitmap because WebGL context is lost" —
51
+ * logged as "scheduler: present failed". In direct mode it's a wasted
52
+ * paint against a dead context.
53
+ *
54
+ * Outstanding waiters for a destroyed manager are resolved (not
55
+ * rejected): the caller asked to tear the chart down, so its awaited
56
+ * `draw()` should observe a clean no-op rather than an error it would
57
+ * have to suppress. An in-flight `present()` (manager in `inFlight`)
58
+ * is left to unwind on its own — its `finally` clause will not find a
59
+ * `deferred` entry to flush, and the manager is already gone from
60
+ * `pending`, so it cannot re-enqueue.
61
+ */
62
+ export declare function unregister(glManager: WebGLContextManager): void;
37
63
  /**
38
64
  * Test-only: clear pending state. Production callers must not use
39
65
  * this — outstanding waiters are silently dropped.
@@ -9,8 +9,8 @@ export type { ThemeSnapshot };
9
9
  * host's real `Client` — that's pure protobuf bytes; this one is the
10
10
  * chart's own renderer control plane.
11
11
  */
12
- export type ControlMsg = InitMsg | SetViewByNameMsg | SetColumnsConfigMsg | SetPluginConfigMsg | SetBufferMaxCapacityMsg | LoadAndRenderMsg | RedrawMsg | ResizeMsg | ClearMsg | InvalidateThemeMsg | RestoreZoomMsg | ResetAllZoomsMsg | ResetExpandedDomainMsg | SaveZoomReqMsg | SnapshotPngReqMsg | InteractionMsg | DestroyMsg;
13
- export type WorkerMsg = ReadyMsg | ZoomChangedMsg | SaveZoomReplyMsg | SnapshotPngReplyMsg | PinTooltipMsg | DismissTooltipMsg | SetCursorMsg | UserClickMsg | UserSelectMsg | LoadAndRenderAckMsg | FrameBitmapMsg | ErrorMsg;
12
+ export type ControlMsg = InitMsg | SetViewByNameMsg | SetColumnsConfigMsg | SetPluginConfigMsg | SetBufferMaxCapacityMsg | LoadAndRenderMsg | RedrawMsg | DeselectMsg | ResizeMsg | ClearMsg | InvalidateThemeMsg | RestoreZoomMsg | ResetAllZoomsMsg | ResetExpandedDomainMsg | SaveZoomReqMsg | SnapshotPngReqMsg | InteractionMsg | DestroyMsg;
13
+ export type WorkerMsg = ReadyMsg | ZoomChangedMsg | SaveZoomReplyMsg | SnapshotPngReplyMsg | PinTooltipMsg | DismissTooltipMsg | SetCursorMsg | UserClickMsg | UserSelectMsg | LoadAndRenderAckMsg | ResizeAckMsg | FrameBitmapMsg | ErrorMsg;
14
14
  /**
15
15
  * Session-tagged envelopes for the shared-worker transport. Every
16
16
  * message between the host and the shared `Worker` carries a numeric
@@ -230,12 +230,41 @@ export interface LoadAndRenderAckMsg {
230
230
  export interface RedrawMsg {
231
231
  kind: "redraw";
232
232
  }
233
+ /**
234
+ * Host → worker: silently clear the chart's selection state (pinned
235
+ * tooltip) — no `userSelect` reply, so the host filter-bar removal that
236
+ * triggered it can't echo back into the host's filter set. The worker's
237
+ * `TooltipController.dismiss()` posts `dismissTooltip` to tear down the
238
+ * host-side pinned artifact.
239
+ */
240
+ export interface DeselectMsg {
241
+ kind: "deselect";
242
+ }
243
+ /**
244
+ * Host → worker: resize the chart to a new CSS box. Replied with
245
+ * {@link ResizeAckMsg} AFTER the resized frame PRESENTS — the host's
246
+ * `plugin.resize()` promise is the viewer's presize contract ("the
247
+ * plugin has repainted at the target box when this resolves"), so an
248
+ * ack at message-receipt would let the settings-pane layout commit
249
+ * against the old-dimensions bitmap (the aspect-ratio warp).
250
+ */
233
251
  export interface ResizeMsg {
234
252
  kind: "resize";
253
+ msgId: number;
235
254
  cssWidth: number;
236
255
  cssHeight: number;
237
256
  dpr: number;
238
257
  }
258
+ /**
259
+ * Worker → host reply to a `ResizeMsg`, posted after the resized
260
+ * frame's present completes. Always sent — present failures and
261
+ * torn-down charts included — so the host's awaited promise resolves
262
+ * deterministically.
263
+ */
264
+ export interface ResizeAckMsg {
265
+ kind: "resizeAck";
266
+ msgId: number;
267
+ }
239
268
  export interface ClearMsg {
240
269
  kind: "clear";
241
270
  }
@@ -20,11 +20,21 @@ import type { InteractionEvent } from "./protocol";
20
20
  */
21
21
  export declare class RendererTransport {
22
22
  private _handle;
23
+ /**
24
+ * Set by {@link destroy}. Requests allocated after teardown settle
25
+ * immediately in `_allocPending` — `_post` is already a no-op once
26
+ * `_handle` is null, so a late request would otherwise never
27
+ * receive its reply and pend forever.
28
+ */
29
+ private _destroyed;
23
30
  private _proxyChannel;
24
31
  private _proxySession;
25
32
  private _client;
26
33
  private _view;
27
34
  private _tableName;
35
+ /** Source panel `slot`, tagged onto dispatched interaction events so the
36
+ * host can route them (master/detail, global filter) to the right panel. */
37
+ private _panel;
28
38
  private _clientWorkerURL;
29
39
  private _clientWasm;
30
40
  private _chartTag;
@@ -63,6 +73,30 @@ export declare class RendererTransport {
63
73
  * canvas's drawing buffer is the worker's transferred GL canvas).
64
74
  */
65
75
  private _displayCtx;
76
+ /**
77
+ * Present-hold state for the staged-present presize protocol
78
+ * ({@link presize}). While holding, inbound `frameBitmap`s are
79
+ * STAGED (latest wins) instead of blitted, so the on-screen frame
80
+ * — old dimensions in the old, unchanged box — stays put until the
81
+ * host invokes the present closure `presize` resolved to, in the
82
+ * same task as its layout commit. Entered only by `presize`;
83
+ * exited by the present closure, by `presize`'s rejection path, or
84
+ * by `destroy` — every settle path releases the hold or hands the
85
+ * caller the release.
86
+ */
87
+ /**
88
+ * The last `(cssWidth, cssHeight, dpr)` posted to the worker — at init
89
+ * or any resize — i.e. the dimensions the worker has already rendered
90
+ * (or has an in-flight render for). {@link resize} short-circuits when
91
+ * its measurement matches (±0.5px, the presize protocol's own
92
+ * threshold): reactive resizes fan in from several host paths per
93
+ * layout transition (the post-commit fan-out, activation nudges, the
94
+ * fresh-restore tail), and every worker resize is an unconditional
95
+ * full re-render.
96
+ */
97
+ private _lastPostedSize;
98
+ private _holdPresent;
99
+ private _stagedFrame;
66
100
  /**
67
101
  * Host-side sink for tooltip + cursor side-effects. The chart
68
102
  * inside the renderer calls into a `MessageHostSink` that posts
@@ -85,6 +119,7 @@ export declare class RendererTransport {
85
119
  client: Client;
86
120
  view: View;
87
121
  tableName?: string;
122
+ panel?: string;
88
123
  clientWasm: WebAssembly.Module;
89
124
  clientWorkerURL: URL;
90
125
  chartTag: string;
@@ -125,7 +160,8 @@ export declare class RendererTransport {
125
160
  * `loadAndRenderAck`. Per the worker's "resolve on stale"
126
161
  * contract, a mid-flight cancellation (a newer `loadAndRender`
127
162
  * superseding this one) still acks — the host's awaiter just
128
- * resolves quietly.
163
+ * resolves quietly. Calls issued after `destroy()` resolve
164
+ * immediately (see `_allocPending`).
129
165
  */
130
166
  loadAndRender(opts: {
131
167
  viewerConfig: {
@@ -138,7 +174,41 @@ export declare class RendererTransport {
138
174
  };
139
175
  }): Promise<void>;
140
176
  redraw(): void;
141
- resize(): void;
177
+ /**
178
+ * Silently clear the worker chart's selection state (pinned tooltip) —
179
+ * no selection events are emitted; the worker echoes `dismissTooltip`
180
+ * to drop the host-side pinned artifact. See `DeselectMsg`.
181
+ */
182
+ deselect(): void;
183
+ /**
184
+ * Resize the chart to the GL canvas's CURRENT CSS box and resolve
185
+ * once the resized frame has PRESENTED (the worker's `resizeAck`) —
186
+ * not at message-post, which would let callers proceed while the
187
+ * old-dimensions bitmap is still on screen. Reactive geometry
188
+ * changes only; anticipated ones go through {@link presize}.
189
+ *
190
+ * A measurement matching {@link _lastPostedSize} resolves immediately
191
+ * WITHOUT a worker round trip: the worker has already rendered (or is
192
+ * presenting) that exact frame, and re-posting would be a full
193
+ * re-render of an identical bitmap.
194
+ */
195
+ resize(): Promise<void>;
196
+ /**
197
+ * Staged-present variant of {@link resize}: render at the given
198
+ * TARGET canvas CSS box (no measurement — the caller supplies the
199
+ * box the pending layout commit will produce), HOLDING the
200
+ * resulting frame (and any others that arrive meanwhile) offscreen
201
+ * instead of blitting, so nothing on screen changes. Resolves,
202
+ * once the resized frame is staged, to a present closure: calling
203
+ * it blits the latest staged frame and exits the hold — the host
204
+ * runs it in the same task as its layout commit so geometry and
205
+ * pixels land in one paint. On rejection (teardown) the hold is
206
+ * released here, so every settle path either frees the display or
207
+ * hands the caller the release.
208
+ */
209
+ presize(cssWidth: number, cssHeight: number): Promise<(() => void) | void>;
210
+ private _postResize;
211
+ private _presentStaged;
142
212
  clear(): void;
143
213
  invalidateTheme(): void;
144
214
  saveZoom(): Promise<void>;
@@ -146,7 +216,8 @@ export declare class RendererTransport {
146
216
  * Allocate a pending request slot of the given `kind`. Returns the
147
217
  * id (encoded into the outgoing `ControlMsg`) and a promise that
148
218
  * resolves / rejects when the matching reply arrives or
149
- * `destroy()` drains the table.
219
+ * `destroy()` drains the table. Requests issued *after* `destroy()`
220
+ * settle immediately with the same per-kind semantics as the drain.
150
221
  */
151
222
  private _allocPending;
152
223
  restoreZoom(state: any): void;
@@ -1,62 +1,78 @@
1
1
  import { ShaderRegistry } from "./shader-registry";
2
2
  import { BufferPool } from "./buffer-pool";
3
- export type WebGLCanvas = HTMLCanvasElement | OffscreenCanvas;
3
+ import { GLContext, type WebGLCanvas } from "./gl-context";
4
+ export type { WebGLCanvas } from "./gl-context";
4
5
  export interface WebGLContextManagerOptions {
5
6
  /**
6
7
  * If `true`, compile + link every shader in `SHADER_MANIFEST`
7
- * during construction (and again after a context-loss/restore
8
- * cycle). Trades ~30-100ms of init time for elimination of the
9
- * compile/link cost on the first-frame path of every chart type
10
- * that ends up rendered. Default `false` keeps the original
11
- * lazy-compile behavior — programs are compiled on first
12
- * `getOrCreate(name, ...)` call from a glyph's render path.
8
+ * during context construction (and again after a context-loss/
9
+ * restore cycle). Trades ~30-100ms of init time for elimination of
10
+ * the compile/link cost on the first-frame path. Ignored when the
11
+ * manager is constructed against an already-built shared
12
+ * {@link GLContext} — that context owns the precompile decision.
13
+ * Default `false` keeps lazy-compile behavior.
13
14
  */
14
15
  precompile?: boolean;
15
16
  }
17
+ /**
18
+ * Per-chart render handle. Owns the chart's GPU **buffers** and frame
19
+ * bookkeeping, but only *borrows* its GL **context** — either a private
20
+ * one it constructs (the default 1:1 mode, where the manager is handed a
21
+ * raw canvas) or a shared {@link GLContext} from the
22
+ * [ContextPool](./context-pool.ts) (pooled blit mode, where many
23
+ * managers share K contexts to stay under the browser's per-agent
24
+ * context cap).
25
+ *
26
+ * Buffers are always per-manager: {@link BufferPool} keys buffers by
27
+ * name, so two charts sharing one pool would stomp each other's `"x"` /
28
+ * `"y"` / `"color"`. Shaders, the GL context, and the canvas drawing
29
+ * buffer are shared per {@link GLContext}; the scheduler serializes
30
+ * renders of managers that share a context so the shared drawing buffer
31
+ * only ever holds one chart's frame at a time (see {@link beginFrame}).
32
+ */
16
33
  export declare class WebGLContextManager {
17
- private _canvas;
18
- private _gl;
19
- private _isWebGL2;
20
- private _shaders;
34
+ private _ctx;
35
+ private _ownsCtx;
36
+ private _disposeRestore;
21
37
  private _buffers;
22
38
  private _uploadedCount;
23
39
  private _cssWidth;
24
40
  private _cssHeight;
25
41
  private _dpr;
26
- private _precompile;
42
+ private _destroyed;
27
43
  private _frameCallback;
28
44
  /**
29
45
  * Per-instance `MessageChannel` used by `_yieldToTask` to resume a
30
- * polling `awaitGpuFence` loop on the next task. Allocated lazily —
31
- * the polling path is rarely hit when the GPU is idle, and many
32
- * `WebGLContextManager` instances never need one.
33
- *
46
+ * polling `awaitGpuFence` loop on the next task. Allocated lazily.
34
47
  * Must be per-instance: a module-level singleton races when two
35
- * managers poll concurrently. Both call sites assign
36
- * `port1.onmessage = resolve`, the second assignment overwrites the
37
- * first, and the first poll's promise never settles — leaving its
38
- * `awaitGpuFence` hung. The hang propagates up through the render
39
- * scheduler's `present` → `uploadAndRender` → the worker's
40
- * `uploadChunkAck` → the host's `with_typed_arrays` callback,
41
- * stalling `draw()` indefinitely. (Now that fence waits across
42
- * different `WebGLContextManager`s run in parallel inside one
43
- * scheduler drain, the per-instance discipline matters even
44
- * more — concurrent poll loops are the common case, not the
45
- * exception.)
46
- *
47
- * Cost of per-instance allocation: one extra `MessageChannel` (two
48
- * `MessagePort`s, ~negligible bytes, zero idle CPU) per chart on
49
- * top of the per-chart proxy channel that the transport already
50
- * holds. Bounded by chart count; safe even for pathological pages
51
- * with hundreds of charts. The alternative — allocating a fresh
52
- * channel on every `_yieldToTask` call — would churn ports on every
53
- * fence poll (potentially many per frame on slow GPUs), which is
54
- * far worse for the structured-clone subsystem than holding one
55
- * port pair for the manager's lifetime.
48
+ * managers poll concurrently (the second `onmessage` assignment
49
+ * clobbers the first's resolver, hanging that fence wait). Now that
50
+ * fence waits across managers run in parallel within one scheduler
51
+ * drain, concurrent poll loops are the common case.
56
52
  */
57
53
  private _yieldChannel;
58
- constructor(canvas: WebGLCanvas, options?: WebGLContextManagerOptions);
54
+ private _pendingYieldResolve;
55
+ /**
56
+ * @param source A raw canvas (manager constructs and **owns** a
57
+ * private {@link GLContext}) or an existing shared `GLContext`
58
+ * (manager borrows it; the pool owns its lifecycle).
59
+ */
60
+ constructor(source: WebGLCanvas | GLContext, options?: WebGLContextManagerOptions);
59
61
  get gl(): WebGL2RenderingContext | WebGLRenderingContext;
62
+ /**
63
+ * Identifies the shared GL context behind this manager. The
64
+ * scheduler groups pending renders by this id: managers on the same
65
+ * backend serialize (they share one drawing buffer); managers on
66
+ * different backends render in parallel. In the default 1:1 mode
67
+ * every manager has its own backend, so every render is independent
68
+ * — identical to the pre-pooling behavior.
69
+ */
70
+ get backendId(): number;
71
+ /**
72
+ * True once this manager's context has been lost — its own
73
+ * {@link destroy}, or a browser eviction of the shared context.
74
+ */
75
+ isContextLost(): boolean;
60
76
  get isWebGL2(): boolean;
61
77
  get shaders(): ShaderRegistry;
62
78
  get bufferPool(): BufferPool;
@@ -64,11 +80,10 @@ export declare class WebGLContextManager {
64
80
  set uploadedCount(count: number);
65
81
  /**
66
82
  * Resize the GL canvas's bitmap to match the host's CSS layout. The
67
- * Host is responsible for measuring the DOM element (or otherwise
68
- * deciding the target CSS size) and the device pixel ratio — the
69
- * manager itself does not touch DOM, so the same code path works
70
- * whether the canvas is an `HTMLCanvasElement` (in-process) or an
71
- * `OffscreenCanvas` (in-process via transfer, or in a worker).
83
+ * host measures the DOM element and DPR — the manager never touches
84
+ * DOM, so the same path works for an `HTMLCanvasElement`
85
+ * (in-process), a transferred `OffscreenCanvas` (direct/worker), or
86
+ * a pool-shared `OffscreenCanvas` (pooled blit).
72
87
  */
73
88
  resize(cssWidth: number, cssHeight: number, dpr: number): void;
74
89
  /**
@@ -78,106 +93,74 @@ export declare class WebGLContextManager {
78
93
  */
79
94
  private _pendingResize;
80
95
  /**
81
- * Record a dimension change to be applied at the start of the
82
- * next render's Phase 1, *before* `_fullRender` runs. The actual
83
- * `canvas.width = N` assignment (which clears the drawing buffer
84
- * per the WebGL spec) happens inside `applyPendingResize()`,
85
- * paired in the same synchronous task as the paint that fills
86
- * the new buffer.
96
+ * Record a dimension change to be applied at the start of the next
97
+ * render's Phase 1, before `_fullRender` runs. The actual
98
+ * `canvas.width = N` assignment (which clears the drawing buffer per
99
+ * the WebGL spec) happens inside {@link applyPendingResize} /
100
+ * {@link beginFrame}, paired in the same synchronous task as the
101
+ * paint that fills the new buffer — eliminating the one-frame blank
102
+ * the compositor would otherwise show in direct/in-process modes
103
+ * (where the GL canvas IS the visible canvas).
87
104
  *
88
- * Why split the dimension change off from the existing
89
- * {@link resize} method: in direct / in-process modes the
90
- * GL canvas IS the host's visible canvas, and `canvas.width = N`
91
- * is immediately observable to the browser's compositor as a
92
- * cleared buffer. If the resize lands in the message handler
93
- * (one task) but the matching `_fullRender` lands in the next
94
- * RAF (a later task), the compositor cycles between them and
95
- * presents one full frame of empty canvas — visible flicker.
96
- * Deferring the dimension change to the same RAF as the paint
97
- * eliminates the inter-frame gap; both happen inside Phase 1's
98
- * un-yielded loop.
99
- *
100
- * Multiple `requestResize` calls before the next render coalesce
101
- * to last-write-wins — five rapid width changes from a window
102
- * drag produce one resize+paint, not five.
105
+ * Multiple calls before the next render coalesce to last-write-wins.
103
106
  */
104
107
  requestResize(cssWidth: number, cssHeight: number, dpr: number): void;
105
108
  /**
106
109
  * Apply any pending dimension change recorded by
107
- * {@link requestResize}. Called by the scheduler's Phase 1
108
- * (immediately before each entry's `fullRender`) and by the
109
- * `snapshotPng` bypass path. Returns `true` when a resize was
110
- * applied, `false` when there was nothing pending — useful for
111
- * callers that want to skip a no-op render.
110
+ * {@link requestResize}. Returns `true` when a resize was applied.
112
111
  */
113
112
  applyPendingResize(): boolean;
114
113
  /**
115
- * Last CSS width passed to `resize()`.
114
+ * Prepare the shared GL surface for *this* manager's render. Called
115
+ * by the scheduler immediately before `_fullRender` (and by the
116
+ * `snapshotPng` bypass).
117
+ *
118
+ * - **Own context (1:1):** identical to the old Phase-1 step — apply
119
+ * only a *pending* resize. The context isn't shared, so there is
120
+ * no sibling state to reset and no need to touch the canvas when
121
+ * dimensions are unchanged.
122
+ * - **Shared context (pooled):** a co-tenant chart rendered last and
123
+ * left the shared canvas at *its* dimensions with *its* GL state.
124
+ * Force the canvas to this manager's dimensions and reset the
125
+ * global GL state a sibling might have left dirty (bound
126
+ * framebuffer, viewport, scissor/depth enables) so frames can't
127
+ * bleed across charts. Per-frame draw state (blend, clear) is
128
+ * re-established by the chart's `clearAndSetupFrame`.
116
129
  */
130
+ beginFrame(): void;
117
131
  get cssWidth(): number;
118
- /**
119
- * Last CSS height passed to `resize()`.
120
- */
121
132
  get cssHeight(): number;
122
- /**
123
- * Last device pixel ratio passed to `resize()`.
124
- */
125
133
  get dpr(): number;
126
134
  clear(): void;
127
135
  /**
128
136
  * Register a per-frame hook invoked at the end of each render. In
129
- * blit-mode rendering, the worker installs a callback that
130
- * transfers an `ImageBitmap` from `_canvas` (an `OffscreenCanvas`)
131
- * back to the host so the visible display canvas can `drawImage`
132
- * it. In direct mode the callback is left null and `endFrame` is a
133
- * no-op.
134
- *
135
- * Pass `null` to detach.
137
+ * blit-mode rendering the worker installs a callback that transfers
138
+ * an `ImageBitmap` from the canvas back to the host. In direct mode
139
+ * the callback is null and `endFrame` is a no-op. Pass `null` to
140
+ * detach.
136
141
  */
137
142
  setFrameCallback(cb: ((bitmap: ImageBitmap) => void) | null): void;
138
143
  /**
139
- * Called by chart impls at the bottom of `_fullRender` (and any
140
- * other path that produces a complete frame). When a frame
141
- * callback is registered AND the GL surface is an
142
- * `OffscreenCanvas`, ship its current contents as an
143
- * `ImageBitmap` to the host. Otherwise no-op — direct-mode
144
- * rendering has nothing to ship; the visible canvas already holds
145
- * the drawing buffer.
144
+ * Ship the current canvas contents as an `ImageBitmap` to the host
145
+ * when a frame callback is registered and the surface is an
146
+ * `OffscreenCanvas`. Otherwise no-op (direct mode paints straight to
147
+ * the visible drawing buffer).
146
148
  */
147
149
  endFrame(): void;
148
150
  /**
149
151
  * Resolve when every GL command submitted up to this call has been
150
- * executed by the GPU.
151
- *
152
- * On WebGL2 this issues a `fenceSync(SYNC_GPU_COMMANDS_COMPLETE)`
153
- * and polls `clientWaitSync` with a zero timeout, yielding to the
154
- * task queue between polls. The first poll passes
155
- * `SYNC_FLUSH_COMMANDS_BIT` so the fence becomes reachable without
156
- * a separate `gl.flush()`.
157
- *
158
- * On WebGL1 there is no fenceSync; we fall back to the blocking
159
- * `gl.finish()`. This is acceptable in a worker — never call this
160
- * from the main thread on a heavy frame.
161
- *
162
- * Used as a per-frame "GPU is idle" barrier so callers can serialize
163
- * follow-on work (`endFrame` snapshot, present roundtrip, the next
164
- * chunk upload) against actual GPU completion instead of the
165
- * implicit, implementation-defined timing of `transferToImageBitmap`.
152
+ * executed by the GPU. WebGL2 issues a `fenceSync` and polls
153
+ * `clientWaitSync` (yielding to the task queue between polls); WebGL1
154
+ * falls back to a blocking `gl.finish()` (acceptable in a worker).
166
155
  */
167
156
  awaitGpuFence(): Promise<void>;
168
157
  ensureBufferCapacity(totalRows: number): void;
169
158
  /**
170
- * Yield to the task queue between fence polls. We avoid
171
- * `setTimeout(0)` because Chromium clamps nested `setTimeout` to
172
- * ~4ms in workers, which would inflate the measured cost of
173
- * `awaitGpuFence`. A reused per-instance `MessageChannel` lands the
174
- * resume in the next task with sub-ms latency.
175
- *
176
- * `addEventListener(..., { once: true })` is used over
177
- * `port1.onmessage = ...` so concurrent in-flight resumes (should
178
- * any path ever introduce them) cannot clobber each other's
179
- * resolvers — the previous module-level singleton lost a resolver
180
- * on every overlap and hung one chart's `draw()` indefinitely.
159
+ * Yield to the task queue between fence polls. `setTimeout(0)` is
160
+ * avoided because Chromium clamps nested worker `setTimeout` to
161
+ * ~4ms; a reused per-instance `MessageChannel` resumes in the next
162
+ * task with sub-ms latency. `{ once: true }` over `onmessage = ...`
163
+ * so concurrent resumes can't clobber each other's resolvers.
181
164
  */
182
165
  private _yieldToTask;
183
166
  destroy(): void;
@@ -0,0 +1,40 @@
1
+ import { GLContext } from "./gl-context";
2
+ /**
3
+ * Renderer-scope pool of shared {@link GLContext}s for pooled blit mode.
4
+ *
5
+ * The browser caps live WebGL contexts per agent (~16 in Chromium) and
6
+ * force-loses the oldest past that cap. A page with more than ~16 charts
7
+ * therefore can't give each its own context. This pool caps the number
8
+ * of live contexts at `size`, distributing renderers across them
9
+ * round-robin (sticky for a renderer's lifetime — a renderer keeps the
10
+ * same context so its resident GPU buffers and any cached textures/FBOs
11
+ * stay valid frame to frame). N charts render through K = `size`
12
+ * contexts; the scheduler serializes renders that land on the same
13
+ * context.
14
+ *
15
+ * Pooling applies to blit mode only: direct mode renders into the host's
16
+ * transferred visible canvas, which is permanently 1:1 with a context.
17
+ *
18
+ * One pool per renderer scope (the worker, or the main thread in
19
+ * in-process mode). Contexts are created lazily on first `acquire` and
20
+ * live until renderer-scope teardown — like the contexts they replace,
21
+ * they are not refcounted, because a shared context outlives any single
22
+ * borrowing renderer.
23
+ */
24
+ export declare class ContextPool {
25
+ private _size;
26
+ private _precompile;
27
+ private _contexts;
28
+ private _next;
29
+ constructor(size: number, options?: {
30
+ precompile?: boolean;
31
+ });
32
+ /**
33
+ * Borrow a context for a new renderer. Fills the pool to `size`
34
+ * before reusing, then round-robins. A dead context (browser
35
+ * eviction) is replaced in place so a borrower never receives a lost
36
+ * context.
37
+ */
38
+ acquire(): GLContext;
39
+ private _make;
40
+ }