@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
|
@@ -11,137 +11,125 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
import { ShaderRegistry } from "./shader-registry";
|
|
14
|
-
import { SHADER_MANIFEST } from "./shader-manifest";
|
|
15
14
|
import { BufferPool } from "./buffer-pool";
|
|
15
|
+
import { GLContext, type WebGLCanvas } from "./gl-context";
|
|
16
16
|
|
|
17
|
-
export type WebGLCanvas
|
|
17
|
+
export type { WebGLCanvas } from "./gl-context";
|
|
18
18
|
|
|
19
19
|
export interface WebGLContextManagerOptions {
|
|
20
20
|
/**
|
|
21
21
|
* If `true`, compile + link every shader in `SHADER_MANIFEST`
|
|
22
|
-
* during construction (and again after a context-loss/
|
|
23
|
-
* cycle). Trades ~30-100ms of init time for elimination of
|
|
24
|
-
* compile/link cost on the first-frame path
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* during context construction (and again after a context-loss/
|
|
23
|
+
* restore cycle). Trades ~30-100ms of init time for elimination of
|
|
24
|
+
* the compile/link cost on the first-frame path. Ignored when the
|
|
25
|
+
* manager is constructed against an already-built shared
|
|
26
|
+
* {@link GLContext} — that context owns the precompile decision.
|
|
27
|
+
* Default `false` keeps lazy-compile behavior.
|
|
28
28
|
*/
|
|
29
29
|
precompile?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Per-chart render handle. Owns the chart's GPU **buffers** and frame
|
|
34
|
+
* bookkeeping, but only *borrows* its GL **context** — either a private
|
|
35
|
+
* one it constructs (the default 1:1 mode, where the manager is handed a
|
|
36
|
+
* raw canvas) or a shared {@link GLContext} from the
|
|
37
|
+
* [ContextPool](./context-pool.ts) (pooled blit mode, where many
|
|
38
|
+
* managers share K contexts to stay under the browser's per-agent
|
|
39
|
+
* context cap).
|
|
40
|
+
*
|
|
41
|
+
* Buffers are always per-manager: {@link BufferPool} keys buffers by
|
|
42
|
+
* name, so two charts sharing one pool would stomp each other's `"x"` /
|
|
43
|
+
* `"y"` / `"color"`. Shaders, the GL context, and the canvas drawing
|
|
44
|
+
* buffer are shared per {@link GLContext}; the scheduler serializes
|
|
45
|
+
* renders of managers that share a context so the shared drawing buffer
|
|
46
|
+
* only ever holds one chart's frame at a time (see {@link beginFrame}).
|
|
47
|
+
*/
|
|
32
48
|
export class WebGLContextManager {
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
private
|
|
36
|
-
private _shaders: ShaderRegistry;
|
|
49
|
+
private _ctx: GLContext;
|
|
50
|
+
private _ownsCtx: boolean;
|
|
51
|
+
private _disposeRestore: () => void;
|
|
37
52
|
private _buffers: BufferPool;
|
|
38
53
|
private _uploadedCount = 0;
|
|
39
54
|
private _cssWidth = 0;
|
|
40
55
|
private _cssHeight = 0;
|
|
41
56
|
private _dpr = 1;
|
|
42
|
-
private
|
|
57
|
+
private _destroyed = false;
|
|
43
58
|
private _frameCallback: ((bitmap: ImageBitmap) => void) | null = null;
|
|
44
59
|
|
|
45
60
|
/**
|
|
46
61
|
* Per-instance `MessageChannel` used by `_yieldToTask` to resume a
|
|
47
|
-
* polling `awaitGpuFence` loop on the next task. Allocated lazily
|
|
48
|
-
* the polling path is rarely hit when the GPU is idle, and many
|
|
49
|
-
* `WebGLContextManager` instances never need one.
|
|
50
|
-
*
|
|
62
|
+
* polling `awaitGpuFence` loop on the next task. Allocated lazily.
|
|
51
63
|
* Must be per-instance: a module-level singleton races when two
|
|
52
|
-
* managers poll concurrently
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* scheduler's `present` → `uploadAndRender` → the worker's
|
|
57
|
-
* `uploadChunkAck` → the host's `with_typed_arrays` callback,
|
|
58
|
-
* stalling `draw()` indefinitely. (Now that fence waits across
|
|
59
|
-
* different `WebGLContextManager`s run in parallel inside one
|
|
60
|
-
* scheduler drain, the per-instance discipline matters even
|
|
61
|
-
* more — concurrent poll loops are the common case, not the
|
|
62
|
-
* exception.)
|
|
63
|
-
*
|
|
64
|
-
* Cost of per-instance allocation: one extra `MessageChannel` (two
|
|
65
|
-
* `MessagePort`s, ~negligible bytes, zero idle CPU) per chart on
|
|
66
|
-
* top of the per-chart proxy channel that the transport already
|
|
67
|
-
* holds. Bounded by chart count; safe even for pathological pages
|
|
68
|
-
* with hundreds of charts. The alternative — allocating a fresh
|
|
69
|
-
* channel on every `_yieldToTask` call — would churn ports on every
|
|
70
|
-
* fence poll (potentially many per frame on slow GPUs), which is
|
|
71
|
-
* far worse for the structured-clone subsystem than holding one
|
|
72
|
-
* port pair for the manager's lifetime.
|
|
64
|
+
* managers poll concurrently (the second `onmessage` assignment
|
|
65
|
+
* clobbers the first's resolver, hanging that fence wait). Now that
|
|
66
|
+
* fence waits across managers run in parallel within one scheduler
|
|
67
|
+
* drain, concurrent poll loops are the common case.
|
|
73
68
|
*/
|
|
74
69
|
private _yieldChannel: MessageChannel | null = null;
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
this._canvas = canvas;
|
|
78
|
-
this._precompile = options.precompile ?? false;
|
|
79
|
-
const gl2 = canvas.getContext("webgl2", {
|
|
80
|
-
antialias: true,
|
|
81
|
-
alpha: true,
|
|
82
|
-
premultipliedAlpha: false,
|
|
83
|
-
});
|
|
71
|
+
private _pendingYieldResolve: (() => void) | null = null;
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
/**
|
|
74
|
+
* @param source A raw canvas (manager constructs and **owns** a
|
|
75
|
+
* private {@link GLContext}) or an existing shared `GLContext`
|
|
76
|
+
* (manager borrows it; the pool owns its lifecycle).
|
|
77
|
+
*/
|
|
78
|
+
constructor(
|
|
79
|
+
source: WebGLCanvas | GLContext,
|
|
80
|
+
options: WebGLContextManagerOptions = {},
|
|
81
|
+
) {
|
|
82
|
+
if (source instanceof GLContext) {
|
|
83
|
+
this._ctx = source;
|
|
84
|
+
this._ownsCtx = false;
|
|
88
85
|
} else {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
alpha: true,
|
|
92
|
-
premultipliedAlpha: false,
|
|
86
|
+
this._ctx = new GLContext(source, {
|
|
87
|
+
precompile: options.precompile ?? false,
|
|
93
88
|
});
|
|
94
|
-
|
|
95
|
-
if (!gl1) {
|
|
96
|
-
throw new Error("WebGL is not supported");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this._gl = gl1 as WebGLRenderingContext;
|
|
100
|
-
this._isWebGL2 = false;
|
|
89
|
+
this._ownsCtx = true;
|
|
101
90
|
}
|
|
102
91
|
|
|
103
|
-
this.
|
|
104
|
-
this._buffers = new BufferPool(this._gl);
|
|
105
|
-
|
|
106
|
-
if (this._precompile) {
|
|
107
|
-
this._shaders.precompile(SHADER_MANIFEST);
|
|
108
|
-
}
|
|
92
|
+
this._buffers = new BufferPool(this._ctx.gl);
|
|
109
93
|
|
|
110
|
-
//
|
|
111
|
-
// `
|
|
112
|
-
|
|
113
|
-
(canvas as EventTarget).addEventListener(
|
|
114
|
-
"webglcontextlost",
|
|
115
|
-
(e: Event) => {
|
|
116
|
-
e.preventDefault();
|
|
117
|
-
},
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
(canvas as EventTarget).addEventListener("webglcontextrestored", () => {
|
|
121
|
-
this._shaders.releaseAll();
|
|
94
|
+
// Rebuild this tenant's buffers after a context restore. The
|
|
95
|
+
// shared `GLContext` rebuilds the shader cache itself, once.
|
|
96
|
+
this._disposeRestore = this._ctx.onRestore(() => {
|
|
122
97
|
this._buffers.releaseAll();
|
|
123
|
-
this.
|
|
124
|
-
this._buffers = new BufferPool(this._gl);
|
|
98
|
+
this._buffers = new BufferPool(this._ctx.gl);
|
|
125
99
|
this._uploadedCount = 0;
|
|
126
|
-
|
|
127
|
-
// Re-prime the cache after restore so post-recovery
|
|
128
|
-
// first-frame doesn't re-pay the lazy compile cost.
|
|
129
|
-
if (this._precompile) {
|
|
130
|
-
this._shaders.precompile(SHADER_MANIFEST);
|
|
131
|
-
}
|
|
132
100
|
});
|
|
133
101
|
}
|
|
134
102
|
|
|
135
103
|
get gl(): WebGL2RenderingContext | WebGLRenderingContext {
|
|
136
|
-
return this.
|
|
104
|
+
return this._ctx.gl;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Identifies the shared GL context behind this manager. The
|
|
109
|
+
* scheduler groups pending renders by this id: managers on the same
|
|
110
|
+
* backend serialize (they share one drawing buffer); managers on
|
|
111
|
+
* different backends render in parallel. In the default 1:1 mode
|
|
112
|
+
* every manager has its own backend, so every render is independent
|
|
113
|
+
* — identical to the pre-pooling behavior.
|
|
114
|
+
*/
|
|
115
|
+
get backendId(): number {
|
|
116
|
+
return this._ctx.id;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* True once this manager's context has been lost — its own
|
|
121
|
+
* {@link destroy}, or a browser eviction of the shared context.
|
|
122
|
+
*/
|
|
123
|
+
isContextLost(): boolean {
|
|
124
|
+
return this._destroyed || this._ctx.isContextLost();
|
|
137
125
|
}
|
|
138
126
|
|
|
139
127
|
get isWebGL2(): boolean {
|
|
140
|
-
return this.
|
|
128
|
+
return this._ctx.isWebGL2;
|
|
141
129
|
}
|
|
142
130
|
|
|
143
131
|
get shaders(): ShaderRegistry {
|
|
144
|
-
return this.
|
|
132
|
+
return this._ctx.shaders;
|
|
145
133
|
}
|
|
146
134
|
|
|
147
135
|
get bufferPool(): BufferPool {
|
|
@@ -158,11 +146,10 @@ export class WebGLContextManager {
|
|
|
158
146
|
|
|
159
147
|
/**
|
|
160
148
|
* Resize the GL canvas's bitmap to match the host's CSS layout. The
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* `OffscreenCanvas` (in-process via transfer, or in a worker).
|
|
149
|
+
* host measures the DOM element and DPR — the manager never touches
|
|
150
|
+
* DOM, so the same path works for an `HTMLCanvasElement`
|
|
151
|
+
* (in-process), a transferred `OffscreenCanvas` (direct/worker), or
|
|
152
|
+
* a pool-shared `OffscreenCanvas` (pooled blit).
|
|
166
153
|
*/
|
|
167
154
|
resize(cssWidth: number, cssHeight: number, dpr: number): void {
|
|
168
155
|
this._cssWidth = cssWidth;
|
|
@@ -171,11 +158,11 @@ export class WebGLContextManager {
|
|
|
171
158
|
|
|
172
159
|
const width = Math.round(cssWidth * dpr);
|
|
173
160
|
const height = Math.round(cssHeight * dpr);
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
161
|
+
const canvas = this._ctx.canvas;
|
|
162
|
+
if (canvas.width !== width || canvas.height !== height) {
|
|
163
|
+
canvas.width = width;
|
|
164
|
+
canvas.height = height;
|
|
165
|
+
this._ctx.gl.viewport(0, 0, width, height);
|
|
179
166
|
}
|
|
180
167
|
}
|
|
181
168
|
|
|
@@ -191,28 +178,16 @@ export class WebGLContextManager {
|
|
|
191
178
|
} | null = null;
|
|
192
179
|
|
|
193
180
|
/**
|
|
194
|
-
* Record a dimension change to be applied at the start of the
|
|
195
|
-
*
|
|
196
|
-
* `canvas.width = N` assignment (which clears the drawing buffer
|
|
197
|
-
*
|
|
198
|
-
* paired in the same synchronous task as the
|
|
199
|
-
* the new buffer
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* {@link resize} method: in direct / in-process modes the
|
|
203
|
-
* GL canvas IS the host's visible canvas, and `canvas.width = N`
|
|
204
|
-
* is immediately observable to the browser's compositor as a
|
|
205
|
-
* cleared buffer. If the resize lands in the message handler
|
|
206
|
-
* (one task) but the matching `_fullRender` lands in the next
|
|
207
|
-
* RAF (a later task), the compositor cycles between them and
|
|
208
|
-
* presents one full frame of empty canvas — visible flicker.
|
|
209
|
-
* Deferring the dimension change to the same RAF as the paint
|
|
210
|
-
* eliminates the inter-frame gap; both happen inside Phase 1's
|
|
211
|
-
* un-yielded loop.
|
|
181
|
+
* Record a dimension change to be applied at the start of the next
|
|
182
|
+
* render's Phase 1, before `_fullRender` runs. The actual
|
|
183
|
+
* `canvas.width = N` assignment (which clears the drawing buffer per
|
|
184
|
+
* the WebGL spec) happens inside {@link applyPendingResize} /
|
|
185
|
+
* {@link beginFrame}, paired in the same synchronous task as the
|
|
186
|
+
* paint that fills the new buffer — eliminating the one-frame blank
|
|
187
|
+
* the compositor would otherwise show in direct/in-process modes
|
|
188
|
+
* (where the GL canvas IS the visible canvas).
|
|
212
189
|
*
|
|
213
|
-
* Multiple
|
|
214
|
-
* to last-write-wins — five rapid width changes from a window
|
|
215
|
-
* drag produce one resize+paint, not five.
|
|
190
|
+
* Multiple calls before the next render coalesce to last-write-wins.
|
|
216
191
|
*/
|
|
217
192
|
requestResize(cssWidth: number, cssHeight: number, dpr: number): void {
|
|
218
193
|
this._pendingResize = { cssWidth, cssHeight, dpr };
|
|
@@ -220,11 +195,7 @@ export class WebGLContextManager {
|
|
|
220
195
|
|
|
221
196
|
/**
|
|
222
197
|
* Apply any pending dimension change recorded by
|
|
223
|
-
* {@link requestResize}.
|
|
224
|
-
* (immediately before each entry's `fullRender`) and by the
|
|
225
|
-
* `snapshotPng` bypass path. Returns `true` when a resize was
|
|
226
|
-
* applied, `false` when there was nothing pending — useful for
|
|
227
|
-
* callers that want to skip a no-op render.
|
|
198
|
+
* {@link requestResize}. Returns `true` when a resize was applied.
|
|
228
199
|
*/
|
|
229
200
|
applyPendingResize(): boolean {
|
|
230
201
|
if (!this._pendingResize) {
|
|
@@ -238,61 +209,99 @@ export class WebGLContextManager {
|
|
|
238
209
|
}
|
|
239
210
|
|
|
240
211
|
/**
|
|
241
|
-
*
|
|
212
|
+
* Prepare the shared GL surface for *this* manager's render. Called
|
|
213
|
+
* by the scheduler immediately before `_fullRender` (and by the
|
|
214
|
+
* `snapshotPng` bypass).
|
|
215
|
+
*
|
|
216
|
+
* - **Own context (1:1):** identical to the old Phase-1 step — apply
|
|
217
|
+
* only a *pending* resize. The context isn't shared, so there is
|
|
218
|
+
* no sibling state to reset and no need to touch the canvas when
|
|
219
|
+
* dimensions are unchanged.
|
|
220
|
+
* - **Shared context (pooled):** a co-tenant chart rendered last and
|
|
221
|
+
* left the shared canvas at *its* dimensions with *its* GL state.
|
|
222
|
+
* Force the canvas to this manager's dimensions and reset the
|
|
223
|
+
* global GL state a sibling might have left dirty (bound
|
|
224
|
+
* framebuffer, viewport, scissor/depth enables) so frames can't
|
|
225
|
+
* bleed across charts. Per-frame draw state (blend, clear) is
|
|
226
|
+
* re-established by the chart's `clearAndSetupFrame`.
|
|
242
227
|
*/
|
|
228
|
+
beginFrame(): void {
|
|
229
|
+
if (this._ownsCtx) {
|
|
230
|
+
this.applyPendingResize();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Honor a queued `requestResize` (it carries the latest dims but
|
|
235
|
+
// hasn't touched the canvas yet), then force the shared canvas
|
|
236
|
+
// to this manager's dims — a co-tenant chart may have left it at
|
|
237
|
+
// its own size. `resize` is a no-op when dims already match.
|
|
238
|
+
const pending = this._pendingResize;
|
|
239
|
+
this._pendingResize = null;
|
|
240
|
+
if (pending) {
|
|
241
|
+
this.resize(pending.cssWidth, pending.cssHeight, pending.dpr);
|
|
242
|
+
} else {
|
|
243
|
+
this.resize(this._cssWidth, this._cssHeight, this._dpr);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const gl = this._ctx.gl;
|
|
247
|
+
const canvas = this._ctx.canvas;
|
|
248
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
249
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
250
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
251
|
+
gl.disable(gl.DEPTH_TEST);
|
|
252
|
+
this._ctx.resetVertexArrayState();
|
|
253
|
+
}
|
|
254
|
+
|
|
243
255
|
get cssWidth(): number {
|
|
244
256
|
return this._cssWidth;
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
/**
|
|
248
|
-
* Last CSS height passed to `resize()`.
|
|
249
|
-
*/
|
|
250
259
|
get cssHeight(): number {
|
|
251
260
|
return this._cssHeight;
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
/**
|
|
255
|
-
* Last device pixel ratio passed to `resize()`.
|
|
256
|
-
*/
|
|
257
263
|
get dpr(): number {
|
|
258
264
|
return this._dpr;
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
clear(): void {
|
|
262
|
-
this.
|
|
263
|
-
this.
|
|
268
|
+
this._ctx.gl.clearColor(0, 0, 0, 0);
|
|
269
|
+
this._ctx.gl.clear(
|
|
270
|
+
this._ctx.gl.COLOR_BUFFER_BIT | this._ctx.gl.DEPTH_BUFFER_BIT,
|
|
271
|
+
);
|
|
264
272
|
this._uploadedCount = 0;
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
/**
|
|
268
276
|
* Register a per-frame hook invoked at the end of each render. In
|
|
269
|
-
* blit-mode rendering
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* no-op.
|
|
274
|
-
*
|
|
275
|
-
* Pass `null` to detach.
|
|
277
|
+
* blit-mode rendering the worker installs a callback that transfers
|
|
278
|
+
* an `ImageBitmap` from the canvas back to the host. In direct mode
|
|
279
|
+
* the callback is null and `endFrame` is a no-op. Pass `null` to
|
|
280
|
+
* detach.
|
|
276
281
|
*/
|
|
277
282
|
setFrameCallback(cb: ((bitmap: ImageBitmap) => void) | null): void {
|
|
278
283
|
this._frameCallback = cb;
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
* `ImageBitmap` to the host. Otherwise no-op — direct-mode
|
|
287
|
-
* rendering has nothing to ship; the visible canvas already holds
|
|
288
|
-
* the drawing buffer.
|
|
287
|
+
* Ship the current canvas contents as an `ImageBitmap` to the host
|
|
288
|
+
* when a frame callback is registered and the surface is an
|
|
289
|
+
* `OffscreenCanvas`. Otherwise no-op (direct mode paints straight to
|
|
290
|
+
* the visible drawing buffer).
|
|
289
291
|
*/
|
|
290
292
|
endFrame(): void {
|
|
291
293
|
if (!this._frameCallback) {
|
|
292
294
|
return;
|
|
293
295
|
}
|
|
294
296
|
|
|
295
|
-
|
|
297
|
+
// A context lost mid-frame (our own `destroy`, or a browser
|
|
298
|
+
// eviction between paint and present) makes
|
|
299
|
+
// `transferToImageBitmap` throw. Bail quietly.
|
|
300
|
+
if (this.isContextLost()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const canvas = this._ctx.canvas as
|
|
296
305
|
| OffscreenCanvas
|
|
297
306
|
| (HTMLCanvasElement & { transferToImageBitmap?: never });
|
|
298
307
|
if (
|
|
@@ -308,30 +317,17 @@ export class WebGLContextManager {
|
|
|
308
317
|
|
|
309
318
|
/**
|
|
310
319
|
* Resolve when every GL command submitted up to this call has been
|
|
311
|
-
* executed by the GPU.
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* and polls `clientWaitSync` with a zero timeout, yielding to the
|
|
315
|
-
* task queue between polls. The first poll passes
|
|
316
|
-
* `SYNC_FLUSH_COMMANDS_BIT` so the fence becomes reachable without
|
|
317
|
-
* a separate `gl.flush()`.
|
|
318
|
-
*
|
|
319
|
-
* On WebGL1 there is no fenceSync; we fall back to the blocking
|
|
320
|
-
* `gl.finish()`. This is acceptable in a worker — never call this
|
|
321
|
-
* from the main thread on a heavy frame.
|
|
322
|
-
*
|
|
323
|
-
* Used as a per-frame "GPU is idle" barrier so callers can serialize
|
|
324
|
-
* follow-on work (`endFrame` snapshot, present roundtrip, the next
|
|
325
|
-
* chunk upload) against actual GPU completion instead of the
|
|
326
|
-
* implicit, implementation-defined timing of `transferToImageBitmap`.
|
|
320
|
+
* executed by the GPU. WebGL2 issues a `fenceSync` and polls
|
|
321
|
+
* `clientWaitSync` (yielding to the task queue between polls); WebGL1
|
|
322
|
+
* falls back to a blocking `gl.finish()` (acceptable in a worker).
|
|
327
323
|
*/
|
|
328
324
|
async awaitGpuFence(): Promise<void> {
|
|
329
|
-
if (!this.
|
|
330
|
-
this.
|
|
325
|
+
if (!this._ctx.isWebGL2) {
|
|
326
|
+
this._ctx.gl.finish();
|
|
331
327
|
return;
|
|
332
328
|
}
|
|
333
329
|
|
|
334
|
-
const gl = this.
|
|
330
|
+
const gl = this._ctx.gl as WebGL2RenderingContext;
|
|
335
331
|
const fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
336
332
|
if (!fence) {
|
|
337
333
|
gl.finish();
|
|
@@ -356,6 +352,10 @@ export class WebGLContextManager {
|
|
|
356
352
|
|
|
357
353
|
flags = 0;
|
|
358
354
|
await this._yieldToTask();
|
|
355
|
+
|
|
356
|
+
if (this._destroyed) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
359
|
}
|
|
360
360
|
} finally {
|
|
361
361
|
gl.deleteSync(fence);
|
|
@@ -367,48 +367,54 @@ export class WebGLContextManager {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/**
|
|
370
|
-
* Yield to the task queue between fence polls.
|
|
371
|
-
*
|
|
372
|
-
* ~4ms
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* `addEventListener(..., { once: true })` is used over
|
|
377
|
-
* `port1.onmessage = ...` so concurrent in-flight resumes (should
|
|
378
|
-
* any path ever introduce them) cannot clobber each other's
|
|
379
|
-
* resolvers — the previous module-level singleton lost a resolver
|
|
380
|
-
* on every overlap and hung one chart's `draw()` indefinitely.
|
|
370
|
+
* Yield to the task queue between fence polls. `setTimeout(0)` is
|
|
371
|
+
* avoided because Chromium clamps nested worker `setTimeout` to
|
|
372
|
+
* ~4ms; a reused per-instance `MessageChannel` resumes in the next
|
|
373
|
+
* task with sub-ms latency. `{ once: true }` over `onmessage = ...`
|
|
374
|
+
* so concurrent resumes can't clobber each other's resolvers.
|
|
381
375
|
*/
|
|
382
376
|
private _yieldToTask(): Promise<void> {
|
|
383
377
|
if (!this._yieldChannel) {
|
|
384
378
|
this._yieldChannel = new MessageChannel();
|
|
385
|
-
// `addEventListener` does not auto-start a `MessagePort` —
|
|
386
|
-
// only `onmessage = ...` does. Start once at allocation so
|
|
387
|
-
// posted resumes are actually delivered.
|
|
388
379
|
this._yieldChannel.port1.start();
|
|
389
380
|
}
|
|
390
381
|
|
|
391
382
|
return new Promise<void>((resolve) => {
|
|
392
383
|
const ch = this._yieldChannel!;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
384
|
+
this._pendingYieldResolve = resolve;
|
|
385
|
+
ch.port1.addEventListener(
|
|
386
|
+
"message",
|
|
387
|
+
() => {
|
|
388
|
+
this._pendingYieldResolve = null;
|
|
389
|
+
resolve();
|
|
390
|
+
},
|
|
391
|
+
{ once: true },
|
|
392
|
+
);
|
|
396
393
|
ch.port2.postMessage(null);
|
|
397
394
|
});
|
|
398
395
|
}
|
|
399
396
|
|
|
400
397
|
destroy(): void {
|
|
398
|
+
this._destroyed = true;
|
|
399
|
+
this._frameCallback = null;
|
|
400
|
+
this._disposeRestore();
|
|
401
401
|
this._buffers.releaseAll();
|
|
402
|
-
this.
|
|
402
|
+
if (this._pendingYieldResolve) {
|
|
403
|
+
const resolve = this._pendingYieldResolve;
|
|
404
|
+
this._pendingYieldResolve = null;
|
|
405
|
+
resolve();
|
|
406
|
+
}
|
|
407
|
+
|
|
403
408
|
if (this._yieldChannel) {
|
|
404
409
|
this._yieldChannel.port1.close();
|
|
405
410
|
this._yieldChannel.port2.close();
|
|
406
411
|
this._yieldChannel = null;
|
|
407
412
|
}
|
|
408
413
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
414
|
+
// Only lose the GL context when we own it. A shared context
|
|
415
|
+
// outlives any one tenant — the pool destroys it.
|
|
416
|
+
if (this._ownsCtx) {
|
|
417
|
+
this._ctx.destroy();
|
|
412
418
|
}
|
|
413
419
|
}
|
|
414
420
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import { GLContext } from "./gl-context";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renderer-scope pool of shared {@link GLContext}s for pooled blit mode.
|
|
17
|
+
*
|
|
18
|
+
* The browser caps live WebGL contexts per agent (~16 in Chromium) and
|
|
19
|
+
* force-loses the oldest past that cap. A page with more than ~16 charts
|
|
20
|
+
* therefore can't give each its own context. This pool caps the number
|
|
21
|
+
* of live contexts at `size`, distributing renderers across them
|
|
22
|
+
* round-robin (sticky for a renderer's lifetime — a renderer keeps the
|
|
23
|
+
* same context so its resident GPU buffers and any cached textures/FBOs
|
|
24
|
+
* stay valid frame to frame). N charts render through K = `size`
|
|
25
|
+
* contexts; the scheduler serializes renders that land on the same
|
|
26
|
+
* context.
|
|
27
|
+
*
|
|
28
|
+
* Pooling applies to blit mode only: direct mode renders into the host's
|
|
29
|
+
* transferred visible canvas, which is permanently 1:1 with a context.
|
|
30
|
+
*
|
|
31
|
+
* One pool per renderer scope (the worker, or the main thread in
|
|
32
|
+
* in-process mode). Contexts are created lazily on first `acquire` and
|
|
33
|
+
* live until renderer-scope teardown — like the contexts they replace,
|
|
34
|
+
* they are not refcounted, because a shared context outlives any single
|
|
35
|
+
* borrowing renderer.
|
|
36
|
+
*/
|
|
37
|
+
export class ContextPool {
|
|
38
|
+
private _size: number;
|
|
39
|
+
private _precompile: boolean;
|
|
40
|
+
private _contexts: GLContext[] = [];
|
|
41
|
+
private _next = 0;
|
|
42
|
+
|
|
43
|
+
constructor(size: number, options: { precompile?: boolean } = {}) {
|
|
44
|
+
this._size = Math.max(1, size);
|
|
45
|
+
this._precompile = options.precompile ?? false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Borrow a context for a new renderer. Fills the pool to `size`
|
|
50
|
+
* before reusing, then round-robins. A dead context (browser
|
|
51
|
+
* eviction) is replaced in place so a borrower never receives a lost
|
|
52
|
+
* context.
|
|
53
|
+
*/
|
|
54
|
+
acquire(): GLContext {
|
|
55
|
+
let ctx: GLContext;
|
|
56
|
+
if (this._contexts.length < this._size) {
|
|
57
|
+
ctx = this._make();
|
|
58
|
+
this._contexts.push(ctx);
|
|
59
|
+
} else {
|
|
60
|
+
const idx = this._next % this._contexts.length;
|
|
61
|
+
this._next++;
|
|
62
|
+
if (this._contexts[idx].isContextLost()) {
|
|
63
|
+
this._contexts[idx] = this._make();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ctx = this._contexts[idx];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return ctx;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private _make(): GLContext {
|
|
73
|
+
// Initial size is arbitrary — every render resizes the shared
|
|
74
|
+
// canvas to the borrowing renderer's dimensions in `beginFrame`.
|
|
75
|
+
const canvas = new OffscreenCanvas(1, 1);
|
|
76
|
+
return new GLContext(canvas, { precompile: this._precompile });
|
|
77
|
+
}
|
|
78
|
+
}
|