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.
Files changed (50) hide show
  1. package/CANVAS_PROXY.md +602 -0
  2. package/README.md +854 -0
  3. package/favicon.ico +0 -0
  4. package/index.html +14 -0
  5. package/index.js +21 -0
  6. package/original/paths.canvas2d/bars.js +252 -0
  7. package/original/paths.canvas2d/catmullRomCentrip.js +125 -0
  8. package/original/paths.canvas2d/linear.js +170 -0
  9. package/original/paths.canvas2d/monotoneCubic.js +68 -0
  10. package/original/paths.canvas2d/points.js +66 -0
  11. package/original/paths.canvas2d/spline.js +103 -0
  12. package/original/paths.canvas2d/stepped.js +124 -0
  13. package/original/paths.canvas2d/utils.js +301 -0
  14. package/original/uPlot.canvas2d.js +3548 -0
  15. package/package.json +110 -0
  16. package/paths/bars.js +253 -0
  17. package/paths/catmullRomCentrip.js +126 -0
  18. package/paths/linear.js +171 -0
  19. package/paths/monotoneCubic.js +69 -0
  20. package/paths/points.js +67 -0
  21. package/paths/spline.js +104 -0
  22. package/paths/stepped.js +125 -0
  23. package/paths/utils.js +301 -0
  24. package/scripts/uPlot.css +168 -0
  25. package/scripts/uPlot.d.ts +26 -0
  26. package/scripts/uPlot.js +3687 -0
  27. package/scripts/utils/dom.js +124 -0
  28. package/scripts/utils/domClasses.js +22 -0
  29. package/scripts/utils/feats.js +13 -0
  30. package/scripts/utils/fmtDate.js +398 -0
  31. package/scripts/utils/opts.js +844 -0
  32. package/scripts/utils/strings.js +22 -0
  33. package/scripts/utils/sync.js +27 -0
  34. package/scripts/utils/utils.js +692 -0
  35. package/scripts/webgpu/GPUPath.d.ts +46 -0
  36. package/scripts/webgpu/GPUPath.js +633 -0
  37. package/scripts/webgpu/GPUPath.ts +634 -0
  38. package/scripts/webgpu/WebGPURenderer.d.ts +176 -0
  39. package/scripts/webgpu/WebGPURenderer.js +4256 -0
  40. package/scripts/webgpu/WebGPURenderer.ts +4257 -0
  41. package/scripts/webgpu/browserSmokeHarness.js +105 -0
  42. package/scripts/webgpu/exporters.d.ts +8 -0
  43. package/scripts/webgpu/exporters.js +212 -0
  44. package/scripts/webgpu/shaders.d.ts +2 -0
  45. package/scripts/webgpu/shaders.js +76 -0
  46. package/scripts/webgpu/shaders.ts +77 -0
  47. package/scripts/webgpu/smokeTest.d.ts +2 -0
  48. package/scripts/webgpu/smokeTest.js +144 -0
  49. package/scripts/webgpu/webgpu-ambient.d.ts +41 -0
  50. 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.