@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
|
@@ -0,0 +1,191 @@
|
|
|
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 { ShaderRegistry } from "./shader-registry";
|
|
14
|
+
import { SHADER_MANIFEST } from "./shader-manifest";
|
|
15
|
+
|
|
16
|
+
export type WebGLCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The unit the browser actually counts against its per-agent WebGL
|
|
20
|
+
* context cap (~16 in Chromium): one canvas + one GL context + the
|
|
21
|
+
* shader programs compiled against it.
|
|
22
|
+
*
|
|
23
|
+
* Factored out of {@link WebGLContextManager} so that *many* managers
|
|
24
|
+
* (one per chart) can share *one* GL context. The browser limit is on
|
|
25
|
+
* contexts, not on the buffers/programs living inside one — a single
|
|
26
|
+
* context can host an arbitrary number of independent
|
|
27
|
+
* `WebGLBuffer`s/programs (bounded only by memory). So N charts can
|
|
28
|
+
* render through K ≪ N contexts as long as each keeps its own
|
|
29
|
+
* `BufferPool` (buffers are keyed by name; two charts would otherwise
|
|
30
|
+
* collide) and renders are serialized per context (the shared drawing
|
|
31
|
+
* buffer can hold one chart's frame at a time). See
|
|
32
|
+
* [context-pool.ts](./context-pool.ts) and the scheduler's per-backend
|
|
33
|
+
* grouping in [../render/scheduler.ts](../render/scheduler.ts).
|
|
34
|
+
*
|
|
35
|
+
* Shaders are shared per context: programs are immutable GLSL once
|
|
36
|
+
* compiled (uniforms are set per draw), so co-tenant charts reuse them
|
|
37
|
+
* and only pay the compile cost once per context.
|
|
38
|
+
*/
|
|
39
|
+
let NEXT_CONTEXT_ID = 1;
|
|
40
|
+
|
|
41
|
+
export class GLContext {
|
|
42
|
+
readonly id: number;
|
|
43
|
+
readonly canvas: WebGLCanvas;
|
|
44
|
+
readonly gl: WebGL2RenderingContext | WebGLRenderingContext;
|
|
45
|
+
readonly isWebGL2: boolean;
|
|
46
|
+
shaders: ShaderRegistry;
|
|
47
|
+
private _maxVertexAttribs: number;
|
|
48
|
+
private _angle: ANGLE_instanced_arrays | null = null;
|
|
49
|
+
private _precompile: boolean;
|
|
50
|
+
private _destroyed = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-tenant hooks fired after a `webglcontextrestored`. Each
|
|
54
|
+
* {@link WebGLContextManager} on this context registers one that
|
|
55
|
+
* rebuilds its `BufferPool` (the shaders are rebuilt here, once, for
|
|
56
|
+
* all tenants). On an intentional {@link destroy} these are dropped
|
|
57
|
+
* without firing.
|
|
58
|
+
*/
|
|
59
|
+
private _onRestore = new Set<() => void>();
|
|
60
|
+
|
|
61
|
+
constructor(canvas: WebGLCanvas, options: { precompile?: boolean } = {}) {
|
|
62
|
+
this.id = NEXT_CONTEXT_ID++;
|
|
63
|
+
this.canvas = canvas;
|
|
64
|
+
this._precompile = options.precompile ?? false;
|
|
65
|
+
const gl2 = canvas.getContext("webgl2", {
|
|
66
|
+
antialias: true,
|
|
67
|
+
alpha: true,
|
|
68
|
+
premultipliedAlpha: false,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (gl2) {
|
|
72
|
+
this.gl = gl2 as WebGL2RenderingContext;
|
|
73
|
+
this.isWebGL2 = true;
|
|
74
|
+
} else {
|
|
75
|
+
const gl1 = canvas.getContext("webgl", {
|
|
76
|
+
antialias: true,
|
|
77
|
+
alpha: true,
|
|
78
|
+
premultipliedAlpha: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!gl1) {
|
|
82
|
+
throw new Error("WebGL is not supported");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.gl = gl1 as WebGLRenderingContext;
|
|
86
|
+
this.isWebGL2 = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.shaders = new ShaderRegistry(this.gl);
|
|
90
|
+
if (this._precompile) {
|
|
91
|
+
this.shaders.precompile(SHADER_MANIFEST);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._maxVertexAttribs = this.gl.getParameter(
|
|
95
|
+
this.gl.MAX_VERTEX_ATTRIBS,
|
|
96
|
+
) as number;
|
|
97
|
+
if (!this.isWebGL2) {
|
|
98
|
+
this._angle = this.gl.getExtension("ANGLE_instanced_arrays");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Both `HTMLCanvasElement` and `OffscreenCanvas` dispatch
|
|
102
|
+
// `webglcontextlost` / `webglcontextrestored` on the canvas.
|
|
103
|
+
(canvas as EventTarget).addEventListener(
|
|
104
|
+
"webglcontextlost",
|
|
105
|
+
(e: Event) => {
|
|
106
|
+
// Don't reserve the slot for a restore once we've
|
|
107
|
+
// intentionally torn the context down — `destroy()`
|
|
108
|
+
// calls `loseContext()`, which fires this event.
|
|
109
|
+
if (this._destroyed) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
(canvas as EventTarget).addEventListener("webglcontextrestored", () => {
|
|
118
|
+
// Rebuild the shared program cache once for the whole
|
|
119
|
+
// context, then let each tenant rebuild its own buffers.
|
|
120
|
+
this.shaders.releaseAll();
|
|
121
|
+
this.shaders = new ShaderRegistry(this.gl);
|
|
122
|
+
if (this._precompile) {
|
|
123
|
+
this.shaders.precompile(SHADER_MANIFEST);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const cb of this._onRestore) {
|
|
127
|
+
cb();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Return every vertex-attribute slot to its default state: disabled,
|
|
134
|
+
* with a zero instance divisor. Vertex-array state (enable flags,
|
|
135
|
+
* pointers, divisors) is global to a context's default VAO — there
|
|
136
|
+
* are no per-chart VAOs in this codebase — so when many charts share
|
|
137
|
+
* one pooled context, a slot a prior chart enabled stays enabled for
|
|
138
|
+
* the next chart, now pointing at a buffer the prior chart's
|
|
139
|
+
* teardown deleted. Its next `drawArraysInstanced` then fails GL
|
|
140
|
+
* validation: "no buffer is bound to enabled attribute", silently
|
|
141
|
+
* dropping the draw.
|
|
142
|
+
*
|
|
143
|
+
* Glyphs already (re)enable + bind + set the divisor for every slot
|
|
144
|
+
* they use on each frame, so clearing to the default here is a safe
|
|
145
|
+
* baseline — it only removes the *leftover* enables a sibling chart
|
|
146
|
+
* left behind. Called from {@link WebGLContextManager.beginFrame} in
|
|
147
|
+
* shared (pooled) mode only; the 1:1 default never shares a context
|
|
148
|
+
* and so keeps its original, reset-free path.
|
|
149
|
+
*/
|
|
150
|
+
resetVertexArrayState(): void {
|
|
151
|
+
const gl = this.gl;
|
|
152
|
+
for (let i = 0; i < this._maxVertexAttribs; i++) {
|
|
153
|
+
gl.disableVertexAttribArray(i);
|
|
154
|
+
if (this.isWebGL2) {
|
|
155
|
+
(gl as WebGL2RenderingContext).vertexAttribDivisor(i, 0);
|
|
156
|
+
} else {
|
|
157
|
+
this._angle?.vertexAttribDivisorANGLE(i, 0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Register a tenant's post-restore rebuild hook. Returns a disposer.
|
|
164
|
+
*/
|
|
165
|
+
onRestore(cb: () => void): () => void {
|
|
166
|
+
this._onRestore.add(cb);
|
|
167
|
+
return () => this._onRestore.delete(cb);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* True once lost — either our own {@link destroy} or a browser
|
|
172
|
+
* eviction of the oldest context past the per-agent cap.
|
|
173
|
+
*/
|
|
174
|
+
isContextLost(): boolean {
|
|
175
|
+
return this._destroyed || this.gl.isContextLost();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get destroyed(): boolean {
|
|
179
|
+
return this._destroyed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
destroy(): void {
|
|
183
|
+
this._destroyed = true;
|
|
184
|
+
this._onRestore.clear();
|
|
185
|
+
this.shaders.releaseAll();
|
|
186
|
+
const ext = this.gl.getExtension("WEBGL_lose_context");
|
|
187
|
+
if (ext) {
|
|
188
|
+
ext.loseContext();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -108,6 +108,12 @@ export function createQuadCornerBuffer(gl: GL): WebGLBuffer {
|
|
|
108
108
|
* paint zero data). No-op return `true` when `attr` is negative
|
|
109
109
|
* (optimized-out attribute — drawing the rest is still valid).
|
|
110
110
|
*
|
|
111
|
+
* `firstInstance` byte-offsets the attribute pointer so the draw's
|
|
112
|
+
* instance 0 reads element `firstInstance` of the buffer — the WebGL
|
|
113
|
+
* substitute for a `baseInstance` draw parameter. Faceted renderers
|
|
114
|
+
* use it to draw a contiguous per-facet sub-range of a shared
|
|
115
|
+
* instance buffer.
|
|
116
|
+
*
|
|
111
117
|
* Render-path uses `peek`, never `getOrCreate`: the latter recreates
|
|
112
118
|
* with zero-initialized contents when `_totalCapacity` has grown past
|
|
113
119
|
* the current buffer, which would wipe the previous draw's vertex
|
|
@@ -120,6 +126,7 @@ export function bindInstancedFloatAttr(
|
|
|
120
126
|
attr: number,
|
|
121
127
|
name: string,
|
|
122
128
|
components: number,
|
|
129
|
+
firstInstance = 0,
|
|
123
130
|
): boolean {
|
|
124
131
|
if (attr < 0) {
|
|
125
132
|
return true;
|
|
@@ -133,7 +140,14 @@ export function bindInstancedFloatAttr(
|
|
|
133
140
|
const gl: GL = glManager.gl;
|
|
134
141
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
|
|
135
142
|
gl.enableVertexAttribArray(attr);
|
|
136
|
-
gl.vertexAttribPointer(
|
|
143
|
+
gl.vertexAttribPointer(
|
|
144
|
+
attr,
|
|
145
|
+
components,
|
|
146
|
+
gl.FLOAT,
|
|
147
|
+
false,
|
|
148
|
+
0,
|
|
149
|
+
firstInstance * components * Float32Array.BYTES_PER_ELEMENT,
|
|
150
|
+
);
|
|
137
151
|
instancing.setDivisor(attr, 1);
|
|
138
152
|
return true;
|
|
139
153
|
}
|
|
@@ -40,9 +40,12 @@ export function dispatch(r: WorkerRenderer, msg: ControlMsg): void {
|
|
|
40
40
|
case "redraw":
|
|
41
41
|
r.redraw();
|
|
42
42
|
break;
|
|
43
|
+
case "deselect":
|
|
44
|
+
r.chartImpl.deselect?.();
|
|
45
|
+
break;
|
|
43
46
|
case "resize":
|
|
44
47
|
r.resize(msg.cssWidth, msg.cssHeight, msg.dpr);
|
|
45
|
-
r.
|
|
48
|
+
r.redrawAck(msg.msgId);
|
|
46
49
|
break;
|
|
47
50
|
case "clear":
|
|
48
51
|
r.clear();
|
|
@@ -16,6 +16,8 @@ import type { Client, Table, View } from "@perspective-dev/client";
|
|
|
16
16
|
|
|
17
17
|
import type * as wasm_module_type from "@perspective-dev/viewer/dist/wasm/perspective-viewer.js";
|
|
18
18
|
import { WebGLContextManager } from "../webgl/context-manager";
|
|
19
|
+
import { ContextPool } from "../webgl/context-pool";
|
|
20
|
+
import { RENDER_CONTEXT_POOL_SIZE } from "../config";
|
|
19
21
|
import { ChartImplementation } from "../charts/chart";
|
|
20
22
|
import { ZoomController } from "../interaction/zoom-controller";
|
|
21
23
|
import {
|
|
@@ -37,7 +39,7 @@ import { viewToColumnDataMap } from "../data/view-reader";
|
|
|
37
39
|
import { loadFontDeduped } from "./font-loader";
|
|
38
40
|
import { dispatch } from "./dispatch";
|
|
39
41
|
import { installSessionHost } from "./session-host";
|
|
40
|
-
import { deferIfDraining } from "../render/scheduler";
|
|
42
|
+
import { deferIfDraining, unregister } from "../render/scheduler";
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Sentinel thrown inside the `with_typed_arrays` callback when a newer
|
|
@@ -76,6 +78,25 @@ async function resolveChartImpl(
|
|
|
76
78
|
return await factory();
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Renderer-scope shared context pool for pooled blit mode. Lazily
|
|
83
|
+
* created on first use so non-blit / non-pooled scopes never allocate
|
|
84
|
+
* one. One per renderer scope (the worker, or the main thread in
|
|
85
|
+
* in-process mode) — `WorkerRenderer`s in the same scope share its K
|
|
86
|
+
* contexts.
|
|
87
|
+
*/
|
|
88
|
+
let CONTEXT_POOL: ContextPool | null = null;
|
|
89
|
+
|
|
90
|
+
function getContextPool(precompile: boolean): ContextPool {
|
|
91
|
+
if (!CONTEXT_POOL) {
|
|
92
|
+
CONTEXT_POOL = new ContextPool(RENDER_CONTEXT_POOL_SIZE, {
|
|
93
|
+
precompile,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return CONTEXT_POOL;
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
export class WorkerRenderer {
|
|
80
101
|
chartImpl: ChartImplementation;
|
|
81
102
|
glManager: WebGLContextManager;
|
|
@@ -131,20 +152,35 @@ export class WorkerRenderer {
|
|
|
131
152
|
|
|
132
153
|
this.chartImpl = new ImplClass();
|
|
133
154
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
// Three surfaces, by mode:
|
|
156
|
+
// - direct: the host's transferred `.webgl-canvas` (1:1 with a
|
|
157
|
+
// context, permanently).
|
|
158
|
+
// - pooled blit: borrow one of K shared contexts so live-
|
|
159
|
+
// context count stays bounded regardless of chart count.
|
|
160
|
+
// - unpooled blit: a private offscreen surface per chart.
|
|
161
|
+
// Blit modes omit `msg.glCanvas`; the renderer ships each frame
|
|
162
|
+
// as an `ImageBitmap` via the `endFrame` callback wired below.
|
|
163
|
+
const pooled =
|
|
164
|
+
msg.renderMode === "blit" &&
|
|
165
|
+
!msg.glCanvas &&
|
|
166
|
+
RENDER_CONTEXT_POOL_SIZE > 0;
|
|
167
|
+
|
|
168
|
+
if (pooled) {
|
|
169
|
+
this.glManager = new WebGLContextManager(
|
|
170
|
+
getContextPool(msg.precompileShaders ?? false).acquire(),
|
|
143
171
|
);
|
|
172
|
+
} else {
|
|
173
|
+
const glCanvas =
|
|
174
|
+
msg.glCanvas ??
|
|
175
|
+
new OffscreenCanvas(
|
|
176
|
+
Math.max(1, Math.round(msg.cssWidth * msg.dpr)),
|
|
177
|
+
Math.max(1, Math.round(msg.cssHeight * msg.dpr)),
|
|
178
|
+
);
|
|
144
179
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
this.glManager = new WebGLContextManager(glCanvas, {
|
|
181
|
+
precompile: msg.precompileShaders ?? false,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
148
184
|
|
|
149
185
|
if (msg.renderMode === "blit") {
|
|
150
186
|
this.glManager.setFrameCallback((bitmap) => {
|
|
@@ -259,16 +295,23 @@ export class WorkerRenderer {
|
|
|
259
295
|
try {
|
|
260
296
|
await viewToColumnDataMap(
|
|
261
297
|
this.view,
|
|
262
|
-
async (cols) => {
|
|
298
|
+
async (cols, deliveredRows) => {
|
|
263
299
|
if (this._renderGen !== myGen) {
|
|
264
300
|
throw new StaleGenerationError();
|
|
265
301
|
}
|
|
266
302
|
|
|
303
|
+
// `totalRows` came from a SEPARATE `num_rows()`
|
|
304
|
+
// round-trip; a fast-updating table with deletions
|
|
305
|
+
// can shrink the view before the Arrow fetch, which
|
|
306
|
+
// clamps its window silently. Iterating to the stale
|
|
307
|
+
// count reads typed arrays out-of-bounds (`undefined`
|
|
308
|
+
// values no validity bitmap covers) — clamp to what
|
|
309
|
+
// was actually delivered.
|
|
267
310
|
await this.chartImpl.uploadAndRender(
|
|
268
311
|
this.glManager,
|
|
269
312
|
cols,
|
|
270
313
|
0,
|
|
271
|
-
totalRows,
|
|
314
|
+
Math.min(totalRows, deliveredRows),
|
|
272
315
|
);
|
|
273
316
|
},
|
|
274
317
|
{ end_row: totalRows, float32: msg.options.float32 },
|
|
@@ -291,6 +334,24 @@ export class WorkerRenderer {
|
|
|
291
334
|
this.chartImpl.requestRender(this.glManager);
|
|
292
335
|
}
|
|
293
336
|
|
|
337
|
+
/**
|
|
338
|
+
* `redraw`, acked to the host AFTER the frame's present completes —
|
|
339
|
+
* the reply backing the viewer's presize contract (`ResizeAckMsg`):
|
|
340
|
+
* `plugin.resize()` must not resolve until the resized frame is
|
|
341
|
+
* actually on screen. A resize landing mid-present defers its
|
|
342
|
+
* canvas mutation (`deferIfDraining`), but the redraw's own frame
|
|
343
|
+
* applies pending dimensions in `beginFrame`, so the present this
|
|
344
|
+
* awaits IS at the new size. Always acks — a failed present or a
|
|
345
|
+
* torn-down chart resolves the host's await rather than stranding
|
|
346
|
+
* it (completion, not success, is the contract).
|
|
347
|
+
*/
|
|
348
|
+
redrawAck(msgId: number): void {
|
|
349
|
+
this.chartImpl
|
|
350
|
+
.requestRender(this.glManager)
|
|
351
|
+
.catch(() => {})
|
|
352
|
+
.finally(() => this.post({ kind: "resizeAck", msgId }));
|
|
353
|
+
}
|
|
354
|
+
|
|
294
355
|
resize(cssWidth: number, cssHeight: number, dpr: number): void {
|
|
295
356
|
// `glManager.resize` would set `canvas.width = N`, which the
|
|
296
357
|
// spec mandates clears the drawing buffer immediately. In
|
|
@@ -528,13 +589,19 @@ export class WorkerRenderer {
|
|
|
528
589
|
* Composite the three layers into a single PNG `Blob`.
|
|
529
590
|
*/
|
|
530
591
|
async snapshotPng(): Promise<Blob> {
|
|
531
|
-
// Snapshot bypasses the scheduler's drain, so it must
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
592
|
+
// Snapshot bypasses the scheduler's drain, so it must mirror
|
|
593
|
+
// the per-frame prep itself — apply any pending resize, and in
|
|
594
|
+
// pooled blit mode size the shared canvas to this chart and
|
|
595
|
+
// reset shared GL state — otherwise a snapshot taken after a
|
|
596
|
+
// resize message but before the next drain (or after a co-tenant
|
|
597
|
+
// chart left the shared canvas at its own size) would render at
|
|
598
|
+
// the wrong dimensions.
|
|
599
|
+
this.glManager.beginFrame();
|
|
600
|
+
// `renderFrameSync` (not `_fullRender`) so the gridline + chrome
|
|
601
|
+
// 2D canvases are actually painted here — the scheduler normally
|
|
602
|
+
// defers those to a post-fence flush, but the snapshot path
|
|
603
|
+
// bypasses the scheduler and composites the 2D layers below.
|
|
604
|
+
this.chartImpl.renderFrameSync(this.glManager);
|
|
538
605
|
const gl = this.glManager.gl;
|
|
539
606
|
const glCanvas = gl.canvas as OffscreenCanvas;
|
|
540
607
|
const w = glCanvas.width;
|
|
@@ -574,6 +641,11 @@ export class WorkerRenderer {
|
|
|
574
641
|
}
|
|
575
642
|
|
|
576
643
|
destroy(): void {
|
|
644
|
+
// Drop any frame this manager still has queued in the
|
|
645
|
+
// module-level scheduler *before* losing its GL context, so a
|
|
646
|
+
// next-RAF `drain()` can't paint/present against a dead
|
|
647
|
+
// context (the "scheduler: present failed" path).
|
|
648
|
+
unregister(this.glManager);
|
|
577
649
|
this.chartImpl.destroy();
|
|
578
650
|
this.glManager.destroy();
|
|
579
651
|
}
|
|
@@ -21,6 +21,20 @@ import { dispatch } from "./dispatch";
|
|
|
21
21
|
|
|
22
22
|
const RENDERERS = new Map<number, WorkerRenderer>();
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Sessions whose `destroy` arrived before their async `bootstrap`
|
|
26
|
+
* resolved. Bootstrap does multiple awaits (wasm `initSync`, font
|
|
27
|
+
* loads, `open_table`, lazy chart-impl import); a host that connects
|
|
28
|
+
* then immediately disconnects a viewer can post `init` and `destroy`
|
|
29
|
+
* back-to-back, with `destroy` landing while the renderer is still
|
|
30
|
+
* being built. The plain `RENDERERS.get(sessionId)` lookup in the
|
|
31
|
+
* destroy branch would miss it and the bootstrap would then register a
|
|
32
|
+
* renderer (and its WebGL context) that nothing ever tears down —
|
|
33
|
+
* leaking one context per raced toggle until the browser evicts the
|
|
34
|
+
* oldest. We tombstone the id here and honor it when bootstrap lands.
|
|
35
|
+
*/
|
|
36
|
+
const PENDING_DESTROY = new Set<number>();
|
|
37
|
+
|
|
24
38
|
function postSession(
|
|
25
39
|
sessionId: number,
|
|
26
40
|
msg: WorkerMsg,
|
|
@@ -89,9 +103,18 @@ export function installSessionHost(
|
|
|
89
103
|
|
|
90
104
|
bootstrap(msg as InitMsg, makeSessionPort(sessionId))
|
|
91
105
|
.then((r) => {
|
|
106
|
+
// A `destroy` raced ahead of bootstrap completing —
|
|
107
|
+
// honor it now instead of leaking the freshly-built
|
|
108
|
+
// renderer + its WebGL context.
|
|
109
|
+
if (PENDING_DESTROY.delete(sessionId)) {
|
|
110
|
+
dispatch(r, { kind: "destroy" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
RENDERERS.set(sessionId, r);
|
|
93
115
|
})
|
|
94
116
|
.catch((err) => {
|
|
117
|
+
PENDING_DESTROY.delete(sessionId);
|
|
95
118
|
postSession(sessionId, {
|
|
96
119
|
kind: "error",
|
|
97
120
|
message: String(err),
|
|
@@ -105,6 +128,12 @@ export function installSessionHost(
|
|
|
105
128
|
if (r) {
|
|
106
129
|
dispatch(r, msg);
|
|
107
130
|
RENDERERS.delete(sessionId);
|
|
131
|
+
} else {
|
|
132
|
+
// Renderer not registered yet — bootstrap is still in
|
|
133
|
+
// flight. Tombstone the id so the bootstrap `.then`
|
|
134
|
+
// tears it down on arrival instead of registering a
|
|
135
|
+
// leaked context.
|
|
136
|
+
PENDING_DESTROY.add(sessionId);
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
return;
|