uplot-webgpu 0.1.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/CANVAS_PROXY.md +602 -0
- package/README.md +854 -0
- package/favicon.ico +0 -0
- package/index.html +14 -0
- package/index.js +21 -0
- package/original/paths.canvas2d/bars.js +252 -0
- package/original/paths.canvas2d/catmullRomCentrip.js +125 -0
- package/original/paths.canvas2d/linear.js +170 -0
- package/original/paths.canvas2d/monotoneCubic.js +68 -0
- package/original/paths.canvas2d/points.js +66 -0
- package/original/paths.canvas2d/spline.js +103 -0
- package/original/paths.canvas2d/stepped.js +124 -0
- package/original/paths.canvas2d/utils.js +301 -0
- package/original/uPlot.canvas2d.js +3548 -0
- package/package.json +110 -0
- package/paths/bars.js +253 -0
- package/paths/catmullRomCentrip.js +126 -0
- package/paths/linear.js +171 -0
- package/paths/monotoneCubic.js +69 -0
- package/paths/points.js +67 -0
- package/paths/spline.js +104 -0
- package/paths/stepped.js +125 -0
- package/paths/utils.js +301 -0
- package/scripts/uPlot.css +168 -0
- package/scripts/uPlot.d.ts +26 -0
- package/scripts/uPlot.js +3687 -0
- package/scripts/utils/dom.js +124 -0
- package/scripts/utils/domClasses.js +22 -0
- package/scripts/utils/feats.js +13 -0
- package/scripts/utils/fmtDate.js +398 -0
- package/scripts/utils/opts.js +844 -0
- package/scripts/utils/strings.js +22 -0
- package/scripts/utils/sync.js +27 -0
- package/scripts/utils/utils.js +692 -0
- package/scripts/webgpu/GPUPath.d.ts +46 -0
- package/scripts/webgpu/GPUPath.js +633 -0
- package/scripts/webgpu/GPUPath.ts +634 -0
- package/scripts/webgpu/WebGPURenderer.d.ts +176 -0
- package/scripts/webgpu/WebGPURenderer.js +4256 -0
- package/scripts/webgpu/WebGPURenderer.ts +4257 -0
- package/scripts/webgpu/browserSmokeHarness.js +105 -0
- package/scripts/webgpu/exporters.d.ts +8 -0
- package/scripts/webgpu/exporters.js +212 -0
- package/scripts/webgpu/shaders.d.ts +2 -0
- package/scripts/webgpu/shaders.js +76 -0
- package/scripts/webgpu/shaders.ts +77 -0
- package/scripts/webgpu/smokeTest.d.ts +2 -0
- package/scripts/webgpu/smokeTest.js +144 -0
- package/scripts/webgpu/webgpu-ambient.d.ts +41 -0
- package/tinybuild.config.js +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
# uPlot WebGPU
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm i uplot-webgpu
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
`uplot-webgpu` is a drop-in [uPlot](https://github.com/leeoniya/uPlot)-compatible WebGPU renderer for high-throughput browser charts.
|
|
8
|
+
|
|
9
|
+
It keeps the normal uPlot setup pattern, options, legends, cursors, scales, axes, styling, selection zoom, and plugin hooks, while moving the heavy drawing path onto WebGPU.
|
|
10
|
+
|
|
11
|
+
The demo tests ~4 million points animating on 8 charts and handily achieves >60fps on a decent laptop.
|
|
12
|
+
|
|
13
|
+
Use it when you want uPlot but need more headroom for large live buffers, dense lines, heatmaps, ribbons, dashboards, animation-heavy views, or GPU-resident data workflows.
|
|
14
|
+
|
|
15
|
+
The base uPlot Canvas2D renderer is already extremely fast. For small static charts, Canvas2D may still load faster and feel just as good or better. WebGPU has a higher startup cost because the browser must create a GPU device and compile render pipelines asynchronously. The WebGPU path is designed to pay off when charts are large, repeated, animated, or connected to other GPU work.
|
|
16
|
+
|
|
17
|
+
## When to use it
|
|
18
|
+
|
|
19
|
+
Use `uplot-webgpu` when:
|
|
20
|
+
|
|
21
|
+
1. You want to stream hundreds of thousands or millions of points.
|
|
22
|
+
2. You want many large charts animating at the same time.
|
|
23
|
+
3. You want lower CPU rendering pressure during repeated redraws.
|
|
24
|
+
4. You want to reuse signal buffers across WebGPU compute, analysis, or custom shader passes.
|
|
25
|
+
5. You want a uPlot-compatible chart inside a larger WebGPU dashboard.
|
|
26
|
+
|
|
27
|
+
Use base uPlot when:
|
|
28
|
+
|
|
29
|
+
1. Your charts are small or mostly static.
|
|
30
|
+
2. Canvas2D performance is already fine.
|
|
31
|
+
3. Package size matters more than rendering throughput.
|
|
32
|
+
4. You do not need WebGPU integration for your data buffers.
|
|
33
|
+
|
|
34
|
+
Bundled and minified, the core package is currently around `132kb`. It is larger than base uPlot because it includes a WebGPU renderer, shader programs, buffer management, and a Canvas2D-compatible drawing layer for uPlot internals and plugins.
|
|
35
|
+
|
|
36
|
+
Compatibility is basically 1:1 minus some missing Canvas API features for further customization. The library includes an actual proxy Canvas API that runs on a WebGPU context but it is not full-featured. We tested 116 different charts with zero functional or visual differences.
|
|
37
|
+
|
|
38
|
+
## Browser support
|
|
39
|
+
|
|
40
|
+
`uplot-webgpu` is tested primarily in Chromium-based browsers.
|
|
41
|
+
|
|
42
|
+
Firefox support depends on the current Firefox WebGPU implementation and browser settings.
|
|
43
|
+
|
|
44
|
+
## Live demo
|
|
45
|
+
|
|
46
|
+
[https://uplot-webgpu.netlify.app](https://uplot-webgpu.netlify.app)
|
|
47
|
+
|
|
48
|
+
The demo includes:
|
|
49
|
+
|
|
50
|
+
1. Visual parity checks against the Canvas2D uPlot reference build.
|
|
51
|
+
2. Large static chart tests.
|
|
52
|
+
3. Live animated dashboard tests.
|
|
53
|
+
4. WebGPU worker and buffer-backed rendering experiments.
|
|
54
|
+
|
|
55
|
+
## Basic usage
|
|
56
|
+
|
|
57
|
+
The default export is the WebGPU-backed uPlot constructor. Use it the same way as upstream uPlot:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import uPlot from "uplot-webgpu";
|
|
61
|
+
import "uplot-webgpu/uPlot.css";
|
|
62
|
+
|
|
63
|
+
const chart = new uPlot(opts, data, document.body);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
There is no WebGPU wrapper, provider, adapter call, renderer object, or special mount step required.
|
|
67
|
+
|
|
68
|
+
The WebGPU renderer is embedded inside the uPlot constructor path.
|
|
69
|
+
|
|
70
|
+
## Normal uPlot options still work
|
|
71
|
+
|
|
72
|
+
Because this is still uPlot, normal controls, legends, scales, cursors, selection zoom, and plugin hooks work the same way:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const opts = {
|
|
76
|
+
width: 900,
|
|
77
|
+
height: 320,
|
|
78
|
+
|
|
79
|
+
title: "Live signal",
|
|
80
|
+
|
|
81
|
+
cursor: {
|
|
82
|
+
drag: {
|
|
83
|
+
setScale: true,
|
|
84
|
+
x: true,
|
|
85
|
+
y: false,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
legend: {
|
|
90
|
+
show: true,
|
|
91
|
+
live: true,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
scales: {
|
|
95
|
+
x: {
|
|
96
|
+
time: false,
|
|
97
|
+
},
|
|
98
|
+
y: {
|
|
99
|
+
auto: true,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
axes: [
|
|
104
|
+
{
|
|
105
|
+
label: "sample",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: "value",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
|
|
112
|
+
series: [
|
|
113
|
+
{},
|
|
114
|
+
{
|
|
115
|
+
label: "signal A",
|
|
116
|
+
stroke: "#276ef1",
|
|
117
|
+
width: 2,
|
|
118
|
+
points: {
|
|
119
|
+
show: false,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: "signal B",
|
|
124
|
+
stroke: "#e4572e",
|
|
125
|
+
width: 2,
|
|
126
|
+
dash: [8, 5],
|
|
127
|
+
points: {
|
|
128
|
+
show: false,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Create the data the same way you would for uPlot:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
const points = 500_000;
|
|
139
|
+
|
|
140
|
+
const x = new Float64Array(points);
|
|
141
|
+
const a = new Float32Array(points);
|
|
142
|
+
const b = new Float32Array(points);
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < points; i++) {
|
|
145
|
+
x[i] = i;
|
|
146
|
+
a[i] = Math.sin(i * 0.01);
|
|
147
|
+
b[i] = Math.cos(i * 0.008);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const data = [x, a, b];
|
|
151
|
+
|
|
152
|
+
const chart = new uPlot(opts, data, document.body);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Animating a chart
|
|
156
|
+
|
|
157
|
+
Animated charts use the normal `setData` or `redraw` flow.
|
|
158
|
+
|
|
159
|
+
For large animated buffers, reuse typed arrays instead of allocating new arrays every frame:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
let phase = 0;
|
|
163
|
+
let running = true;
|
|
164
|
+
|
|
165
|
+
function frame() {
|
|
166
|
+
if (!running)
|
|
167
|
+
return;
|
|
168
|
+
|
|
169
|
+
phase += 0.04;
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < points; i++) {
|
|
172
|
+
a[i] = Math.sin(i * 0.01 + phase);
|
|
173
|
+
b[i] = Math.cos(i * 0.008 + phase * 0.7);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
chart.setData(data, false);
|
|
177
|
+
chart.redraw();
|
|
178
|
+
|
|
179
|
+
requestAnimationFrame(frame);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
requestAnimationFrame(frame);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Simple controls
|
|
186
|
+
|
|
187
|
+
Controls can call normal uPlot methods:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
const controls = document.createElement("div");
|
|
191
|
+
|
|
192
|
+
controls.innerHTML = `
|
|
193
|
+
<button data-action="toggle">Pause</button>
|
|
194
|
+
<button data-action="reset">Reset zoom</button>
|
|
195
|
+
<button data-action="trim">Trim GPU memory</button>
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
document.body.prepend(controls);
|
|
199
|
+
|
|
200
|
+
controls.addEventListener("click", event => {
|
|
201
|
+
const action = event.target?.dataset?.action;
|
|
202
|
+
|
|
203
|
+
if (action === "toggle") {
|
|
204
|
+
running = !running;
|
|
205
|
+
event.target.textContent = running ? "Pause" : "Resume";
|
|
206
|
+
|
|
207
|
+
if (running)
|
|
208
|
+
requestAnimationFrame(frame);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (action === "reset") {
|
|
212
|
+
chart.setScale("x", {
|
|
213
|
+
min: x[0],
|
|
214
|
+
max: x[x.length - 1],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (action === "trim") {
|
|
219
|
+
chart.ctx.trimMemory();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The legend is still uPlot’s normal live legend. Series labels, cursor values, dashed styles, hidden points, axes, scales, and plugins are configured through normal uPlot options. The difference is that the heavy draw path is backed by WebGPU.
|
|
225
|
+
|
|
226
|
+
## Web Worker animation example
|
|
227
|
+
|
|
228
|
+
For large animated charts, a useful pattern is to let a Web Worker generate or update the signal while the main thread owns the uPlot instance, legend, cursor, controls, and DOM interaction.
|
|
229
|
+
|
|
230
|
+
This keeps uPlot usage normal while moving expensive data generation off the main thread.
|
|
231
|
+
|
|
232
|
+
### `main.js`
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
import uPlot from "uplot-webgpu";
|
|
236
|
+
import "uplot-webgpu/uPlot.css";
|
|
237
|
+
|
|
238
|
+
const points = 500_000;
|
|
239
|
+
|
|
240
|
+
const x = new Float64Array(points);
|
|
241
|
+
const y = new Float32Array(points);
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < points; i++) {
|
|
244
|
+
x[i] = i;
|
|
245
|
+
y[i] = 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const opts = {
|
|
249
|
+
width: 900,
|
|
250
|
+
height: 320,
|
|
251
|
+
|
|
252
|
+
title: "Worker-driven signal",
|
|
253
|
+
|
|
254
|
+
cursor: {
|
|
255
|
+
drag: {
|
|
256
|
+
setScale: true,
|
|
257
|
+
x: true,
|
|
258
|
+
y: false,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
legend: {
|
|
263
|
+
show: true,
|
|
264
|
+
live: true,
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
scales: {
|
|
268
|
+
x: {
|
|
269
|
+
time: false,
|
|
270
|
+
},
|
|
271
|
+
y: {
|
|
272
|
+
auto: true,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
axes: [
|
|
277
|
+
{
|
|
278
|
+
label: "sample",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
label: "value",
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
|
|
285
|
+
series: [
|
|
286
|
+
{},
|
|
287
|
+
{
|
|
288
|
+
label: "worker signal",
|
|
289
|
+
stroke: "#276ef1",
|
|
290
|
+
width: 2,
|
|
291
|
+
points: {
|
|
292
|
+
show: false,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const data = [x, y];
|
|
299
|
+
|
|
300
|
+
const chart = new uPlot(opts, data, document.body);
|
|
301
|
+
|
|
302
|
+
const worker = new Worker(new URL("./signal.worker.js", import.meta.url), {
|
|
303
|
+
type: "module",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
let running = true;
|
|
307
|
+
let pending = false;
|
|
308
|
+
let yBuffer = y.buffer;
|
|
309
|
+
|
|
310
|
+
worker.postMessage({
|
|
311
|
+
type: "init",
|
|
312
|
+
points,
|
|
313
|
+
yBuffer,
|
|
314
|
+
}, [yBuffer]);
|
|
315
|
+
|
|
316
|
+
worker.onmessage = event => {
|
|
317
|
+
const message = event.data;
|
|
318
|
+
|
|
319
|
+
if (message.type !== "frame")
|
|
320
|
+
return;
|
|
321
|
+
|
|
322
|
+
pending = false;
|
|
323
|
+
|
|
324
|
+
yBuffer = message.yBuffer;
|
|
325
|
+
data[1] = new Float32Array(yBuffer);
|
|
326
|
+
|
|
327
|
+
chart.setData(data, false);
|
|
328
|
+
chart.redraw();
|
|
329
|
+
|
|
330
|
+
if (running)
|
|
331
|
+
requestWorkerFrame();
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
function requestWorkerFrame() {
|
|
335
|
+
if (pending)
|
|
336
|
+
return;
|
|
337
|
+
|
|
338
|
+
pending = true;
|
|
339
|
+
|
|
340
|
+
worker.postMessage({
|
|
341
|
+
type: "frame",
|
|
342
|
+
time: performance.now(),
|
|
343
|
+
yBuffer,
|
|
344
|
+
}, [yBuffer]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
requestWorkerFrame();
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### `signal.worker.js`
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
let points = 0;
|
|
354
|
+
let phase = 0;
|
|
355
|
+
|
|
356
|
+
self.onmessage = event => {
|
|
357
|
+
const message = event.data;
|
|
358
|
+
|
|
359
|
+
if (message.type === "init") {
|
|
360
|
+
points = message.points;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (message.type !== "frame")
|
|
365
|
+
return;
|
|
366
|
+
|
|
367
|
+
const y = new Float32Array(message.yBuffer);
|
|
368
|
+
|
|
369
|
+
phase += 0.04;
|
|
370
|
+
|
|
371
|
+
const slowShift = Math.sin(phase * 0.25) * 0.8;
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < points; i++) {
|
|
374
|
+
const fast = Math.sin(i * 0.01 + phase);
|
|
375
|
+
const slow = Math.sin(i * 0.0007 + slowShift) * 0.35;
|
|
376
|
+
const drift = Math.sin(i * 0.00003 + phase * 0.1) * 0.15;
|
|
377
|
+
|
|
378
|
+
y[i] = fast + slow + drift;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
self.postMessage({
|
|
382
|
+
type: "frame",
|
|
383
|
+
yBuffer: y.buffer,
|
|
384
|
+
}, [y.buffer]);
|
|
385
|
+
};
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This example transfers the `y` buffer back and forth instead of allocating a new `Float32Array` every frame.
|
|
389
|
+
|
|
390
|
+
A good dashboard split is:
|
|
391
|
+
|
|
392
|
+
```txt
|
|
393
|
+
Worker
|
|
394
|
+
-> generate or update large typed arrays
|
|
395
|
+
|
|
396
|
+
Main thread
|
|
397
|
+
-> uPlot interaction, legend, cursor, controls
|
|
398
|
+
|
|
399
|
+
WebGPU renderer
|
|
400
|
+
-> high-throughput drawing
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
For heavier pipelines, combine this with GPU buffers so a worker, compute pass, or custom renderer can update GPU-side data directly.
|
|
404
|
+
|
|
405
|
+
## Cleanup
|
|
406
|
+
|
|
407
|
+
Manual cleanup works the same as in uPlot:
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
chart.destroy();
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Additional cleanup helpers are available for dynamic pages, route transitions, modals, dashboards, and popups that create and remove many charts:
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
uPlot.destroyDetached();
|
|
417
|
+
uPlot.destroyAll();
|
|
418
|
+
uPlot.getLivePlots();
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Detached charts are cleaned up automatically when a chart that was once connected to the document is later removed. Manual cleanup is still recommended when your app framework has a reliable unmount hook.
|
|
422
|
+
|
|
423
|
+
## Runtime memory controls
|
|
424
|
+
|
|
425
|
+
The WebGPU renderer defaults to:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
memoryMode: "balanced"
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
This releases CPU-side draw commands after submit, trims oversized staging arrays, and caps cached image textures.
|
|
432
|
+
|
|
433
|
+
Apps that prefer lower retained memory can use:
|
|
434
|
+
|
|
435
|
+
```js
|
|
436
|
+
uPlot.configure({
|
|
437
|
+
rendererOptions: {
|
|
438
|
+
memoryMode: "low",
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Animation-heavy views can prefer fewer reallocations:
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
uPlot.configure({
|
|
447
|
+
rendererOptions: {
|
|
448
|
+
memoryMode: "throughput",
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Per-chart renderer options can also be passed through normal uPlot options where supported by the port.
|
|
454
|
+
|
|
455
|
+
Advanced cleanup hooks are available on the renderer-backed context:
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
chart.ctx.getMemoryStats();
|
|
459
|
+
chart.ctx.trimMemory();
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Shared WebGPU runtime
|
|
463
|
+
|
|
464
|
+
Several charts can share one WebGPU runtime. This avoids repeatedly creating device-level renderer resources for every chart.
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
import uPlot, { WebGPURenderer } from "uplot-webgpu";
|
|
468
|
+
|
|
469
|
+
WebGPURenderer.setSharedRuntimeEnabled(true);
|
|
470
|
+
|
|
471
|
+
const chartA = new uPlot(optsA, dataA, elA);
|
|
472
|
+
const chartB = new uPlot(optsB, dataB, elB);
|
|
473
|
+
|
|
474
|
+
await Promise.all([
|
|
475
|
+
chartA.ctx.initPromise,
|
|
476
|
+
chartB.ctx.initPromise,
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
console.log(WebGPURenderer.getSharedRuntimeStats());
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The shared runtime is useful for dashboards, route-level chart groups, and pages with many live plots.
|
|
483
|
+
|
|
484
|
+
## Advanced GPU buffer reuse
|
|
485
|
+
|
|
486
|
+
Most users only need normal uPlot data:
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
const chart = new uPlot(opts, data, target);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Advanced WebGPU apps can also access the renderer from the chart context:
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
const renderer = chart.ctx;
|
|
496
|
+
|
|
497
|
+
await renderer.initPromise;
|
|
498
|
+
|
|
499
|
+
const device = renderer.getDevice();
|
|
500
|
+
const gpuContext = renderer.getCanvasContext();
|
|
501
|
+
|
|
502
|
+
if (!device)
|
|
503
|
+
throw new Error("WebGPU device was not initialized");
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
This lets large signal data stay in `GPUBuffer`s and be reused by compute passes, custom shader passes, and chart visualization code.
|
|
507
|
+
|
|
508
|
+
A common layout is:
|
|
509
|
+
|
|
510
|
+
```txt
|
|
511
|
+
sensor / simulation / worker
|
|
512
|
+
-> GPUBuffer
|
|
513
|
+
-> compute pass
|
|
514
|
+
-> chart render pass
|
|
515
|
+
-> overlay / analysis pass
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Example signal buffer:
|
|
519
|
+
|
|
520
|
+
```js
|
|
521
|
+
const sampleCount = 1_000_000;
|
|
522
|
+
|
|
523
|
+
const signalBuffer = device.createBuffer({
|
|
524
|
+
size: sampleCount * 2 * 4,
|
|
525
|
+
usage:
|
|
526
|
+
GPUBufferUsage.STORAGE |
|
|
527
|
+
GPUBufferUsage.VERTEX |
|
|
528
|
+
GPUBufferUsage.COPY_DST |
|
|
529
|
+
GPUBufferUsage.COPY_SRC,
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Upload data from JavaScript when needed:
|
|
534
|
+
|
|
535
|
+
```js
|
|
536
|
+
const points = new Float32Array(sampleCount * 2);
|
|
537
|
+
|
|
538
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
539
|
+
points[i * 2 + 0] = i;
|
|
540
|
+
points[i * 2 + 1] = Math.sin(i * 0.01);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
device.queue.writeBuffer(signalBuffer, 0, points);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
That same buffer can be used by:
|
|
547
|
+
|
|
548
|
+
1. A compute shader that filters or transforms the signal.
|
|
549
|
+
2. A custom render pass that visualizes the signal.
|
|
550
|
+
3. A uPlot WebGPU chart or overlay.
|
|
551
|
+
4. Another GPU pass that calculates thresholds, histograms, FFT prep, alerts, or markers.
|
|
552
|
+
|
|
553
|
+
### Experimental GPUBuffer-backed series
|
|
554
|
+
|
|
555
|
+
The stable drop-in API accepts normal uPlot data:
|
|
556
|
+
|
|
557
|
+
```js
|
|
558
|
+
const data = [xValues, yValues];
|
|
559
|
+
|
|
560
|
+
const chart = new uPlot(opts, data, target);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
For GPU-heavy views, keep a mirrored `GPUBuffer` beside the uPlot data. The normal arrays provide scale and domain metadata. The GPU buffer can be reused by compute shaders and custom render paths.
|
|
564
|
+
|
|
565
|
+
```js
|
|
566
|
+
const sampleCount = 1_000_000;
|
|
567
|
+
|
|
568
|
+
const xValues = new Float64Array(sampleCount);
|
|
569
|
+
const yValues = new Float32Array(sampleCount);
|
|
570
|
+
|
|
571
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
572
|
+
xValues[i] = i;
|
|
573
|
+
yValues[i] = Math.sin(i * 0.01);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const data = [
|
|
577
|
+
xValues,
|
|
578
|
+
yValues,
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
const chart = new uPlot(opts, data, document.body);
|
|
582
|
+
|
|
583
|
+
await chart.ctx.initPromise;
|
|
584
|
+
|
|
585
|
+
const renderer = chart.ctx;
|
|
586
|
+
const device = renderer.getDevice();
|
|
587
|
+
|
|
588
|
+
if (!device)
|
|
589
|
+
throw new Error("WebGPU device was not initialized");
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Create a GPU-side copy of the same signal:
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
const pointBuffer = device.createBuffer({
|
|
596
|
+
size: sampleCount * 2 * 4,
|
|
597
|
+
usage:
|
|
598
|
+
GPUBufferUsage.STORAGE |
|
|
599
|
+
GPUBufferUsage.VERTEX |
|
|
600
|
+
GPUBufferUsage.COPY_DST |
|
|
601
|
+
GPUBufferUsage.COPY_SRC,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const pointData = new Float32Array(sampleCount * 2);
|
|
605
|
+
|
|
606
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
607
|
+
pointData[i * 2 + 0] = xValues[i];
|
|
608
|
+
pointData[i * 2 + 1] = yValues[i];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
device.queue.writeBuffer(pointBuffer, 0, pointData);
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Describe the GPU-backed series source:
|
|
615
|
+
|
|
616
|
+
```js
|
|
617
|
+
const gpuSignal = {
|
|
618
|
+
buffer: pointBuffer,
|
|
619
|
+
|
|
620
|
+
count: sampleCount,
|
|
621
|
+
stride: 8,
|
|
622
|
+
|
|
623
|
+
xOffset: 0,
|
|
624
|
+
yOffset: 4,
|
|
625
|
+
|
|
626
|
+
xType: "f32",
|
|
627
|
+
yType: "f32",
|
|
628
|
+
|
|
629
|
+
xMin: xValues[0],
|
|
630
|
+
xMax: xValues[sampleCount - 1],
|
|
631
|
+
yMin: -1,
|
|
632
|
+
yMax: 1,
|
|
633
|
+
};
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Attach it when the external-series path is enabled:
|
|
637
|
+
|
|
638
|
+
```js
|
|
639
|
+
renderer.setExternalSeries?.(1, gpuSignal);
|
|
640
|
+
|
|
641
|
+
chart.redraw();
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
The chart still behaves like uPlot from the outside:
|
|
645
|
+
|
|
646
|
+
```js
|
|
647
|
+
chart.setScale("x", {
|
|
648
|
+
min: 100_000,
|
|
649
|
+
max: 140_000,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
chart.redraw();
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The important idea is that `data` remains the normal uPlot-facing data shape, while `gpuSignal` is the WebGPU-side representation that can be reused by other shaders.
|
|
656
|
+
|
|
657
|
+
### Compute pass example
|
|
658
|
+
|
|
659
|
+
The same `pointBuffer` can be read or written by a compute shader.
|
|
660
|
+
|
|
661
|
+
```js
|
|
662
|
+
const paramsBuffer = device.createBuffer({
|
|
663
|
+
size: 16,
|
|
664
|
+
usage:
|
|
665
|
+
GPUBufferUsage.UNIFORM |
|
|
666
|
+
GPUBufferUsage.COPY_DST,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
device.queue.writeBuffer(
|
|
670
|
+
paramsBuffer,
|
|
671
|
+
0,
|
|
672
|
+
new Float32Array([
|
|
673
|
+
performance.now() * 0.001,
|
|
674
|
+
sampleCount,
|
|
675
|
+
0,
|
|
676
|
+
0,
|
|
677
|
+
]),
|
|
678
|
+
);
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Example WGSL compute shader:
|
|
682
|
+
|
|
683
|
+
```wgsl
|
|
684
|
+
struct Params {
|
|
685
|
+
time: f32,
|
|
686
|
+
count: f32,
|
|
687
|
+
pad0: f32,
|
|
688
|
+
pad1: f32,
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
@group(0) @binding(0)
|
|
692
|
+
var<storage, read_write> points: array<vec2<f32>>;
|
|
693
|
+
|
|
694
|
+
@group(0) @binding(1)
|
|
695
|
+
var<uniform> params: Params;
|
|
696
|
+
|
|
697
|
+
@compute @workgroup_size(256)
|
|
698
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
699
|
+
let i = gid.x;
|
|
700
|
+
|
|
701
|
+
if (i >= u32(params.count)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
let x = points[i].x;
|
|
706
|
+
let wave = sin(x * 0.01 + params.time) * 0.75;
|
|
707
|
+
let slow = sin(x * 0.0007 + params.time * 0.25) * 0.25;
|
|
708
|
+
|
|
709
|
+
points[i].y = wave + slow;
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Create the compute pipeline and bind group:
|
|
714
|
+
|
|
715
|
+
```js
|
|
716
|
+
const shader = device.createShaderModule({
|
|
717
|
+
code: computeWGSL,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const computePipeline = device.createComputePipeline({
|
|
721
|
+
layout: "auto",
|
|
722
|
+
compute: {
|
|
723
|
+
module: shader,
|
|
724
|
+
entryPoint: "main",
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const computeBindGroup = device.createBindGroup({
|
|
729
|
+
layout: computePipeline.getBindGroupLayout(0),
|
|
730
|
+
entries: [
|
|
731
|
+
{
|
|
732
|
+
binding: 0,
|
|
733
|
+
resource: {
|
|
734
|
+
buffer: pointBuffer,
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
binding: 1,
|
|
739
|
+
resource: {
|
|
740
|
+
buffer: paramsBuffer,
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
});
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
Run the compute pass, then redraw the chart from the same GPU buffer:
|
|
748
|
+
|
|
749
|
+
```js
|
|
750
|
+
function animate() {
|
|
751
|
+
device.queue.writeBuffer(
|
|
752
|
+
paramsBuffer,
|
|
753
|
+
0,
|
|
754
|
+
new Float32Array([
|
|
755
|
+
performance.now() * 0.001,
|
|
756
|
+
sampleCount,
|
|
757
|
+
0,
|
|
758
|
+
0,
|
|
759
|
+
]),
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
const encoder = device.createCommandEncoder();
|
|
763
|
+
const pass = encoder.beginComputePass();
|
|
764
|
+
|
|
765
|
+
pass.setPipeline(computePipeline);
|
|
766
|
+
pass.setBindGroup(0, computeBindGroup);
|
|
767
|
+
pass.dispatchWorkgroups(Math.ceil(sampleCount / 256));
|
|
768
|
+
|
|
769
|
+
pass.end();
|
|
770
|
+
|
|
771
|
+
device.queue.submit([encoder.finish()]);
|
|
772
|
+
|
|
773
|
+
chart.redraw();
|
|
774
|
+
|
|
775
|
+
requestAnimationFrame(animate);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
requestAnimationFrame(animate);
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
This avoids rebuilding a million-point JavaScript array every frame. The CPU-side uPlot arrays are still useful for initial scale and domain setup, but the live signal update can happen directly in GPU memory.
|
|
782
|
+
|
|
783
|
+
## Advanced renderer exports
|
|
784
|
+
|
|
785
|
+
Normal chart usage only needs the default export:
|
|
786
|
+
|
|
787
|
+
```js
|
|
788
|
+
import uPlot from "uplot-webgpu";
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Advanced users can access the renderer directly:
|
|
792
|
+
|
|
793
|
+
```js
|
|
794
|
+
import uPlot, {
|
|
795
|
+
WebGPURenderer,
|
|
796
|
+
GPUPath,
|
|
797
|
+
} from "uplot-webgpu";
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Renderer-only convenience subpath:
|
|
801
|
+
|
|
802
|
+
```js
|
|
803
|
+
import {
|
|
804
|
+
WebGPURenderer,
|
|
805
|
+
GPUPath,
|
|
806
|
+
} from "uplot-webgpu/renderer";
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
These exports are part of the core runtime because the default WebGPU uPlot constructor uses them internally.
|
|
810
|
+
|
|
811
|
+
## Canvas proxy and standalone renderer
|
|
812
|
+
|
|
813
|
+
The package also includes a Canvas2D-like WebGPU renderer that can be used independently of uPlot.
|
|
814
|
+
|
|
815
|
+
This is useful for custom GPU dashboards, plugin experiments, signal-buffer visualization, and canvas-like drawing that should render through WebGPU.
|
|
816
|
+
|
|
817
|
+
See:
|
|
818
|
+
|
|
819
|
+
```txt
|
|
820
|
+
CANVAS_PROXY.md
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
## Running the benchmark locally
|
|
824
|
+
|
|
825
|
+
Serve `index.html` with the `dist` folder present.
|
|
826
|
+
|
|
827
|
+
To rebuild the demo bundle:
|
|
828
|
+
|
|
829
|
+
```bash
|
|
830
|
+
npm i -g tinybuild
|
|
831
|
+
tinybuild
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
Or use the project start script:
|
|
835
|
+
|
|
836
|
+
```bash
|
|
837
|
+
npm start
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The benchmark entrypoint includes the old Canvas2D reference build and smoke tests so you can compare visual parity, cold start, warm chart creation, redraw submission, zoom behavior, and live dashboard FPS.
|
|
841
|
+
|
|
842
|
+
## Performance notes
|
|
843
|
+
|
|
844
|
+
Canvas2D often wins small static charts because it has almost no GPU setup cost and uPlot is already highly optimized.
|
|
845
|
+
|
|
846
|
+
`uplot-webgpu` is most useful when the chart workload is large enough or repeated enough to benefit from GPU-backed rendering and shared GPU resources.
|
|
847
|
+
|
|
848
|
+
## Contribute
|
|
849
|
+
|
|
850
|
+
MIT License, based on the original [uPlot](https://github.com/leeoniya/uPlot) by leeoniya
|
|
851
|
+
|
|
852
|
+
Feel free to make pull requests or fork it.
|
|
853
|
+
|
|
854
|
+
I'm not likely going to keep this up-to-date or explore every edge case that uPlot handles, this was to fill a niche of needing stylizable charts with lots of data in the buffers that still load and animate fast, as I work with lots of real-time data.
|