resolvcharts 0.2.0 → 0.3.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.
@@ -0,0 +1,1191 @@
1
+ //#region src/gpu/color.ts
2
+ /** Parse a CSS color (#rgb, #rrggbb, rgb(), rgba()) into linear-ish 0-1 rgba. */
3
+ function parseColor(c) {
4
+ if (c.startsWith("#")) {
5
+ const h = c.slice(1);
6
+ if (h.length === 3) return [
7
+ parseInt(h[0], 16) / 15,
8
+ parseInt(h[1], 16) / 15,
9
+ parseInt(h[2], 16) / 15,
10
+ 1
11
+ ];
12
+ if (h.length === 6) return [
13
+ parseInt(h.slice(0, 2), 16) / 255,
14
+ parseInt(h.slice(2, 4), 16) / 255,
15
+ parseInt(h.slice(4, 6), 16) / 255,
16
+ 1
17
+ ];
18
+ }
19
+ const m = /rgba?\(([^)]+)\)/.exec(c);
20
+ if (m) {
21
+ const parts = m[1].split(",").map((v) => Number.parseFloat(v));
22
+ return [
23
+ (parts[0] ?? 0) / 255,
24
+ (parts[1] ?? 0) / 255,
25
+ (parts[2] ?? 0) / 255,
26
+ parts[3] ?? 1
27
+ ];
28
+ }
29
+ return [
30
+ 0,
31
+ 0,
32
+ 0,
33
+ 1
34
+ ];
35
+ }
36
+ //#endregion
37
+ //#region src/gpu/device.ts
38
+ /** Shared GPUDevice across charts — adapters/devices are expensive to acquire. */
39
+ const cache = /* @__PURE__ */ new Map();
40
+ function getGpuDevice(opts = {}) {
41
+ const key = JSON.stringify(opts);
42
+ let p = cache.get(key);
43
+ if (!p) {
44
+ p = (async () => {
45
+ try {
46
+ const gpu = (typeof navigator === "undefined" ? null : navigator)?.gpu;
47
+ if (!gpu) return null;
48
+ const req = {};
49
+ if (opts.powerPreference && opts.powerPreference !== "default") req.powerPreference = opts.powerPreference;
50
+ if (opts.compat) req.featureLevel = "compatibility";
51
+ const adapter = await gpu.requestAdapter(req);
52
+ if (!adapter) return null;
53
+ if (!opts.allowFallback && adapter.isFallbackAdapter) return null;
54
+ const requiredFeatures = [];
55
+ if (opts.timing && adapter.features.has("timestamp-query")) requiredFeatures.push("timestamp-query");
56
+ const device = await adapter.requestDevice({ requiredFeatures });
57
+ device.lost.then(() => cache.delete(key));
58
+ return device;
59
+ } catch {
60
+ return null;
61
+ }
62
+ })();
63
+ p.catch(() => cache.delete(key));
64
+ cache.set(key, p);
65
+ }
66
+ return p;
67
+ }
68
+ //#endregion
69
+ //#region src/gpu/series.ts
70
+ /**
71
+ * WebGPU series renderer. Data is uploaded once per series as affine-encoded
72
+ * f32 [x, y] pairs (+ an encoded prevY channel for animation) and lives on the
73
+ * GPU: the domain→pixel transform runs in the vertex shader, so pan/zoom,
74
+ * resize and view animation are uniform writes, not buffer re-uploads.
75
+ * Streaming `append()` writes only the new tail into a per-series ring buffer.
76
+ *
77
+ * When a series has more than ~2 points per pixel column a compute pass does
78
+ * M4 decimation (first/min/max/last per column) into a GPU-side output buffer
79
+ * — the CPU never rescans the data. Pipelines use instanced vertex attributes
80
+ * everywhere (no vertex-stage storage buffers), so they also run in WebGPU
81
+ * compatibility mode.
82
+ */
83
+ const HEADER = `
84
+ struct U {
85
+ res: vec4f, // canvas w,h (device px), dpr, pad
86
+ color: vec4f,
87
+ p: vec4f, // line: width | area: baseCssY, plotY, plotH | points: radius
88
+ map: vec4f, // cssX = ex*map.x + map.y ; cssY = map.w - ey*map.z
89
+ anim: vec4f, // t, flow, invN, pad
90
+ ring: vec4f, // start, cap (0 = linear), n, pad
91
+ dec: vec4f, // cols, gapFillYEnc, pad, pad
92
+ };
93
+ @group(0) @binding(0) var<uniform> u: U;
94
+
95
+ fn encToCss(e: vec2f) -> vec2f {
96
+ return vec2f(e.x * u.map.x + u.map.y, u.map.w - e.y * u.map.z);
97
+ }
98
+ fn toClip(css: vec2f) -> vec4f {
99
+ let d = css * u.res.z;
100
+ return vec4f(d.x / u.res.x * 2.0 - 1.0, 1.0 - d.y / u.res.y * 2.0, 0.0, 1.0);
101
+ }
102
+ fn animT(i: f32) -> f32 {
103
+ return clamp(
104
+ u.anim.x * (1.0 + u.anim.y) - u.anim.y + i * u.anim.z, 0.0, 1.0,
105
+ );
106
+ }
107
+ `;
108
+ const LINE_WGSL = `
109
+ ${HEADER}
110
+ const OFF = array<vec2f, 6>(
111
+ vec2f(0.0, -1.0), vec2f(0.0, 1.0), vec2f(1.0, -1.0),
112
+ vec2f(0.0, 1.0), vec2f(1.0, 1.0), vec2f(1.0, -1.0),
113
+ );
114
+ struct VOut {
115
+ @builtin(position) pos: vec4f,
116
+ @location(0) edge: f32,
117
+ };
118
+ fn segQuad(v: vec2f, A: vec2f, B: vec2f) -> VOut {
119
+ var dir = B - A;
120
+ let len = length(dir);
121
+ if (len < 1e-6) {
122
+ dir = vec2f(1.0, 0.0);
123
+ } else {
124
+ dir = dir / len;
125
+ }
126
+ let nrm = vec2f(-dir.y, dir.x);
127
+ let halfW = u.p.x * 0.5;
128
+ let aa = 0.9;
129
+ var o: VOut;
130
+ o.pos = toClip(mix(A, B, v.x) + nrm * v.y * (halfW + aa));
131
+ o.edge = v.y * (halfW + aa);
132
+ return o;
133
+ }
134
+ @vertex fn vs(
135
+ @builtin(vertex_index) vi: u32,
136
+ @builtin(instance_index) inst: u32,
137
+ @location(0) a: vec2f,
138
+ @location(1) b: vec2f,
139
+ @location(2) pa: f32,
140
+ @location(3) pb: f32,
141
+ ) -> VOut {
142
+ let ya = mix(pa, a.y, animT(f32(inst)));
143
+ let yb = mix(pb, b.y, animT(f32(inst + 1u)));
144
+ return segQuad(
145
+ OFF[vi], encToCss(vec2f(a.x, ya)), encToCss(vec2f(b.x, yb)),
146
+ );
147
+ }
148
+ @fragment fn fs(o: VOut) -> @location(0) vec4f {
149
+ let halfW = u.p.x * 0.5;
150
+ let a = 1.0 - smoothstep(halfW - 0.5, halfW + 0.9, abs(o.edge));
151
+ return vec4f(u.color.rgb, u.color.a * a);
152
+ }
153
+ `;
154
+ const LINE_FLAT_WGSL = `
155
+ ${HEADER}
156
+ const OFF = array<vec2f, 6>(
157
+ vec2f(0.0, -1.0), vec2f(0.0, 1.0), vec2f(1.0, -1.0),
158
+ vec2f(0.0, 1.0), vec2f(1.0, 1.0), vec2f(1.0, -1.0),
159
+ );
160
+ struct VOut {
161
+ @builtin(position) pos: vec4f,
162
+ @location(0) edge: f32,
163
+ };
164
+ @vertex fn vs(
165
+ @builtin(vertex_index) vi: u32,
166
+ @location(0) a: vec2f,
167
+ @location(1) b: vec2f,
168
+ ) -> VOut {
169
+ var o: VOut;
170
+ if (a.x < -1e20 || b.x < -1e20) {
171
+ o.pos = vec4f(0.0, 0.0, 2.0, 1.0); // outside clip z-range → discarded
172
+ o.edge = 0.0;
173
+ return o;
174
+ }
175
+ let A = encToCss(a);
176
+ let B = encToCss(b);
177
+ var dir = B - A;
178
+ let len = length(dir);
179
+ if (len < 1e-6) {
180
+ dir = vec2f(1.0, 0.0);
181
+ } else {
182
+ dir = dir / len;
183
+ }
184
+ let nrm = vec2f(-dir.y, dir.x);
185
+ let halfW = u.p.x * 0.5;
186
+ let aa = 0.9;
187
+ let v = OFF[vi];
188
+ o.pos = toClip(mix(A, B, v.x) + nrm * v.y * (halfW + aa));
189
+ o.edge = v.y * (halfW + aa);
190
+ return o;
191
+ }
192
+ @fragment fn fs(o: VOut) -> @location(0) vec4f {
193
+ let halfW = u.p.x * 0.5;
194
+ let a = 1.0 - smoothstep(halfW - 0.5, halfW + 0.9, abs(o.edge));
195
+ return vec4f(u.color.rgb, u.color.a * a);
196
+ }
197
+ `;
198
+ const AREA_WGSL = `
199
+ ${HEADER}
200
+ struct VOut {
201
+ @builtin(position) pos: vec4f,
202
+ @location(0) grad: f32,
203
+ };
204
+ // input is doubled point data (each point twice): even verts land on the
205
+ // path, odd verts drop to the baseline
206
+ @vertex fn vs(
207
+ @builtin(vertex_index) vi: u32,
208
+ @location(0) top: vec2f,
209
+ @location(1) prev: f32,
210
+ ) -> VOut {
211
+ let isTop = (vi & 1u) == 0u;
212
+ let y = mix(prev, top.y, animT(f32(vi >> 1u)));
213
+ let css = encToCss(vec2f(top.x, y));
214
+ var o: VOut;
215
+ o.pos = toClip(vec2f(css.x, select(u.p.x, css.y, isTop)));
216
+ o.grad = clamp((css.y - u.p.y) / max(u.p.z, 1.0), 0.0, 1.0);
217
+ return o;
218
+ }
219
+ @fragment fn fs(o: VOut) -> @location(0) vec4f {
220
+ let a = mix(0.16, 0.0, o.grad) * u.color.a;
221
+ return vec4f(u.color.rgb, a);
222
+ }
223
+ `;
224
+ const AREA_FLAT_WGSL = `
225
+ ${HEADER}
226
+ struct VOut {
227
+ @builtin(position) pos: vec4f,
228
+ @location(0) grad: f32,
229
+ };
230
+ @vertex fn vs(
231
+ @builtin(vertex_index) vi: u32,
232
+ @location(0) top: vec2f,
233
+ ) -> VOut {
234
+ let isTop = (vi & 1u) == 0u;
235
+ let css = encToCss(top);
236
+ var o: VOut;
237
+ o.pos = toClip(vec2f(css.x, select(u.p.x, css.y, isTop)));
238
+ o.grad = clamp((css.y - u.p.y) / max(u.p.z, 1.0), 0.0, 1.0);
239
+ return o;
240
+ }
241
+ @fragment fn fs(o: VOut) -> @location(0) vec4f {
242
+ let a = mix(0.16, 0.0, o.grad) * u.color.a;
243
+ return vec4f(u.color.rgb, a);
244
+ }
245
+ `;
246
+ const POINTS_WGSL = `
247
+ ${HEADER}
248
+ const QUAD = array<vec2f, 6>(
249
+ vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0),
250
+ vec2f(-1.0, 1.0), vec2f(1.0, 1.0), vec2f(1.0, 1.0),
251
+ );
252
+ struct VOut {
253
+ @builtin(position) pos: vec4f,
254
+ @location(0) local: vec2f,
255
+ };
256
+ @vertex fn vs(
257
+ @builtin(vertex_index) vi: u32,
258
+ @builtin(instance_index) inst: u32,
259
+ @location(0) c: vec2f,
260
+ @location(1) prev: f32,
261
+ ) -> VOut {
262
+ let r = u.p.x;
263
+ let q = QUAD[vi];
264
+ let y = mix(prev, c.y, animT(f32(inst)));
265
+ let css = encToCss(vec2f(c.x, y));
266
+ var o: VOut;
267
+ o.pos = toClip(css + q * r);
268
+ o.local = q * r;
269
+ return o;
270
+ }
271
+ @fragment fn fs(o: VOut) -> @location(0) vec4f {
272
+ let r = u.p.x;
273
+ let a = 1.0 - smoothstep(r - 1.2, r, length(o.local));
274
+ return vec4f(u.color.rgb, u.color.a * a);
275
+ }
276
+ `;
277
+ const DECIMATE_WGSL = `
278
+ ${HEADER}
279
+ @group(1) @binding(0) var<storage, read> xy: array<vec2f>;
280
+ @group(1) @binding(1) var<storage, read> prev: array<f32>;
281
+ @group(1) @binding(2) var<storage, read_write> dst: array<vec2f>;
282
+
283
+ fn physIdx(i: u32) -> u32 {
284
+ let cap = u32(u.ring.y);
285
+ if (cap == 0u) {
286
+ return i;
287
+ }
288
+ return (u32(u.ring.x) + i) % cap;
289
+ }
290
+ fn xEnc(i: u32) -> f32 {
291
+ return xy[physIdx(i)].x;
292
+ }
293
+ fn yLerp(i: u32) -> f32 {
294
+ let p = physIdx(i);
295
+ return mix(prev[p], xy[p].y, animT(f32(i)));
296
+ }
297
+ fn lowerBound(e: f32) -> u32 {
298
+ var lo = 0u;
299
+ var hi = u32(u.ring.z);
300
+ while (lo < hi) {
301
+ let mid = (lo + hi) >> 1u;
302
+ if (xEnc(mid) < e) {
303
+ lo = mid + 1u;
304
+ } else {
305
+ hi = mid;
306
+ }
307
+ }
308
+ return lo;
309
+ }
310
+
311
+ @compute @workgroup_size(64)
312
+ fn dec(@builtin(global_invocation_id) gid: vec3u) {
313
+ let c = gid.x;
314
+ let cols = u32(u.dec.x);
315
+ if (c >= cols) {
316
+ return;
317
+ }
318
+ let lineBase = c * 4u;
319
+ let areaBase = cols * 4u + c * 8u;
320
+ // encoded-x range mapping to css column [c, c+1)
321
+ let lo = lowerBound((f32(c) - u.map.y) / u.map.x);
322
+ let hi = lowerBound((f32(c) + 1.0 - u.map.y) / u.map.x);
323
+ if (lo >= hi) {
324
+ let sent = vec2f(-1e30, -1e30);
325
+ for (var k = 0u; k < 4u; k++) {
326
+ dst[lineBase + k] = sent;
327
+ }
328
+ let gp = vec2f((f32(c) + 0.5 - u.map.y) / u.map.x, u.dec.y);
329
+ for (var k = 0u; k < 8u; k++) {
330
+ dst[areaBase + k] = gp;
331
+ }
332
+ return;
333
+ }
334
+ var iMin = lo;
335
+ var iMax = lo;
336
+ var mn = yLerp(lo);
337
+ var mx = mn;
338
+ for (var i = lo + 1u; i < hi; i++) {
339
+ let y = yLerp(i);
340
+ if (y < mn) {
341
+ mn = y;
342
+ iMin = i;
343
+ }
344
+ if (y > mx) {
345
+ mx = y;
346
+ iMax = i;
347
+ }
348
+ }
349
+ let xs = array<u32, 4>(lo, iMin, iMax, hi - 1u);
350
+ for (var k = 0u; k < 4u; k++) {
351
+ let i = xs[k];
352
+ let pt = vec2f(xEnc(i), yLerp(i));
353
+ dst[lineBase + k] = pt;
354
+ dst[areaBase + 2u * k] = pt;
355
+ dst[areaBase + 2u * k + 1u] = pt;
356
+ }
357
+ }
358
+ `;
359
+ const BUF_VERTEX_DST = 40;
360
+ const BUF_UNIFORM_DST = 72;
361
+ const BUF_RESOLVE_SRC = 516;
362
+ const BUF_MAPREAD_DST = 9;
363
+ const BUF_SERIES = 172;
364
+ const BUF_DEC = 160;
365
+ const MAP_READ = 1;
366
+ const STAGE_ALL = 7;
367
+ const UBYTES = 256;
368
+ const UBUF_SIZE = 262144;
369
+ const DEC_ELEMS_PER_COL = 12;
370
+ var GpuSeries = class GpuSeries {
371
+ get unavailable() {
372
+ return this.dead || this.recovering;
373
+ }
374
+ constructor(device, canvas, opts) {
375
+ this.dpr = 1;
376
+ this.slots = [];
377
+ this.slotCount = 0;
378
+ this.appliedRev = -1;
379
+ this.draws = [];
380
+ this.cmps = [];
381
+ this.uOff = 0;
382
+ this.stamp = 0;
383
+ this.plotR = {
384
+ x: 0,
385
+ y: 0,
386
+ w: 1,
387
+ h: 1
388
+ };
389
+ this.cols = 1;
390
+ this.t = 1;
391
+ this.flow = 0;
392
+ this.tailScratch = /* @__PURE__ */ new Float32Array(4);
393
+ this.gpuMs = null;
394
+ this.querySet = null;
395
+ this.queryBuf = null;
396
+ this.readBuf = null;
397
+ this.reading = false;
398
+ this.timing = false;
399
+ this.destroyed = false;
400
+ this.dead = false;
401
+ this.recovering = false;
402
+ this.opts = opts;
403
+ this.timing = !!opts.timing;
404
+ this.canvas = canvas;
405
+ canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none";
406
+ const ctx = canvas.getContext("webgpu");
407
+ if (!ctx) throw new Error("resolvcharts: webgpu context unavailable");
408
+ this.ctx = ctx;
409
+ this.device = device;
410
+ this.format = navigator.gpu.getPreferredCanvasFormat();
411
+ this.setup(device);
412
+ }
413
+ static async create(opts = {}) {
414
+ const device = await getGpuDevice(opts);
415
+ if (!device) return null;
416
+ const canvas = document.createElement("canvas");
417
+ try {
418
+ return new GpuSeries(device, canvas, opts);
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+ /** (re)create all device-bound resources — called on init and after loss */
424
+ setup(device) {
425
+ this.device = device;
426
+ this.layout = device.createBindGroupLayout({ entries: [{
427
+ binding: 0,
428
+ visibility: STAGE_ALL,
429
+ buffer: {
430
+ type: "uniform",
431
+ hasDynamicOffset: true
432
+ }
433
+ }] });
434
+ this.decLayout = device.createBindGroupLayout({ entries: [
435
+ {
436
+ binding: 0,
437
+ visibility: 4,
438
+ buffer: { type: "read-only-storage" }
439
+ },
440
+ {
441
+ binding: 1,
442
+ visibility: 4,
443
+ buffer: { type: "read-only-storage" }
444
+ },
445
+ {
446
+ binding: 2,
447
+ visibility: 4,
448
+ buffer: { type: "storage" }
449
+ }
450
+ ] });
451
+ this.ubuf = device.createBuffer({
452
+ size: UBUF_SIZE,
453
+ usage: BUF_UNIFORM_DST
454
+ });
455
+ this.ubiGroup = device.createBindGroup({
456
+ layout: this.layout,
457
+ entries: [{
458
+ binding: 0,
459
+ resource: {
460
+ buffer: this.ubuf,
461
+ size: 112
462
+ }
463
+ }]
464
+ });
465
+ const pl = device.createPipelineLayout({ bindGroupLayouts: [this.layout] });
466
+ const decPl = device.createPipelineLayout({ bindGroupLayouts: [this.layout, this.decLayout] });
467
+ const mk = (code, topology, buffers) => device.createRenderPipeline({
468
+ layout: pl,
469
+ vertex: {
470
+ module: device.createShaderModule({ code }),
471
+ entryPoint: "vs",
472
+ buffers
473
+ },
474
+ fragment: {
475
+ module: device.createShaderModule({ code }),
476
+ entryPoint: "fs",
477
+ targets: [{
478
+ format: this.format,
479
+ blend: BLEND
480
+ }]
481
+ },
482
+ primitive: { topology }
483
+ });
484
+ const iXY = {
485
+ arrayStride: 8,
486
+ stepMode: "instance",
487
+ attributes: [{
488
+ shaderLocation: 0,
489
+ offset: 0,
490
+ format: "float32x2"
491
+ }]
492
+ };
493
+ const iXY1 = {
494
+ arrayStride: 8,
495
+ stepMode: "instance",
496
+ attributes: [{
497
+ shaderLocation: 1,
498
+ offset: 0,
499
+ format: "float32x2"
500
+ }]
501
+ };
502
+ const iPrev = {
503
+ arrayStride: 4,
504
+ stepMode: "instance",
505
+ attributes: [{
506
+ shaderLocation: 2,
507
+ offset: 0,
508
+ format: "float32"
509
+ }]
510
+ };
511
+ const iPrev1 = {
512
+ arrayStride: 4,
513
+ stepMode: "instance",
514
+ attributes: [{
515
+ shaderLocation: 3,
516
+ offset: 0,
517
+ format: "float32"
518
+ }]
519
+ };
520
+ const iPrevPt = {
521
+ arrayStride: 4,
522
+ stepMode: "instance",
523
+ attributes: [{
524
+ shaderLocation: 1,
525
+ offset: 0,
526
+ format: "float32"
527
+ }]
528
+ };
529
+ const vXY = {
530
+ arrayStride: 8,
531
+ stepMode: "vertex",
532
+ attributes: [{
533
+ shaderLocation: 0,
534
+ offset: 0,
535
+ format: "float32x2"
536
+ }]
537
+ };
538
+ const vPrev = {
539
+ arrayStride: 4,
540
+ stepMode: "vertex",
541
+ attributes: [{
542
+ shaderLocation: 1,
543
+ offset: 0,
544
+ format: "float32"
545
+ }]
546
+ };
547
+ this.pipes = {
548
+ line: mk(LINE_WGSL, "triangle-list", [
549
+ iXY,
550
+ iXY1,
551
+ iPrev,
552
+ iPrev1
553
+ ]),
554
+ lineFlat: mk(LINE_FLAT_WGSL, "triangle-list", [iXY, iXY1]),
555
+ area: mk(AREA_WGSL, "triangle-strip", [vXY, vPrev]),
556
+ areaFlat: mk(AREA_FLAT_WGSL, "triangle-strip", [vXY]),
557
+ points: mk(POINTS_WGSL, "triangle-list", [iXY, iPrevPt]),
558
+ dec: device.createComputePipeline({
559
+ layout: decPl,
560
+ compute: {
561
+ module: device.createShaderModule({ code: DECIMATE_WGSL }),
562
+ entryPoint: "dec"
563
+ }
564
+ })
565
+ };
566
+ if (this.timing && device.features.has("timestamp-query")) {
567
+ this.querySet = device.createQuerySet({
568
+ type: "timestamp",
569
+ count: 2
570
+ });
571
+ this.queryBuf = device.createBuffer({
572
+ size: 16,
573
+ usage: BUF_RESOLVE_SRC
574
+ });
575
+ this.readBuf = device.createBuffer({
576
+ size: 16,
577
+ usage: BUF_MAPREAD_DST
578
+ });
579
+ }
580
+ device.lost.then((info) => this.onLost(info));
581
+ }
582
+ onLost(info) {
583
+ if (this.destroyed) return;
584
+ if (info.reason === "destroyed") {
585
+ this.dead = true;
586
+ return;
587
+ }
588
+ this.recovering = true;
589
+ getGpuDevice(this.opts).then((nd) => {
590
+ if (!nd || this.destroyed) {
591
+ this.fail();
592
+ return;
593
+ }
594
+ try {
595
+ this.teardownResources();
596
+ this.setup(nd);
597
+ try {
598
+ this.configure();
599
+ } catch {
600
+ const nc = document.createElement("canvas");
601
+ nc.width = this.canvas.width;
602
+ nc.height = this.canvas.height;
603
+ this.canvas.replaceWith(nc);
604
+ this.canvas = nc;
605
+ const ctx = nc.getContext("webgpu");
606
+ if (!ctx) throw new Error("webgpu context lost");
607
+ this.ctx = ctx;
608
+ this.configure();
609
+ }
610
+ this.appliedRev = -1;
611
+ this.recovering = false;
612
+ } catch {
613
+ this.fail();
614
+ }
615
+ });
616
+ }
617
+ fail() {
618
+ this.dead = true;
619
+ this.recovering = false;
620
+ this.destroy();
621
+ }
622
+ teardownResources() {
623
+ for (const s of this.slots) {
624
+ s.xy.destroy();
625
+ s.prev.destroy();
626
+ s.scratch?.destroy();
627
+ s.dec?.destroy();
628
+ }
629
+ this.slots = [];
630
+ this.slotCount = 0;
631
+ this.ubuf.destroy();
632
+ this.queryBuf?.destroy();
633
+ this.readBuf?.destroy();
634
+ this.querySet?.destroy();
635
+ this.querySet = null;
636
+ this.queryBuf = null;
637
+ this.readBuf = null;
638
+ }
639
+ configure() {
640
+ this.ctx.configure({
641
+ device: this.device,
642
+ format: this.format,
643
+ alphaMode: "premultiplied"
644
+ });
645
+ }
646
+ setSize(cssW, cssH, dpr) {
647
+ this.dpr = dpr;
648
+ this.canvas.width = Math.max(1, Math.round(cssW * dpr));
649
+ this.canvas.height = Math.max(1, Math.round(cssH * dpr));
650
+ if (!this.unavailable) this.configure();
651
+ }
652
+ upload(i, xs, s) {
653
+ const n = Math.min(xs.length, s.y.length);
654
+ const xr = n > 0 ? xs[0] : 0;
655
+ const xu = 1 / ((n > 0 ? xs[n - 1] - xr : 0) || Math.max(1, Math.abs(xr), 1));
656
+ let mn = Infinity;
657
+ let mx = -Infinity;
658
+ for (let j = 0; j < n; j++) {
659
+ const v = s.y[j];
660
+ if (v < mn) mn = v;
661
+ if (v > mx) mx = v;
662
+ }
663
+ const yr = mn === Infinity ? 0 : mn;
664
+ const yu = 1 / (mx - mn || Math.max(1, Math.abs(yr)));
665
+ let slot = this.slots[i];
666
+ const cap = Math.max(1024, Math.ceil(n * 1.25));
667
+ if (!slot || slot.cap < n) {
668
+ slot?.xy.destroy();
669
+ slot?.prev.destroy();
670
+ slot?.dec?.destroy();
671
+ slot = {
672
+ n,
673
+ cap,
674
+ start: 0,
675
+ enc: {
676
+ xr,
677
+ xu,
678
+ yr,
679
+ yu
680
+ },
681
+ encXY: new Float32Array(cap * 2),
682
+ encPrev: new Float32Array(cap),
683
+ xy: this.device.createBuffer({
684
+ size: cap * 8,
685
+ usage: BUF_SERIES
686
+ }),
687
+ prev: this.device.createBuffer({
688
+ size: cap * 4,
689
+ usage: BUF_SERIES
690
+ }),
691
+ scratch: null,
692
+ scratchN: 0,
693
+ dec: null,
694
+ decCols: 0,
695
+ bg: null,
696
+ decStamp: -1
697
+ };
698
+ this.slots[i] = slot;
699
+ } else {
700
+ slot.n = n;
701
+ slot.start = 0;
702
+ slot.enc = {
703
+ xr,
704
+ xu,
705
+ yr,
706
+ yu
707
+ };
708
+ slot.bg = null;
709
+ slot.decStamp = -1;
710
+ }
711
+ const { encXY, encPrev } = slot;
712
+ const prev = s.prevY;
713
+ for (let j = 0; j < n; j++) {
714
+ encXY[2 * j] = (xs[j] - xr) * xu;
715
+ encXY[2 * j + 1] = (s.y[j] - yr) * yu;
716
+ encPrev[j] = prev ? (prev[j] - yr) * yu : 0;
717
+ }
718
+ this.device.queue.writeBuffer(slot.xy, 0, encXY, 0, n * 2);
719
+ this.device.queue.writeBuffer(slot.prev, 0, encPrev, 0, n);
720
+ }
721
+ /**
722
+ * Full-upload hook: call once per frame with a revision that changes
723
+ * whenever the data is replaced/rebuilt. Unchanged revisions are free.
724
+ */
725
+ syncData(d, rev) {
726
+ if (this.unavailable || rev === this.appliedRev && this.slotCount === d.series.length) return;
727
+ for (let i = 0; i < d.series.length; i++) {
728
+ const s = d.series[i];
729
+ if (s) this.upload(i, d.sortedX, s);
730
+ }
731
+ this.slotCount = d.series.length;
732
+ this.appliedRev = rev;
733
+ }
734
+ /**
735
+ * Ring-buffer streaming append: writes only the appended tail per series.
736
+ * `ys[i]` is the new y for series i, `prevYs[i]` the y it animates from
737
+ * (the previous tail value), `dropped` how many leading points were evicted.
738
+ */
739
+ appended(x, ys, prevYs, dropped) {
740
+ if (this.unavailable) return false;
741
+ const q = this.device.queue;
742
+ for (let i = 0; i < this.slotCount; i++) {
743
+ const s = this.slots[i];
744
+ if (!s) return false;
745
+ const n2 = s.n - dropped + 1;
746
+ if (n2 > s.cap) this.grow(s, n2);
747
+ s.start = (s.start + dropped) % s.cap;
748
+ s.n = n2;
749
+ const pNew = (s.start + s.n - 1) % s.cap;
750
+ const { xr, xu, yr, yu } = s.enc;
751
+ const ex = (x - xr) * xu;
752
+ const ey = ((ys[i] ?? 0) - yr) * yu;
753
+ const ep = ((prevYs[i] ?? 0) - yr) * yu;
754
+ const t = this.tailScratch;
755
+ t[0] = ex;
756
+ t[1] = ey;
757
+ q.writeBuffer(s.xy, pNew * 8, t, 0, 2);
758
+ s.encXY[2 * pNew] = ex;
759
+ s.encXY[2 * pNew + 1] = ey;
760
+ t[0] = ep;
761
+ q.writeBuffer(s.prev, pNew * 4, t, 0, 1);
762
+ s.encPrev[pNew] = ep;
763
+ if (s.n > 1) {
764
+ const pOld = (pNew + s.cap - 1) % s.cap;
765
+ q.writeBuffer(s.prev, pOld * 4, t, 0, 1);
766
+ s.encPrev[pOld] = ep;
767
+ }
768
+ }
769
+ return true;
770
+ }
771
+ /** grow ring capacity, unwrapping logical order into the new buffer */
772
+ grow(s, need) {
773
+ const cap = Math.max(s.cap * 2, need + 1024);
774
+ const a = s.cap - s.start;
775
+ const enc = this.device.createCommandEncoder();
776
+ const xy = this.device.createBuffer({
777
+ size: cap * 8,
778
+ usage: BUF_SERIES
779
+ });
780
+ const prev = this.device.createBuffer({
781
+ size: cap * 4,
782
+ usage: BUF_SERIES
783
+ });
784
+ enc.copyBufferToBuffer(s.xy, s.start * 8, xy, 0, a * 8);
785
+ if (s.start > 0) enc.copyBufferToBuffer(s.xy, 0, xy, a * 8, s.start * 8);
786
+ enc.copyBufferToBuffer(s.prev, s.start * 4, prev, 0, a * 4);
787
+ if (s.start > 0) enc.copyBufferToBuffer(s.prev, 0, prev, a * 4, s.start * 4);
788
+ this.device.queue.submit([enc.finish()]);
789
+ s.xy.destroy();
790
+ s.prev.destroy();
791
+ s.xy = xy;
792
+ s.prev = prev;
793
+ const encXY = new Float32Array(cap * 2);
794
+ const encPrev = new Float32Array(cap);
795
+ encXY.set(s.encXY.subarray(s.start * 2, s.cap * 2), 0);
796
+ encXY.set(s.encXY.subarray(0, s.start * 2), a * 2);
797
+ encPrev.set(s.encPrev.subarray(s.start, s.cap), 0);
798
+ encPrev.set(s.encPrev.subarray(0, s.start), a);
799
+ s.encXY = encXY;
800
+ s.encPrev = encPrev;
801
+ s.cap = cap;
802
+ s.start = 0;
803
+ s.bg = null;
804
+ }
805
+ begin(plot, t = 1, flow = 0) {
806
+ this.plotR = plot;
807
+ this.t = t;
808
+ this.flow = flow;
809
+ this.cols = Math.max(1, Math.ceil(plot.x + plot.w));
810
+ this.stamp++;
811
+ this.draws.length = 0;
812
+ this.cmps.length = 0;
813
+ this.uOff = 0;
814
+ }
815
+ /** encoded→css affine for this slot and view domain */
816
+ mapFor(s, view) {
817
+ const { xr, xu, yr, yu } = s.enc;
818
+ const p = this.plotR;
819
+ const ex0 = (view.x[0] - xr) * xu;
820
+ const ex1 = (view.x[1] - xr) * xu;
821
+ const ax = p.w / (ex1 - ex0 || 1e-30);
822
+ const bx = p.x - ex0 * ax;
823
+ const ey0 = (view.y[0] - yr) * yu;
824
+ const ey1 = (view.y[1] - yr) * yu;
825
+ const ay = p.h / (ey1 - ey0 || 1e-30);
826
+ return [
827
+ ax,
828
+ bx,
829
+ ay,
830
+ p.y + ey1 * ay
831
+ ];
832
+ }
833
+ writeU(color, alpha, p, map, s, dec) {
834
+ const off = this.uOff;
835
+ this.uOff += UBYTES;
836
+ const u = /* @__PURE__ */ new Float32Array(28);
837
+ u.set([
838
+ this.canvas.width,
839
+ this.canvas.height,
840
+ this.dpr,
841
+ 0
842
+ ]);
843
+ const [r, g, b, a] = parseColor(color);
844
+ u.set([
845
+ r,
846
+ g,
847
+ b,
848
+ a * alpha
849
+ ], 4);
850
+ u.set(p, 8);
851
+ u.set(map, 12);
852
+ const inv = s.n > 1 ? this.flow / (s.n - 1) : 0;
853
+ u.set([
854
+ this.t,
855
+ this.flow,
856
+ inv,
857
+ 0
858
+ ], 16);
859
+ u.set([
860
+ s.start,
861
+ s.cap,
862
+ s.n,
863
+ 0
864
+ ], 20);
865
+ u.set([
866
+ dec[0],
867
+ dec[1],
868
+ 0,
869
+ 0
870
+ ], 24);
871
+ this.device.queue.writeBuffer(this.ubuf, off, u);
872
+ return off;
873
+ }
874
+ decimateWanted(s) {
875
+ return s.n > this.cols * 2;
876
+ }
877
+ ensureDec(s) {
878
+ if (s.dec && s.decCols >= this.cols) return;
879
+ s.dec?.destroy();
880
+ const cols = Math.max(this.cols, Math.ceil(this.canvas.width / this.dpr));
881
+ s.dec = this.device.createBuffer({
882
+ size: cols * DEC_ELEMS_PER_COL * 8,
883
+ usage: BUF_DEC
884
+ });
885
+ s.decCols = cols;
886
+ s.bg = null;
887
+ }
888
+ dispatchDec(s, map, gapFillY) {
889
+ if (s.decStamp === this.stamp) return;
890
+ s.decStamp = this.stamp;
891
+ this.ensureDec(s);
892
+ if (!s.bg) s.bg = this.device.createBindGroup({
893
+ layout: this.decLayout,
894
+ entries: [
895
+ {
896
+ binding: 0,
897
+ resource: { buffer: s.xy }
898
+ },
899
+ {
900
+ binding: 1,
901
+ resource: { buffer: s.prev }
902
+ },
903
+ {
904
+ binding: 2,
905
+ resource: { buffer: s.dec }
906
+ }
907
+ ]
908
+ });
909
+ const u = this.writeU("#ffffff", 1, [
910
+ 0,
911
+ 0,
912
+ 0,
913
+ 0
914
+ ], map, s, [this.cols, gapFillY]);
915
+ this.cmps.push({
916
+ slot: s,
917
+ u
918
+ });
919
+ }
920
+ line(slot, color, width, view) {
921
+ const s = this.slots[slot];
922
+ if (!s || s.n < 2) return;
923
+ const map = this.mapFor(s, view);
924
+ if (this.decimateWanted(s)) {
925
+ this.dispatchDec(s, map, 0);
926
+ const u = this.writeU(color, 1, [
927
+ width,
928
+ 0,
929
+ 0,
930
+ 0
931
+ ], map, s, [0, 0]);
932
+ this.draws.push({
933
+ pipe: this.pipes.lineFlat,
934
+ vbufs: [{
935
+ buf: s.dec,
936
+ off: 0
937
+ }, {
938
+ buf: s.dec,
939
+ off: 8
940
+ }],
941
+ u,
942
+ verts: 6,
943
+ insts: 4 * this.cols - 1
944
+ });
945
+ return;
946
+ }
947
+ const u = this.writeU(color, 1, [
948
+ width,
949
+ 0,
950
+ 0,
951
+ 0
952
+ ], map, s, [0, 0]);
953
+ const mk = (o0, o1, insts) => ({
954
+ pipe: this.pipes.line,
955
+ vbufs: [
956
+ {
957
+ buf: s.xy,
958
+ off: o0 * 8
959
+ },
960
+ {
961
+ buf: s.xy,
962
+ off: o1 * 8
963
+ },
964
+ {
965
+ buf: s.prev,
966
+ off: o0 * 4
967
+ },
968
+ {
969
+ buf: s.prev,
970
+ off: o1 * 4
971
+ }
972
+ ],
973
+ u,
974
+ verts: 6,
975
+ insts
976
+ });
977
+ if (s.start + s.n <= s.cap) {
978
+ this.draws.push(mk(s.start, s.start + 1, s.n - 1));
979
+ return;
980
+ }
981
+ const capA = s.cap - s.start;
982
+ if (capA > 1) this.draws.push(mk(s.start, s.start + 1, capA - 1));
983
+ this.draws.push(mk(s.cap - 1, 0, 1));
984
+ const rem = s.n - 1 - capA;
985
+ if (rem > 0) this.draws.push(mk(0, 1, rem));
986
+ }
987
+ area(slot, color, baseVal, view) {
988
+ const s = this.slots[slot];
989
+ if (!s || s.n < 2) return;
990
+ const map = this.mapFor(s, view);
991
+ const baseEnc = (baseVal - s.enc.yr) * s.enc.yu;
992
+ const baseCss = map[3] - baseEnc * map[2];
993
+ const p = this.plotR;
994
+ if (this.decimateWanted(s)) {
995
+ this.dispatchDec(s, map, baseEnc);
996
+ const u = this.writeU(color, 1, [
997
+ baseCss,
998
+ p.y,
999
+ p.h,
1000
+ 0
1001
+ ], map, s, [0, 0]);
1002
+ this.draws.push({
1003
+ pipe: this.pipes.areaFlat,
1004
+ vbufs: [{
1005
+ buf: s.dec,
1006
+ off: this.cols * 4 * 8
1007
+ }],
1008
+ u,
1009
+ verts: 8 * this.cols,
1010
+ insts: 1
1011
+ });
1012
+ return;
1013
+ }
1014
+ const m = s.n;
1015
+ const xyD = new Float32Array(m * 4);
1016
+ const pvD = new Float32Array(m * 2);
1017
+ for (let i = 0; i < m; i++) {
1018
+ const ph = (s.start + i) % s.cap;
1019
+ xyD[4 * i] = s.encXY[2 * ph];
1020
+ xyD[4 * i + 1] = s.encXY[2 * ph + 1];
1021
+ xyD[4 * i + 2] = xyD[4 * i];
1022
+ xyD[4 * i + 3] = xyD[4 * i + 1];
1023
+ pvD[2 * i] = s.encPrev[ph];
1024
+ pvD[2 * i + 1] = pvD[2 * i];
1025
+ }
1026
+ if (!s.scratch || s.scratchN < m) {
1027
+ s.scratch?.destroy();
1028
+ s.scratch = this.device.createBuffer({
1029
+ size: Math.max(4096, m * 24),
1030
+ usage: BUF_VERTEX_DST
1031
+ });
1032
+ s.scratchN = m;
1033
+ }
1034
+ const prevOff = m * 16;
1035
+ this.device.queue.writeBuffer(s.scratch, 0, xyD);
1036
+ this.device.queue.writeBuffer(s.scratch, prevOff, pvD);
1037
+ const u = this.writeU(color, 1, [
1038
+ baseCss,
1039
+ p.y,
1040
+ p.h,
1041
+ 0
1042
+ ], map, s, [0, 0]);
1043
+ this.draws.push({
1044
+ pipe: this.pipes.area,
1045
+ vbufs: [{
1046
+ buf: s.scratch,
1047
+ off: 0
1048
+ }, {
1049
+ buf: s.scratch,
1050
+ off: prevOff
1051
+ }],
1052
+ u,
1053
+ verts: m * 2,
1054
+ insts: 1
1055
+ });
1056
+ }
1057
+ points(slot, color, radius, alpha, view) {
1058
+ const s = this.slots[slot];
1059
+ if (!s || s.n < 1) return;
1060
+ const map = this.mapFor(s, view);
1061
+ const u = this.writeU(color, alpha, [
1062
+ radius,
1063
+ 0,
1064
+ 0,
1065
+ 0
1066
+ ], map, s, [0, 0]);
1067
+ const mk = (o0, insts) => ({
1068
+ pipe: this.pipes.points,
1069
+ vbufs: [{
1070
+ buf: s.xy,
1071
+ off: o0 * 8
1072
+ }, {
1073
+ buf: s.prev,
1074
+ off: o0 * 4
1075
+ }],
1076
+ u,
1077
+ verts: 6,
1078
+ insts
1079
+ });
1080
+ if (s.start + s.n <= s.cap) {
1081
+ this.draws.push(mk(s.start, s.n));
1082
+ return;
1083
+ }
1084
+ const capA = s.cap - s.start;
1085
+ this.draws.push(mk(s.start, capA));
1086
+ if (s.n - capA > 0) this.draws.push(mk(0, s.n - capA));
1087
+ }
1088
+ end() {
1089
+ if (this.unavailable || !this.draws.length && !this.cmps.length) return;
1090
+ try {
1091
+ const encoder = this.device.createCommandEncoder();
1092
+ if (this.cmps.length) {
1093
+ const cp = encoder.beginComputePass();
1094
+ cp.setPipeline(this.pipes.dec);
1095
+ for (const c of this.cmps) {
1096
+ cp.setBindGroup(0, this.ubiGroup, [c.u]);
1097
+ cp.setBindGroup(1, c.slot.bg);
1098
+ cp.dispatchWorkgroups(Math.ceil(this.cols / 64));
1099
+ }
1100
+ cp.end();
1101
+ }
1102
+ if (this.draws.length) {
1103
+ const view = this.ctx.getCurrentTexture().createView();
1104
+ const d = this.dpr;
1105
+ const plot = this.plotR;
1106
+ const pass = encoder.beginRenderPass({
1107
+ colorAttachments: [{
1108
+ view,
1109
+ loadOp: "clear",
1110
+ storeOp: "store",
1111
+ clearValue: {
1112
+ r: 0,
1113
+ g: 0,
1114
+ b: 0,
1115
+ a: 0
1116
+ }
1117
+ }],
1118
+ ...this.querySet ? { timestampWrites: {
1119
+ querySet: this.querySet,
1120
+ beginningOfPassWriteIndex: 0,
1121
+ endOfPassWriteIndex: 1
1122
+ } } : {}
1123
+ });
1124
+ pass.setScissorRect(Math.max(0, Math.floor(plot.x * d)), Math.max(0, Math.floor(plot.y * d)), Math.max(1, Math.floor(plot.w * d)), Math.max(1, Math.floor(plot.h * d)));
1125
+ for (const op of this.draws) {
1126
+ pass.setPipeline(op.pipe);
1127
+ pass.setBindGroup(0, this.ubiGroup, [op.u]);
1128
+ for (let i = 0; i < op.vbufs.length; i++) {
1129
+ const v = op.vbufs[i];
1130
+ if (v) pass.setVertexBuffer(i, v.buf, v.off);
1131
+ }
1132
+ pass.draw(op.verts, op.insts);
1133
+ }
1134
+ pass.end();
1135
+ }
1136
+ const qs = this.querySet;
1137
+ const qb = this.queryBuf;
1138
+ const rb = this.readBuf;
1139
+ if (qs && qb && rb && !this.reading) {
1140
+ encoder.resolveQuerySet(qs, 0, 2, qb, 0);
1141
+ encoder.copyBufferToBuffer(qb, 0, rb, 0, 16);
1142
+ this.reading = true;
1143
+ this.device.queue.submit([encoder.finish()]);
1144
+ rb.mapAsync(MAP_READ).then(() => {
1145
+ const v = new BigUint64Array(rb.getMappedRange());
1146
+ const t0 = v[0];
1147
+ const t1 = v[1];
1148
+ if (t0 !== void 0 && t1 !== void 0) this.gpuMs = Number(t1 - t0) / 1e6;
1149
+ rb.unmap();
1150
+ this.reading = false;
1151
+ }, () => {
1152
+ this.reading = false;
1153
+ });
1154
+ } else this.device.queue.submit([encoder.finish()]);
1155
+ } catch {}
1156
+ }
1157
+ destroy() {
1158
+ this.destroyed = true;
1159
+ for (const s of this.slots) {
1160
+ s.xy.destroy();
1161
+ s.prev.destroy();
1162
+ s.scratch?.destroy();
1163
+ s.dec?.destroy();
1164
+ }
1165
+ this.slots = [];
1166
+ this.ubuf?.destroy();
1167
+ this.queryBuf?.destroy();
1168
+ this.readBuf?.destroy();
1169
+ this.querySet?.destroy();
1170
+ try {
1171
+ this.ctx.unconfigure();
1172
+ } catch {}
1173
+ this.canvas.remove();
1174
+ }
1175
+ };
1176
+ const BLEND = {
1177
+ color: {
1178
+ srcFactor: "src-alpha",
1179
+ dstFactor: "one-minus-src-alpha",
1180
+ operation: "add"
1181
+ },
1182
+ alpha: {
1183
+ srcFactor: "one",
1184
+ dstFactor: "one-minus-src-alpha",
1185
+ operation: "add"
1186
+ }
1187
+ };
1188
+ //#endregion
1189
+ exports.GpuSeries = GpuSeries;
1190
+
1191
+ //# sourceMappingURL=series-PmAjztEh.cjs.map