@fjader/svelte 0.2.1 → 0.2.3
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/components/axes/BandAxisTicks.svelte +63 -24
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/charts/ChartContainer.svelte +2 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
- package/dist/components/marks/AreaMarkX.svelte +19 -6
- package/dist/components/marks/AreaMarkY.svelte +19 -6
- package/dist/components/marks/BarMarkX.svelte +3 -0
- package/dist/components/marks/BarMarkY.svelte +3 -0
- package/dist/components/marks/DotsMark.svelte +3 -0
- package/dist/components/marks/LineMark.svelte +3 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
- package/dist/components/marks/RectMark.svelte +3 -0
- package/dist/components/marks/RectXMark.svelte +3 -0
- package/dist/components/marks/RectYMark.svelte +3 -0
- package/dist/components/marks/RuleMarkX.svelte +10 -2
- package/dist/components/marks/RuleMarkY.svelte +10 -2
- package/dist/components/marks/TextMark.svelte +68 -4
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
- package/dist/components/marks/lifecycle/domain-queries.js +13 -11
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +3 -0
- package/dist/components/rendering/Areas.svelte +32 -9
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +51 -10
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +28 -8
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +20 -5
- package/dist/components/rendering/Rects.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +93 -0
- package/dist/components/rendering/activity-reads.js +135 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +2 -0
- package/dist/data/DataAccessor.js +1 -1
- package/dist/gpu/ColorScaleShaderLib.d.ts +14 -2
- package/dist/gpu/ColorScaleShaderLib.js +59 -7
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/NanShaderLib.d.ts +30 -0
- package/dist/gpu/NanShaderLib.js +42 -0
- package/dist/gpu/ResolveShader.d.ts +3 -3
- package/dist/gpu/ResolveShader.js +6 -5
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +86 -7
- package/dist/gpu/SizeScaleShaderLib.d.ts +5 -5
- package/dist/gpu/SizeScaleShaderLib.js +6 -5
- package/dist/gpu/decimation/M4Shader.d.ts +13 -7
- package/dist/gpu/decimation/M4Shader.js +79 -64
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.js +1 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +2 -1
- package/dist/interactors/Interval2D.svelte +2 -1
- package/dist/interactors/ParallelAxisSource.svelte +4 -4
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +17 -36
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +2 -1
- package/dist/interactors/TooltipManager.svelte.js +8 -3
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
- package/dist/query-engine/FieldActivity.svelte.js +207 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
- package/dist/query-engine/FilterGroup.svelte.js +181 -122
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
- package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +90 -28
- package/dist/query-engine/categorical-identity.d.ts +99 -27
- package/dist/query-engine/categorical-identity.js +210 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +9 -5
- package/dist/query-engine/preagg/plan.js +13 -11
- package/dist/query-engine/toggle-predicate.js +12 -1
- package/dist/scales/ActivityTable.d.ts +99 -0
- package/dist/scales/ActivityTable.js +211 -0
- package/dist/scales/BandScaleState.svelte.d.ts +28 -7
- package/dist/scales/BandScaleState.svelte.js +55 -20
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +2 -0
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +97 -3
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/lib/gpu/ScaleTemplateSystem.ts
|
|
2
2
|
import { COLOR_SCALE_STRUCTS, COLOR_SCALE_BINDINGS, COLOR_SCALE_FUNCTIONS } from './ColorScaleShaderLib';
|
|
3
3
|
import { SIZE_SCALE_STRUCTS, SIZE_SCALE_BINDINGS, SIZE_SCALE_FUNCTIONS } from './SizeScaleShaderLib';
|
|
4
|
+
import { NO_GROUP } from './identity-slot-packing';
|
|
5
|
+
import { isNanExpr } from './NanShaderLib';
|
|
6
|
+
import { DEFAULT_INACTIVE_OPACITY } from '../scales/ActivityTable';
|
|
4
7
|
/**
|
|
5
8
|
* Scale fragment definitions
|
|
6
9
|
* Each scale type provides: declaration, function call, and implementation
|
|
@@ -280,6 +283,9 @@ function _scaleFunctionName(scaleType, axis) {
|
|
|
280
283
|
* `ScaleParam`, 16 bytes each. Every layout declares the same size because a
|
|
281
284
|
* draw requires every declared byte to have been set, and the pass sets them
|
|
282
285
|
* once, before its first draw.
|
|
286
|
+
*
|
|
287
|
+
* Immediates carry state shared across the pass and read by several marks: the
|
|
288
|
+
* chart's scales and settings. 64 bytes are available on M-series Macs.
|
|
283
289
|
*/
|
|
284
290
|
export const CHART_SCALE_IMMEDIATE_SIZE = 32;
|
|
285
291
|
/**
|
|
@@ -370,6 +376,7 @@ export class ScaleTemplateSystem {
|
|
|
370
376
|
// ===== 3. HELPER FUNCTIONS =====
|
|
371
377
|
|
|
372
378
|
${this._buildSlotTableHelpers()}
|
|
379
|
+
${this._buildBandActivityHelpers(scaleTypes)}
|
|
373
380
|
${bandHelpers}
|
|
374
381
|
${implementations}
|
|
375
382
|
${scaleFunctions}
|
|
@@ -409,6 +416,24 @@ export class ScaleTemplateSystem {
|
|
|
409
416
|
};
|
|
410
417
|
var<immediate> chartScales: ChartScales;
|
|
411
418
|
|
|
419
|
+
// Selection dimming: a mark's activity mapped to an opacity, with
|
|
420
|
+
// inactive categories at DEFAULT_INACTIVE_OPACITY. selectionOpacity in
|
|
421
|
+
// ActivityTable.ts is its CPU twin, for band labels and legend entries,
|
|
422
|
+
// and both read the same constant so they cannot drift apart.
|
|
423
|
+
const SELECTION_DIM_STRENGTH: f32 = ${(1 - DEFAULT_INACTIVE_OPACITY).toFixed(6)};
|
|
424
|
+
fn selectionDim(activity: f32) -> f32 {
|
|
425
|
+
return 1.0 - SELECTION_DIM_STRENGTH * (1.0 - activity);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// A mark's activities combined: colour, and band for marks on band axes.
|
|
429
|
+
// The least active wins rather than the product, so a bar whose colour
|
|
430
|
+
// and band are the same field fades on the same curve as its label
|
|
431
|
+
// instead of twice as fast. At rest every activity is 0 or 1, where the
|
|
432
|
+
// two agree.
|
|
433
|
+
fn markActivity(color: f32, bandX: f32, bandY: f32) -> f32 {
|
|
434
|
+
return min(color, min(bandX, bandY));
|
|
435
|
+
}
|
|
436
|
+
|
|
412
437
|
// The ONE place that says where a chart's scales come from. Every
|
|
413
438
|
// consumer reads through these — scaleX/scaleY, the bandwidth helpers,
|
|
414
439
|
// and mark code that needs the raw params. A mark with scales of its own
|
|
@@ -423,6 +448,39 @@ export class ScaleTemplateSystem {
|
|
|
423
448
|
`;
|
|
424
449
|
return `${structDef}\n${chartScaleDeclaration}`;
|
|
425
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
|
|
453
|
+
* selected a band or point axis's category is in the mark's filter group,
|
|
454
|
+
* 0..1, for selection dimming.
|
|
455
|
+
*
|
|
456
|
+
* Emitted for every shader, so a renderer can call both without knowing its
|
|
457
|
+
* scale types: an axis that is not band or point returns 1.0. On a band or
|
|
458
|
+
* point axis the value is an identity id and the chart's scale carries its
|
|
459
|
+
* block base in p4, as for the slot lookup. A missing category (-1) or a NaN
|
|
460
|
+
* is fully active, checked before the cast, since u32() of a negative or NaN
|
|
461
|
+
* float is indeterminate in WGSL.
|
|
462
|
+
*/
|
|
463
|
+
_buildBandActivityHelpers(scaleTypes) {
|
|
464
|
+
const helper = (axis, type) => {
|
|
465
|
+
if (type !== 'band' && type !== 'point') {
|
|
466
|
+
return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
|
|
467
|
+
return 1.0;
|
|
468
|
+
}`;
|
|
469
|
+
}
|
|
470
|
+
return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
|
|
471
|
+
if (value < 0.0 || ${isNanExpr('value')}) {
|
|
472
|
+
return 1.0;
|
|
473
|
+
}
|
|
474
|
+
return identityActivity(u32(chartScale${axis}().p4), group, u32(value));
|
|
475
|
+
}`;
|
|
476
|
+
};
|
|
477
|
+
return `
|
|
478
|
+
// ===== BAND SELECTION ACTIVITY =====
|
|
479
|
+
${helper('X', scaleTypes.x)}
|
|
480
|
+
|
|
481
|
+
${helper('Y', scaleTypes.y)}
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
426
484
|
/**
|
|
427
485
|
* The shared slot table, and the lookup into it.
|
|
428
486
|
*
|
|
@@ -459,12 +517,12 @@ export class ScaleTemplateSystem {
|
|
|
459
517
|
}
|
|
460
518
|
|
|
461
519
|
fn identitySlot(base: u32, id: u32) -> f32 {
|
|
462
|
-
// Block layout: [count, slot0, slot1, ...]. The count is what
|
|
463
|
-
// block self-describing, so an id past this scale's end is
|
|
464
|
-
// instead of reading into the next scale's slots. It survives
|
|
465
|
-
// exactly because it holds the same value in both halves, and
|
|
466
|
-
// returns equal ends as they are — u32() truncates, so an
|
|
467
|
-
// would drop the last id.
|
|
520
|
+
// Block layout: [count, actRef, n, slot0, slot1, ...]. The count is what
|
|
521
|
+
// makes a block self-describing, so an id past this scale's end is
|
|
522
|
+
// discarded instead of reading into the next scale's slots. It survives
|
|
523
|
+
// the resolve exactly because it holds the same value in both halves, and
|
|
524
|
+
// the kernel returns equal ends as they are — u32() truncates, so an
|
|
525
|
+
// inexact mix would drop the last id.
|
|
468
526
|
let count = u32(identitySlots[base]);
|
|
469
527
|
if (id >= count) {
|
|
470
528
|
return -1.0;
|
|
@@ -473,7 +531,28 @@ export class ScaleTemplateSystem {
|
|
|
473
531
|
// both halves, so the resolve wrote -1 without a branch; one that is
|
|
474
532
|
// leaving holds a real slot in both, which is what lets it travel. The
|
|
475
533
|
// scale normalises that before packing, so nothing here re-derives it.
|
|
476
|
-
return identitySlots[base +
|
|
534
|
+
return identitySlots[base + 3u + id];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
fn identityActivity(base: u32, group: u32, id: u32) -> f32 {
|
|
538
|
+
// How selected this id is in the mark's filter group, 1 active and 0
|
|
539
|
+
// inactive, already resolved at that group's selection clock. The scale
|
|
540
|
+
// block's header points at its field's activity block (actRef) and says
|
|
541
|
+
// how many ids it covers (n); the block holds one run of n per group.
|
|
542
|
+
// Header first, so this is two dependent reads. Past n, or in the empty
|
|
543
|
+
// block where n is 0, an id is fully active.
|
|
544
|
+
//
|
|
545
|
+
// NO_GROUP (a mark whose selectionResponse is 'filter') is fully active,
|
|
546
|
+
// checked before anything is read, so it never becomes an offset.
|
|
547
|
+
if (group == ${NO_GROUP}u) {
|
|
548
|
+
return 1.0;
|
|
549
|
+
}
|
|
550
|
+
let actRef = u32(identitySlots[base + 1u]);
|
|
551
|
+
let n = u32(identitySlots[base + 2u]);
|
|
552
|
+
if (id >= n) {
|
|
553
|
+
return 1.0;
|
|
554
|
+
}
|
|
555
|
+
return identitySlots[actRef + n * group + id];
|
|
477
556
|
}
|
|
478
557
|
`;
|
|
479
558
|
}
|
|
@@ -19,13 +19,13 @@ export declare const MAX_SIZE_SLOTS = 64;
|
|
|
19
19
|
* changes rather than per frame. Every property that makes a uniform buffer the
|
|
20
20
|
* right container was already true of it.
|
|
21
21
|
*
|
|
22
|
-
* What it buys: the vertex stage
|
|
23
|
-
* maxStorageBuffersPerShaderStage of 8
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* What it buys: a slot in the vertex stage's storage budget, the default
|
|
23
|
+
* maxStorageBuffersPerShaderStage of 8, of which Rects uses 7 (six mark
|
|
24
|
+
* channels and the identity slot table). Uniform buffers are nowhere near
|
|
25
|
+
* their own default of 12.
|
|
26
26
|
*
|
|
27
27
|
* Dynamic indexing — `sizeConfigs[scaleIndex]` — is permitted on uniform arrays
|
|
28
28
|
* in WGSL, and the shared slot table already relies on the same thing.
|
|
29
29
|
*/
|
|
30
30
|
export declare const SIZE_SCALE_BINDINGS = "\n@group(2) @binding(4) var<uniform> sizeConfigs: array<SizeConfig, 64>;\n";
|
|
31
|
-
export declare const SIZE_SCALE_FUNCTIONS
|
|
31
|
+
export declare const SIZE_SCALE_FUNCTIONS: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNanExpr } from './NanShaderLib';
|
|
1
2
|
// src/lib/gpu/SizeScaleShaderLib.js
|
|
2
3
|
export const SIZE_SCALE_STRUCTS = /* wgsl */ `
|
|
3
4
|
struct SizeConfig {
|
|
@@ -27,10 +28,10 @@ export const MAX_SIZE_SLOTS = 64;
|
|
|
27
28
|
* changes rather than per frame. Every property that makes a uniform buffer the
|
|
28
29
|
* right container was already true of it.
|
|
29
30
|
*
|
|
30
|
-
* What it buys: the vertex stage
|
|
31
|
-
* maxStorageBuffersPerShaderStage of 8
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* What it buys: a slot in the vertex stage's storage budget, the default
|
|
32
|
+
* maxStorageBuffersPerShaderStage of 8, of which Rects uses 7 (six mark
|
|
33
|
+
* channels and the identity slot table). Uniform buffers are nowhere near
|
|
34
|
+
* their own default of 12.
|
|
34
35
|
*
|
|
35
36
|
* Dynamic indexing — `sizeConfigs[scaleIndex]` — is permitted on uniform arrays
|
|
36
37
|
* in WGSL, and the shared slot table already relies on the same thing.
|
|
@@ -43,7 +44,7 @@ fn getSizeFromScale(scaleIndex: u32, value: f32) -> f32 {
|
|
|
43
44
|
let config = sizeConfigs[scaleIndex];
|
|
44
45
|
|
|
45
46
|
// Handle NaN → rangeMin
|
|
46
|
-
if (value
|
|
47
|
+
if (${isNanExpr('value')}) {
|
|
47
48
|
return config.rangeMin;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
*
|
|
28
28
|
* The x scale's params reach passes 1 and 2 as immediates, pushed before each
|
|
29
29
|
* dispatch, rather than as an index into the global scale table.
|
|
30
|
+
*
|
|
31
|
+
* Every pass reads and writes the data through u32 views. NaN (a null, or an
|
|
32
|
+
* empty bin) is found by its bit pattern and moved as bits, never compared as
|
|
33
|
+
* a float: Metal compilers may fold `x != x` to false (see NanShaderLib), and
|
|
34
|
+
* where they do, compaction keeps the empty bins and the line loses every
|
|
35
|
+
* segment that touches one.
|
|
30
36
|
*/
|
|
31
37
|
declare const WORKGROUP_SIZE = 256;
|
|
32
38
|
/** Bytes of immediate data passes 1 and 2 read: one ScaleParam. */
|
|
@@ -44,7 +50,7 @@ declare const X_SCALE_IMMEDIATE_SIZE = 16;
|
|
|
44
50
|
* Group 0: inputLayout (data dummies + config + bins)
|
|
45
51
|
* Group 1: controlLayout (control)
|
|
46
52
|
*/
|
|
47
|
-
declare const INIT_BINS_SHADER = "\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn
|
|
53
|
+
declare const INIT_BINS_SHADER = "\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn sortableFromBits(bits: u32) -> u32 {\n\treturn select(~bits, bits ^ 0x80000000u, (bits & 0x80000000u) == 0u);\n}\n\n\nconst NAN_BITS: u32 = 0x7FC00000u;\n\nfn isNanBits(u: u32) -> bool {\n\treturn (u & 0x7fffffffu) > 0x7f800000u;\n}\n\nfn isNaN(v: f32) -> bool {\n\treturn isNanBits(bitcast<u32>(v));\n}\n\n\n\n@group(0) @binding(4) var<uniform> config: M4Config;\n@group(0) @binding(5) var<storage, read_write> bins: array<M4Bin>;\n@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\tlet binKey = gid.x;\n\tlet totalBins = config.numSeries * config.numBins;\n\n\t// Thread 0: read JS decision, set instance count\n\tif (binKey == 0u) {\n\t\tlet outputSize = config.numBins * config.numSeries * 4u;\n\t\t// JS sets _pad: 1 = decimate, 2 = passthrough\n\t\tlet shouldDec = config.decimationMode == 1u;\n\t\tatomicStore(&control[4], select(0u, 1u, shouldDec));\n\t\tatomicStore(&control[1], select(0u, outputSize, shouldDec));\n\t}\n\n\t// Always init bins \u2014 cheap and ensures clean state for potential decimation.\n\tif (binKey >= totalBins) { return; }\n\n\tatomicStore(&bins[binKey].firstIdx, 0xFFFFFFFFu);\n\tatomicStore(&bins[binKey].lastIdx, 0u);\n\tatomicStore(&bins[binKey].minYSortable, 0xFFFFFFFFu);\n\tatomicStore(&bins[binKey].maxYSortable, 0u);\n\tatomicStore(&bins[binKey].minYIdx, 0xFFFFFFFFu);\n\tatomicStore(&bins[binKey].maxYIdx, 0xFFFFFFFFu);\n}\n";
|
|
48
54
|
/**
|
|
49
55
|
* Pass 1: Bin each point by scaled x position.
|
|
50
56
|
*
|
|
@@ -55,7 +61,7 @@ declare const INIT_BINS_SHADER = "\n\nstruct M4Config {\n\tpointCount: u32,\n\tn
|
|
|
55
61
|
* Group 1: controlLayout (control)
|
|
56
62
|
* Immediates: the x scale (ScaleParam)
|
|
57
63
|
*/
|
|
58
|
-
declare const BIN_POINTS_SHADER = "\nrequires immediate_address_space;\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn
|
|
64
|
+
declare const BIN_POINTS_SHADER = "\nrequires immediate_address_space;\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn sortableFromBits(bits: u32) -> u32 {\n\treturn select(~bits, bits ^ 0x80000000u, (bits & 0x80000000u) == 0u);\n}\n\n\nconst NAN_BITS: u32 = 0x7FC00000u;\n\nfn isNanBits(u: u32) -> bool {\n\treturn (u & 0x7fffffffu) > 0x7f800000u;\n}\n\nfn isNaN(v: f32) -> bool {\n\treturn isNanBits(bitcast<u32>(v));\n}\n\n\n\nstruct ScaleParam {\n\tp1: f32,\n\tp2: f32,\n\tp3: f32,\n\tp4: f32,\n};\n\nvar<immediate> xScale: ScaleParam;\n\n\n@group(0) @binding(0) var<storage, read> xBits: array<u32>;\n@group(0) @binding(1) var<storage, read> yBits: array<u32>;\n@group(0) @binding(2) var<storage, read> seriesBits: array<u32>;\n@group(0) @binding(4) var<uniform> config: M4Config;\n@group(0) @binding(5) var<storage, read_write> bins: array<M4Bin>;\n@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\t// Only bin if decimating\n\tif (atomicLoad(&control[4]) == 0u) { return; }\n\n\tlet idx = gid.x;\n\tif (idx >= config.pointCount) { return; }\n\n\t// Skip NaN points, tested on the bits before either becomes a float\n\tlet xb = xBits[idx];\n\tlet yb = yBits[idx];\n\tif (isNanBits(xb) || isNanBits(yb)) { return; }\n\tlet x = bitcast<f32>(xb);\n\n\tlet domainMin = xScale.p1;\n\tlet domainMax = xScale.p2;\n\n\tlet t = (x - domainMin) / (domainMax - domainMin);\n\n\t// Series: 0-indexed from ViewManager (-1 = null/unknown, 0+ = categories)\n\tlet seriesB = seriesBits[idx];\n\tif (isNanBits(seriesB)) { return; }\n\tlet seriesF = bitcast<f32>(seriesB);\n\tif (seriesF < 0.0) { return; }\n\tlet series = u32(seriesF);\n\tif (series >= config.numSeries) { return; }\n\n\t// Bin assignment: boundary bins capture off-screen points for line connectivity.\n\t// Bins 0 and numBins-1 are boundary bins; bins 1..numBins-2 map to viewport pixels.\n\tvar bin: u32;\n\tif (t < 0.0) {\n\t\tbin = 0u;\n\t} else if (t > 1.0) {\n\t\tbin = config.numBins - 1u;\n\t} else {\n\t\tlet innerBins = config.numBins - 2u;\n\t\tbin = 1u + min(u32(floor(t * f32(innerBins))), innerBins - 1u);\n\t}\n\tlet binKey = series * config.numBins + bin;\n\n\tatomicMin(&bins[binKey].firstIdx, idx);\n\tatomicMax(&bins[binKey].lastIdx, idx);\n\n\tlet ySortable = sortableFromBits(yb);\n\tatomicMin(&bins[binKey].minYSortable, ySortable);\n\tatomicMax(&bins[binKey].maxYSortable, ySortable);\n}\n";
|
|
59
65
|
/**
|
|
60
66
|
* Pass 2: Find indices of min/max Y points.
|
|
61
67
|
* Each thread re-checks if its Y value matches the bin's min/max.
|
|
@@ -67,7 +73,7 @@ declare const BIN_POINTS_SHADER = "\nrequires immediate_address_space;\n\nstruct
|
|
|
67
73
|
* Group 1: controlLayout (control — only reads shouldDecimate)
|
|
68
74
|
* Immediates: the x scale (ScaleParam)
|
|
69
75
|
*/
|
|
70
|
-
declare const FIND_MINMAX_SHADER = "\nrequires immediate_address_space;\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn
|
|
76
|
+
declare const FIND_MINMAX_SHADER = "\nrequires immediate_address_space;\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn sortableFromBits(bits: u32) -> u32 {\n\treturn select(~bits, bits ^ 0x80000000u, (bits & 0x80000000u) == 0u);\n}\n\n\nconst NAN_BITS: u32 = 0x7FC00000u;\n\nfn isNanBits(u: u32) -> bool {\n\treturn (u & 0x7fffffffu) > 0x7f800000u;\n}\n\nfn isNaN(v: f32) -> bool {\n\treturn isNanBits(bitcast<u32>(v));\n}\n\n\n\nstruct ScaleParam {\n\tp1: f32,\n\tp2: f32,\n\tp3: f32,\n\tp4: f32,\n};\n\nvar<immediate> xScale: ScaleParam;\n\n\n@group(0) @binding(0) var<storage, read> xBits: array<u32>;\n@group(0) @binding(1) var<storage, read> yBits: array<u32>;\n@group(0) @binding(2) var<storage, read> seriesBits: array<u32>;\n@group(0) @binding(4) var<uniform> config: M4Config;\n@group(0) @binding(5) var<storage, read_write> bins: array<M4Bin>;\n@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\tif (atomicLoad(&control[4]) == 0u) { return; }\n\n\tlet idx = gid.x;\n\tif (idx >= config.pointCount) { return; }\n\n\t// NaN skip on the bits (matches binPoints pass)\n\tlet xb = xBits[idx];\n\tlet yb = yBits[idx];\n\tif (isNanBits(xb) || isNanBits(yb)) { return; }\n\tlet x = bitcast<f32>(xb);\n\n\tlet domainMin = xScale.p1;\n\tlet domainMax = xScale.p2;\n\n\tlet t = (x - domainMin) / (domainMax - domainMin);\n\n\t// Series: 0-indexed (matches binPoints pass)\n\tlet seriesB = seriesBits[idx];\n\tif (isNanBits(seriesB)) { return; }\n\tlet seriesF = bitcast<f32>(seriesB);\n\tif (seriesF < 0.0) { return; }\n\tlet series = u32(seriesF);\n\tif (series >= config.numSeries) { return; }\n\n\t// Boundary bin clamping (matches binPoints pass)\n\tvar bin: u32;\n\tif (t < 0.0) {\n\t\tbin = 0u;\n\t} else if (t > 1.0) {\n\t\tbin = config.numBins - 1u;\n\t} else {\n\t\tlet innerBins = config.numBins - 2u;\n\t\tbin = 1u + min(u32(floor(t * f32(innerBins))), innerBins - 1u);\n\t}\n\tlet binKey = series * config.numBins + bin;\n\n\tlet ySortable = sortableFromBits(yb);\n\n\tif (ySortable == atomicLoad(&bins[binKey].minYSortable)) {\n\t\tatomicMin(&bins[binKey].minYIdx, idx);\n\t}\n\tif (ySortable == atomicLoad(&bins[binKey].maxYSortable)) {\n\t\tatomicMin(&bins[binKey].maxYIdx, idx);\n\t}\n}\n";
|
|
71
77
|
/**
|
|
72
78
|
* Pass 3: Emit decimated output arrays.
|
|
73
79
|
* Each thread processes one bin, collecting up to 4 unique indices
|
|
@@ -82,7 +88,7 @@ declare const FIND_MINMAX_SHADER = "\nrequires immediate_address_space;\n\nstruc
|
|
|
82
88
|
* Group 0: emitInputLayout (x, y, color, config, bins)
|
|
83
89
|
* Group 1: outputLayout (xOut, yOut, seriesOut, colorOut)
|
|
84
90
|
*/
|
|
85
|
-
declare const EMIT_OUTPUT_SHADER = "\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn
|
|
91
|
+
declare const EMIT_OUTPUT_SHADER = "\n\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\nstruct M4Bin {\n\tfirstIdx: atomic<u32>,\n\tlastIdx: atomic<u32>,\n\tminYSortable: atomic<u32>,\n\tmaxYSortable: atomic<u32>,\n\tminYIdx: atomic<u32>,\n\tmaxYIdx: atomic<u32>,\n};\n\nfn sortableFromBits(bits: u32) -> u32 {\n\treturn select(~bits, bits ^ 0x80000000u, (bits & 0x80000000u) == 0u);\n}\n\n\nconst NAN_BITS: u32 = 0x7FC00000u;\n\nfn isNanBits(u: u32) -> bool {\n\treturn (u & 0x7fffffffu) > 0x7f800000u;\n}\n\nfn isNaN(v: f32) -> bool {\n\treturn isNanBits(bitcast<u32>(v));\n}\n\n\n\n// Note: seriesValues not bound here \u2014 series is derived from binKey / numBins.\n// This keeps total storage buffers within WebGPU default limit.\n@group(0) @binding(0) var<storage, read> xBits: array<u32>;\n@group(0) @binding(1) var<storage, read> yBits: array<u32>;\n@group(0) @binding(2) var<storage, read> colorBits: array<u32>;\n@group(0) @binding(3) var<uniform> config: M4Config;\n@group(0) @binding(4) var<storage, read_write> bins: array<M4Bin>;\n@group(1) @binding(0) var<storage, read_write> xOut: array<u32>;\n@group(1) @binding(1) var<storage, read_write> yOut: array<u32>;\n@group(1) @binding(2) var<storage, read_write> seriesOut: array<u32>;\n@group(1) @binding(3) var<storage, read_write> colorOut: array<u32>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\tlet binKey = gid.x;\n\tlet totalBins = config.numSeries * config.numBins;\n\tif (binKey >= totalBins) { return; }\n\n\tlet outBase = binKey * 4u;\n\tlet firstIdx = atomicLoad(&bins[binKey].firstIdx);\n\n\t// Empty bin: NaN in all 4 output slots, written as bits\n\tif (firstIdx == 0xFFFFFFFFu) {\n\t\tfor (var i = 0u; i < 4u; i++) {\n\t\t\txOut[outBase + i] = NAN_BITS;\n\t\t\tyOut[outBase + i] = NAN_BITS;\n\t\t\tseriesOut[outBase + i] = NAN_BITS;\n\t\t\tcolorOut[outBase + i] = NAN_BITS;\n\t\t}\n\t\treturn;\n\t}\n\n\tlet lastIdx = atomicLoad(&bins[binKey].lastIdx);\n\tlet minYIdx = atomicLoad(&bins[binKey].minYIdx);\n\tlet maxYIdx = atomicLoad(&bins[binKey].maxYIdx);\n\n\t// Collect up to 4 unique indices, sorted by original index (= x-order)\n\tvar indices: array<u32, 4>;\n\tvar count = 0u;\n\n\t// Insert in sorted order, skip duplicates\n\tvar candidates = array<u32, 4>(firstIdx, lastIdx, minYIdx, maxYIdx);\n\n\t// Simple insertion sort for up to 4 candidates\n\tfor (var c = 0u; c < 4u; c++) {\n\t\tlet candidate = candidates[c];\n\t\t// Check if already present\n\t\tvar isDuplicate = false;\n\t\tfor (var k = 0u; k < count; k++) {\n\t\t\tif (indices[k] == candidate) {\n\t\t\t\tisDuplicate = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (isDuplicate) { continue; }\n\n\t\t// Insert in sorted position\n\t\tvar insertPos = count;\n\t\tfor (var k = 0u; k < count; k++) {\n\t\t\tif (candidate < indices[k]) {\n\t\t\t\tinsertPos = k;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t// Shift elements right\n\t\tfor (var k = count; k > insertPos; k--) {\n\t\t\tindices[k] = indices[k - 1u];\n\t\t}\n\t\tindices[insertPos] = candidate;\n\t\tcount++;\n\t}\n\n\t// Derive series value from bin key (0-indexed, matching ViewManager convention).\n\t// Vertex shader uses != comparison for series breaks, so any distinct values work.\n\tlet seriesVal = bitcast<u32>(f32(binKey / config.numBins));\n\n\t// Write unique points, fill remaining slots with last unique point.\n\t// Copied as bits, so a value arrives exactly as it left.\n\tfor (var i = 0u; i < 4u; i++) {\n\t\tlet srcIdx = indices[min(i, count - 1u)];\n\t\txOut[outBase + i] = xBits[srcIdx];\n\t\tyOut[outBase + i] = yBits[srcIdx];\n\t\tseriesOut[outBase + i] = seriesVal;\n\t\tcolorOut[outBase + i] = colorBits[srcIdx];\n\t}\n}\n";
|
|
86
92
|
/**
|
|
87
93
|
* Pass 4: Compact non-NaN output points and deduplicate.
|
|
88
94
|
* After the emit pass, empty bins contain NaN. This pass packs real data
|
|
@@ -100,7 +106,7 @@ declare const EMIT_OUTPUT_SHADER = "\n\nstruct M4Config {\n\tpointCount: u32,\n\
|
|
|
100
106
|
* Group 0: fillLayout (config + output arrays)
|
|
101
107
|
* Group 1: controlLayout (control)
|
|
102
108
|
*/
|
|
103
|
-
declare const COMPACT_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\n@group(0) @binding(0) var<uniform> config: M4Config;\n@group(0) @binding(1) var<storage, read_write> xOut: array<
|
|
109
|
+
declare const COMPACT_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\n\nconst NAN_BITS: u32 = 0x7FC00000u;\n\nfn isNanBits(u: u32) -> bool {\n\treturn (u & 0x7fffffffu) > 0x7f800000u;\n}\n\nfn isNaN(v: f32) -> bool {\n\treturn isNanBits(bitcast<u32>(v));\n}\n\n\n@group(0) @binding(0) var<uniform> config: M4Config;\n@group(0) @binding(1) var<storage, read_write> xOut: array<u32>;\n@group(0) @binding(2) var<storage, read_write> yOut: array<u32>;\n@group(0) @binding(3) var<storage, read_write> seriesOut: array<u32>;\n@group(0) @binding(4) var<storage, read_write> colorOut: array<u32>;\n@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;\n\n@compute @workgroup_size(1)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\tif (atomicLoad(&control[4]) == 0u) { return; }\n\n\tlet series = gid.x;\n\tif (series >= config.numSeries) { return; }\n\n\tlet start = series * 4u * config.numBins;\n\tlet end = start + 4u * config.numBins;\n\n\t// Pack non-NaN points to the front, skipping consecutive duplicates.\n\t// All on bits: NaN by its pattern, duplicates by identical bits. lastX\n\t// starts as NaN, which no kept x can equal.\n\tvar writeIdx = start;\n\tvar lastX = NAN_BITS;\n\tvar lastY = NAN_BITS;\n\n\tfor (var i = start; i < end; i++) {\n\t\tlet x = xOut[i];\n\t\tif (isNanBits(x)) { continue; }\n\t\tlet y = yOut[i];\n\t\tif (x == lastX && y == lastY) { continue; }\n\n\t\tif (writeIdx != i) {\n\t\t\txOut[writeIdx] = x;\n\t\t\tyOut[writeIdx] = y;\n\t\t\tseriesOut[writeIdx] = seriesOut[i];\n\t\t\tcolorOut[writeIdx] = colorOut[i];\n\t\t}\n\t\tlastX = x;\n\t\tlastY = y;\n\t\twriteIdx++;\n\t}\n\n\t// NaN-pad remaining slots\n\tfor (var i = writeIdx; i < end; i++) {\n\t\txOut[i] = NAN_BITS;\n\t\tyOut[i] = NAN_BITS;\n\t\tseriesOut[i] = NAN_BITS;\n\t\tcolorOut[i] = NAN_BITS;\n\t}\n\n\t// Instance count is set by pass 0 (outputSize) \u2014 compact only reorganizes\n\t// data within each series block, it doesn't change the total count.\n\t// The vertex shader discards NaN-padded entries at the end of each block.\n}\n";
|
|
104
110
|
/**
|
|
105
111
|
* Pass 5a: Passthrough copy — raw x,y → output + set indirect count.
|
|
106
112
|
*
|
|
@@ -113,7 +119,7 @@ declare const COMPACT_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBi
|
|
|
113
119
|
* Group 0: passthroughInputLayout (data0, data1, config, control)
|
|
114
120
|
* Group 1: halfOutputLayout (out0, out1)
|
|
115
121
|
*/
|
|
116
|
-
declare const PASSTHROUGH_XY_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\n@group(0) @binding(0) var<storage, read> xIn: array<
|
|
122
|
+
declare const PASSTHROUGH_XY_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\n// Copied as bits, so a null's NaN arrives exactly as it left.\n@group(0) @binding(0) var<storage, read> xIn: array<u32>;\n@group(0) @binding(1) var<storage, read> yIn: array<u32>;\n@group(0) @binding(2) var<uniform> config: M4Config;\n@group(0) @binding(3) var<storage, read_write> control: array<atomic<u32>>;\n@group(1) @binding(0) var<storage, read_write> xOut: array<u32>;\n@group(1) @binding(1) var<storage, read_write> yOut: array<u32>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\t// Only run in passthrough mode\n\tif (atomicLoad(&control[4]) == 1u) { return; }\n\n\tlet idx = gid.x;\n\n\t// Thread 0 sets instance count to raw point count\n\tif (idx == 0u) {\n\t\tatomicStore(&control[1], config.pointCount);\n\t}\n\n\tif (idx >= config.pointCount) { return; }\n\n\txOut[idx] = xIn[idx];\n\tyOut[idx] = yIn[idx];\n}\n";
|
|
117
123
|
/**
|
|
118
124
|
* Pass 5b: Passthrough copy — raw series,color → output.
|
|
119
125
|
*
|
|
@@ -125,5 +131,5 @@ declare const PASSTHROUGH_XY_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n
|
|
|
125
131
|
* Group 0: passthroughInputLayout (data0, data1, config, control)
|
|
126
132
|
* Group 1: halfOutputLayout (out0, out1)
|
|
127
133
|
*/
|
|
128
|
-
declare const PASSTHROUGH_SC_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\n@group(0) @binding(0) var<storage, read> seriesIn: array<
|
|
134
|
+
declare const PASSTHROUGH_SC_SHADER = "\nstruct M4Config {\n\tpointCount: u32,\n\tnumBins: u32,\n\tnumSeries: u32,\n\t_reserved: u32,\n\t_reserved0: u32,\n\t_reserved1: u32,\n\t_reserved2: u32,\n\tdecimationMode: u32,\n};\n\n// Copied as bits, like pass 5a.\n@group(0) @binding(0) var<storage, read> seriesIn: array<u32>;\n@group(0) @binding(1) var<storage, read> colorIn: array<u32>;\n@group(0) @binding(2) var<uniform> config: M4Config;\n@group(0) @binding(3) var<storage, read_write> control: array<atomic<u32>>;\n@group(1) @binding(0) var<storage, read_write> seriesOut: array<u32>;\n@group(1) @binding(1) var<storage, read_write> colorOut: array<u32>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n\t// Only run in passthrough mode\n\tif (atomicLoad(&control[4]) == 1u) { return; }\n\n\tlet idx = gid.x;\n\tif (idx >= config.pointCount) { return; }\n\n\tseriesOut[idx] = seriesIn[idx];\n\tcolorOut[idx] = colorIn[idx];\n}\n";
|
|
129
135
|
export { WORKGROUP_SIZE, X_SCALE_IMMEDIATE_SIZE, INIT_BINS_SHADER, BIN_POINTS_SHADER, FIND_MINMAX_SHADER, EMIT_OUTPUT_SHADER, COMPACT_SHADER, PASSTHROUGH_XY_SHADER, PASSTHROUGH_SC_SHADER };
|
|
@@ -27,7 +27,14 @@
|
|
|
27
27
|
*
|
|
28
28
|
* The x scale's params reach passes 1 and 2 as immediates, pushed before each
|
|
29
29
|
* dispatch, rather than as an index into the global scale table.
|
|
30
|
+
*
|
|
31
|
+
* Every pass reads and writes the data through u32 views. NaN (a null, or an
|
|
32
|
+
* empty bin) is found by its bit pattern and moved as bits, never compared as
|
|
33
|
+
* a float: Metal compilers may fold `x != x` to false (see NanShaderLib), and
|
|
34
|
+
* where they do, compaction keeps the empty bins and the line loses every
|
|
35
|
+
* segment that touches one.
|
|
30
36
|
*/
|
|
37
|
+
import { NAN_WGSL } from '../NanShaderLib';
|
|
31
38
|
const WORKGROUP_SIZE = 256;
|
|
32
39
|
/**
|
|
33
40
|
* Shared type definitions used across all kernels.
|
|
@@ -53,10 +60,11 @@ struct M4Bin {
|
|
|
53
60
|
maxYIdx: atomic<u32>,
|
|
54
61
|
};
|
|
55
62
|
|
|
56
|
-
fn
|
|
57
|
-
let bits = bitcast<u32>(f);
|
|
63
|
+
fn sortableFromBits(bits: u32) -> u32 {
|
|
58
64
|
return select(~bits, bits ^ 0x80000000u, (bits & 0x80000000u) == 0u);
|
|
59
65
|
}
|
|
66
|
+
|
|
67
|
+
${NAN_WGSL}
|
|
60
68
|
`;
|
|
61
69
|
/** Bytes of immediate data passes 1 and 2 read: one ScaleParam. */
|
|
62
70
|
const X_SCALE_IMMEDIATE_SIZE = 16;
|
|
@@ -135,9 +143,9 @@ requires immediate_address_space;
|
|
|
135
143
|
${SHARED_TYPES}
|
|
136
144
|
${X_SCALE_IMMEDIATE}
|
|
137
145
|
|
|
138
|
-
@group(0) @binding(0) var<storage, read>
|
|
139
|
-
@group(0) @binding(1) var<storage, read>
|
|
140
|
-
@group(0) @binding(2) var<storage, read>
|
|
146
|
+
@group(0) @binding(0) var<storage, read> xBits: array<u32>;
|
|
147
|
+
@group(0) @binding(1) var<storage, read> yBits: array<u32>;
|
|
148
|
+
@group(0) @binding(2) var<storage, read> seriesBits: array<u32>;
|
|
141
149
|
@group(0) @binding(4) var<uniform> config: M4Config;
|
|
142
150
|
@group(0) @binding(5) var<storage, read_write> bins: array<M4Bin>;
|
|
143
151
|
@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;
|
|
@@ -150,11 +158,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
150
158
|
let idx = gid.x;
|
|
151
159
|
if (idx >= config.pointCount) { return; }
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
// Skip NaN points, tested on the bits before either becomes a float
|
|
162
|
+
let xb = xBits[idx];
|
|
163
|
+
let yb = yBits[idx];
|
|
164
|
+
if (isNanBits(xb) || isNanBits(yb)) { return; }
|
|
165
|
+
let x = bitcast<f32>(xb);
|
|
158
166
|
|
|
159
167
|
let domainMin = xScale.p1;
|
|
160
168
|
let domainMax = xScale.p2;
|
|
@@ -162,8 +170,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
162
170
|
let t = (x - domainMin) / (domainMax - domainMin);
|
|
163
171
|
|
|
164
172
|
// Series: 0-indexed from ViewManager (-1 = null/unknown, 0+ = categories)
|
|
165
|
-
let
|
|
166
|
-
if (
|
|
173
|
+
let seriesB = seriesBits[idx];
|
|
174
|
+
if (isNanBits(seriesB)) { return; }
|
|
175
|
+
let seriesF = bitcast<f32>(seriesB);
|
|
176
|
+
if (seriesF < 0.0) { return; }
|
|
167
177
|
let series = u32(seriesF);
|
|
168
178
|
if (series >= config.numSeries) { return; }
|
|
169
179
|
|
|
@@ -183,7 +193,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
183
193
|
atomicMin(&bins[binKey].firstIdx, idx);
|
|
184
194
|
atomicMax(&bins[binKey].lastIdx, idx);
|
|
185
195
|
|
|
186
|
-
let ySortable =
|
|
196
|
+
let ySortable = sortableFromBits(yb);
|
|
187
197
|
atomicMin(&bins[binKey].minYSortable, ySortable);
|
|
188
198
|
atomicMax(&bins[binKey].maxYSortable, ySortable);
|
|
189
199
|
}
|
|
@@ -204,9 +214,9 @@ requires immediate_address_space;
|
|
|
204
214
|
${SHARED_TYPES}
|
|
205
215
|
${X_SCALE_IMMEDIATE}
|
|
206
216
|
|
|
207
|
-
@group(0) @binding(0) var<storage, read>
|
|
208
|
-
@group(0) @binding(1) var<storage, read>
|
|
209
|
-
@group(0) @binding(2) var<storage, read>
|
|
217
|
+
@group(0) @binding(0) var<storage, read> xBits: array<u32>;
|
|
218
|
+
@group(0) @binding(1) var<storage, read> yBits: array<u32>;
|
|
219
|
+
@group(0) @binding(2) var<storage, read> seriesBits: array<u32>;
|
|
210
220
|
@group(0) @binding(4) var<uniform> config: M4Config;
|
|
211
221
|
@group(0) @binding(5) var<storage, read_write> bins: array<M4Bin>;
|
|
212
222
|
@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;
|
|
@@ -218,10 +228,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
218
228
|
let idx = gid.x;
|
|
219
229
|
if (idx >= config.pointCount) { return; }
|
|
220
230
|
|
|
221
|
-
|
|
222
|
-
let
|
|
223
|
-
|
|
224
|
-
if (
|
|
231
|
+
// NaN skip on the bits (matches binPoints pass)
|
|
232
|
+
let xb = xBits[idx];
|
|
233
|
+
let yb = yBits[idx];
|
|
234
|
+
if (isNanBits(xb) || isNanBits(yb)) { return; }
|
|
235
|
+
let x = bitcast<f32>(xb);
|
|
225
236
|
|
|
226
237
|
let domainMin = xScale.p1;
|
|
227
238
|
let domainMax = xScale.p2;
|
|
@@ -229,8 +240,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
229
240
|
let t = (x - domainMin) / (domainMax - domainMin);
|
|
230
241
|
|
|
231
242
|
// Series: 0-indexed (matches binPoints pass)
|
|
232
|
-
let
|
|
233
|
-
if (
|
|
243
|
+
let seriesB = seriesBits[idx];
|
|
244
|
+
if (isNanBits(seriesB)) { return; }
|
|
245
|
+
let seriesF = bitcast<f32>(seriesB);
|
|
246
|
+
if (seriesF < 0.0) { return; }
|
|
234
247
|
let series = u32(seriesF);
|
|
235
248
|
if (series >= config.numSeries) { return; }
|
|
236
249
|
|
|
@@ -246,7 +259,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
246
259
|
}
|
|
247
260
|
let binKey = series * config.numBins + bin;
|
|
248
261
|
|
|
249
|
-
let ySortable =
|
|
262
|
+
let ySortable = sortableFromBits(yb);
|
|
250
263
|
|
|
251
264
|
if (ySortable == atomicLoad(&bins[binKey].minYSortable)) {
|
|
252
265
|
atomicMin(&bins[binKey].minYIdx, idx);
|
|
@@ -275,15 +288,15 @@ ${SHARED_TYPES}
|
|
|
275
288
|
|
|
276
289
|
// Note: seriesValues not bound here — series is derived from binKey / numBins.
|
|
277
290
|
// This keeps total storage buffers within WebGPU default limit.
|
|
278
|
-
@group(0) @binding(0) var<storage, read>
|
|
279
|
-
@group(0) @binding(1) var<storage, read>
|
|
280
|
-
@group(0) @binding(2) var<storage, read>
|
|
291
|
+
@group(0) @binding(0) var<storage, read> xBits: array<u32>;
|
|
292
|
+
@group(0) @binding(1) var<storage, read> yBits: array<u32>;
|
|
293
|
+
@group(0) @binding(2) var<storage, read> colorBits: array<u32>;
|
|
281
294
|
@group(0) @binding(3) var<uniform> config: M4Config;
|
|
282
295
|
@group(0) @binding(4) var<storage, read_write> bins: array<M4Bin>;
|
|
283
|
-
@group(1) @binding(0) var<storage, read_write> xOut: array<
|
|
284
|
-
@group(1) @binding(1) var<storage, read_write> yOut: array<
|
|
285
|
-
@group(1) @binding(2) var<storage, read_write> seriesOut: array<
|
|
286
|
-
@group(1) @binding(3) var<storage, read_write> colorOut: array<
|
|
296
|
+
@group(1) @binding(0) var<storage, read_write> xOut: array<u32>;
|
|
297
|
+
@group(1) @binding(1) var<storage, read_write> yOut: array<u32>;
|
|
298
|
+
@group(1) @binding(2) var<storage, read_write> seriesOut: array<u32>;
|
|
299
|
+
@group(1) @binding(3) var<storage, read_write> colorOut: array<u32>;
|
|
287
300
|
|
|
288
301
|
@compute @workgroup_size(${WORKGROUP_SIZE})
|
|
289
302
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
@@ -294,16 +307,13 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
294
307
|
let outBase = binKey * 4u;
|
|
295
308
|
let firstIdx = atomicLoad(&bins[binKey].firstIdx);
|
|
296
309
|
|
|
297
|
-
// Empty bin:
|
|
298
|
-
// Use var to avoid const-expression NaN (WGSL disallows NaN in const context)
|
|
310
|
+
// Empty bin: NaN in all 4 output slots, written as bits
|
|
299
311
|
if (firstIdx == 0xFFFFFFFFu) {
|
|
300
|
-
var nanBits = 0x7FC00000u;
|
|
301
|
-
let nan = bitcast<f32>(nanBits);
|
|
302
312
|
for (var i = 0u; i < 4u; i++) {
|
|
303
|
-
xOut[outBase + i] =
|
|
304
|
-
yOut[outBase + i] =
|
|
305
|
-
seriesOut[outBase + i] =
|
|
306
|
-
colorOut[outBase + i] =
|
|
313
|
+
xOut[outBase + i] = NAN_BITS;
|
|
314
|
+
yOut[outBase + i] = NAN_BITS;
|
|
315
|
+
seriesOut[outBase + i] = NAN_BITS;
|
|
316
|
+
colorOut[outBase + i] = NAN_BITS;
|
|
307
317
|
}
|
|
308
318
|
return;
|
|
309
319
|
}
|
|
@@ -350,15 +360,16 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
350
360
|
|
|
351
361
|
// Derive series value from bin key (0-indexed, matching ViewManager convention).
|
|
352
362
|
// Vertex shader uses != comparison for series breaks, so any distinct values work.
|
|
353
|
-
let seriesVal = f32(binKey / config.numBins);
|
|
363
|
+
let seriesVal = bitcast<u32>(f32(binKey / config.numBins));
|
|
354
364
|
|
|
355
|
-
// Write unique points, fill remaining slots with last unique point
|
|
365
|
+
// Write unique points, fill remaining slots with last unique point.
|
|
366
|
+
// Copied as bits, so a value arrives exactly as it left.
|
|
356
367
|
for (var i = 0u; i < 4u; i++) {
|
|
357
368
|
let srcIdx = indices[min(i, count - 1u)];
|
|
358
|
-
xOut[outBase + i] =
|
|
359
|
-
yOut[outBase + i] =
|
|
369
|
+
xOut[outBase + i] = xBits[srcIdx];
|
|
370
|
+
yOut[outBase + i] = yBits[srcIdx];
|
|
360
371
|
seriesOut[outBase + i] = seriesVal;
|
|
361
|
-
colorOut[outBase + i] =
|
|
372
|
+
colorOut[outBase + i] = colorBits[srcIdx];
|
|
362
373
|
}
|
|
363
374
|
}
|
|
364
375
|
`;
|
|
@@ -391,11 +402,13 @@ struct M4Config {
|
|
|
391
402
|
decimationMode: u32,
|
|
392
403
|
};
|
|
393
404
|
|
|
405
|
+
${NAN_WGSL}
|
|
406
|
+
|
|
394
407
|
@group(0) @binding(0) var<uniform> config: M4Config;
|
|
395
|
-
@group(0) @binding(1) var<storage, read_write> xOut: array<
|
|
396
|
-
@group(0) @binding(2) var<storage, read_write> yOut: array<
|
|
397
|
-
@group(0) @binding(3) var<storage, read_write> seriesOut: array<
|
|
398
|
-
@group(0) @binding(4) var<storage, read_write> colorOut: array<
|
|
408
|
+
@group(0) @binding(1) var<storage, read_write> xOut: array<u32>;
|
|
409
|
+
@group(0) @binding(2) var<storage, read_write> yOut: array<u32>;
|
|
410
|
+
@group(0) @binding(3) var<storage, read_write> seriesOut: array<u32>;
|
|
411
|
+
@group(0) @binding(4) var<storage, read_write> colorOut: array<u32>;
|
|
399
412
|
@group(1) @binding(0) var<storage, read_write> control: array<atomic<u32>>;
|
|
400
413
|
|
|
401
414
|
@compute @workgroup_size(1)
|
|
@@ -409,14 +422,15 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
409
422
|
let end = start + 4u * config.numBins;
|
|
410
423
|
|
|
411
424
|
// Pack non-NaN points to the front, skipping consecutive duplicates.
|
|
425
|
+
// All on bits: NaN by its pattern, duplicates by identical bits. lastX
|
|
426
|
+
// starts as NaN, which no kept x can equal.
|
|
412
427
|
var writeIdx = start;
|
|
413
|
-
var
|
|
414
|
-
var
|
|
415
|
-
var lastY: f32 = bitcast<f32>(nanBits);
|
|
428
|
+
var lastX = NAN_BITS;
|
|
429
|
+
var lastY = NAN_BITS;
|
|
416
430
|
|
|
417
431
|
for (var i = start; i < end; i++) {
|
|
418
432
|
let x = xOut[i];
|
|
419
|
-
if (x
|
|
433
|
+
if (isNanBits(x)) { continue; }
|
|
420
434
|
let y = yOut[i];
|
|
421
435
|
if (x == lastX && y == lastY) { continue; }
|
|
422
436
|
|
|
@@ -432,12 +446,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
432
446
|
}
|
|
433
447
|
|
|
434
448
|
// NaN-pad remaining slots
|
|
435
|
-
let nan = bitcast<f32>(nanBits);
|
|
436
449
|
for (var i = writeIdx; i < end; i++) {
|
|
437
|
-
xOut[i] =
|
|
438
|
-
yOut[i] =
|
|
439
|
-
seriesOut[i] =
|
|
440
|
-
colorOut[i] =
|
|
450
|
+
xOut[i] = NAN_BITS;
|
|
451
|
+
yOut[i] = NAN_BITS;
|
|
452
|
+
seriesOut[i] = NAN_BITS;
|
|
453
|
+
colorOut[i] = NAN_BITS;
|
|
441
454
|
}
|
|
442
455
|
|
|
443
456
|
// Instance count is set by pass 0 (outputSize) — compact only reorganizes
|
|
@@ -469,12 +482,13 @@ struct M4Config {
|
|
|
469
482
|
decimationMode: u32,
|
|
470
483
|
};
|
|
471
484
|
|
|
472
|
-
|
|
473
|
-
@group(0) @binding(
|
|
485
|
+
// Copied as bits, so a null's NaN arrives exactly as it left.
|
|
486
|
+
@group(0) @binding(0) var<storage, read> xIn: array<u32>;
|
|
487
|
+
@group(0) @binding(1) var<storage, read> yIn: array<u32>;
|
|
474
488
|
@group(0) @binding(2) var<uniform> config: M4Config;
|
|
475
489
|
@group(0) @binding(3) var<storage, read_write> control: array<atomic<u32>>;
|
|
476
|
-
@group(1) @binding(0) var<storage, read_write> xOut: array<
|
|
477
|
-
@group(1) @binding(1) var<storage, read_write> yOut: array<
|
|
490
|
+
@group(1) @binding(0) var<storage, read_write> xOut: array<u32>;
|
|
491
|
+
@group(1) @binding(1) var<storage, read_write> yOut: array<u32>;
|
|
478
492
|
|
|
479
493
|
@compute @workgroup_size(${WORKGROUP_SIZE})
|
|
480
494
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
@@ -517,12 +531,13 @@ struct M4Config {
|
|
|
517
531
|
decimationMode: u32,
|
|
518
532
|
};
|
|
519
533
|
|
|
520
|
-
|
|
521
|
-
@group(0) @binding(
|
|
534
|
+
// Copied as bits, like pass 5a.
|
|
535
|
+
@group(0) @binding(0) var<storage, read> seriesIn: array<u32>;
|
|
536
|
+
@group(0) @binding(1) var<storage, read> colorIn: array<u32>;
|
|
522
537
|
@group(0) @binding(2) var<uniform> config: M4Config;
|
|
523
538
|
@group(0) @binding(3) var<storage, read_write> control: array<atomic<u32>>;
|
|
524
|
-
@group(1) @binding(0) var<storage, read_write> seriesOut: array<
|
|
525
|
-
@group(1) @binding(1) var<storage, read_write> colorOut: array<
|
|
539
|
+
@group(1) @binding(0) var<storage, read_write> seriesOut: array<u32>;
|
|
540
|
+
@group(1) @binding(1) var<storage, read_write> colorOut: array<u32>;
|
|
526
541
|
|
|
527
542
|
@compute @workgroup_size(${WORKGROUP_SIZE})
|
|
528
543
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|