@jbrowse/render-core 5.0.0-beta.7 → 5.0.0-beta.9

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 (174) hide show
  1. package/esm/RenderCanvas.d.ts +2 -2
  2. package/esm/RenderLifecycleMixin.d.ts +7 -6
  3. package/esm/RenderLifecycleMixin.js +7 -6
  4. package/esm/canvas2dUtils.d.ts +58 -17
  5. package/esm/canvas2dUtils.js +82 -30
  6. package/esm/computePipeline.d.ts +4 -3
  7. package/esm/computePipeline.js +21 -14
  8. package/esm/coverageBand.d.ts +3 -3
  9. package/esm/coverageBand.js +2 -2
  10. package/esm/coverageBandBuffers.d.ts +2 -3
  11. package/esm/createRenderingBackend.d.ts +1 -1
  12. package/esm/displayPhase.d.ts +79 -109
  13. package/esm/displayPhase.js +42 -83
  14. package/esm/gpuDevice.d.ts +11 -10
  15. package/esm/gpuDevice.js +10 -11
  16. package/esm/graphicsCapabilities.d.ts +6 -7
  17. package/esm/graphicsCapabilities.js +3 -4
  18. package/esm/hal/createHal.js +1 -1
  19. package/esm/hal/deviceGpuCache.d.ts +25 -30
  20. package/esm/hal/deviceGpuCache.js +85 -97
  21. package/esm/hal/index.d.ts +2 -2
  22. package/esm/hal/index.js +1 -1
  23. package/esm/hal/mockHal.d.ts +8 -6
  24. package/esm/hal/mockHal.js +7 -2
  25. package/esm/hal/passIds.d.ts +8 -7
  26. package/esm/hal/passIds.js +11 -10
  27. package/esm/hal/types.d.ts +28 -12
  28. package/esm/hal/webgl2Hal.d.ts +11 -3
  29. package/esm/hal/webgl2Hal.js +116 -145
  30. package/esm/hal/webgpuHal.d.ts +8 -8
  31. package/esm/hal/webgpuHal.js +97 -124
  32. package/esm/instancePass.d.ts +2 -1
  33. package/esm/mapUploadSync.d.ts +2 -2
  34. package/esm/mapUploadSync.js +2 -2
  35. package/esm/marks/barMark.d.ts +6 -3
  36. package/esm/marks/barMark.js +19 -13
  37. package/esm/marks/drawAgainstHit.d.ts +58 -39
  38. package/esm/marks/drawAgainstHit.js +136 -96
  39. package/esm/marks/glyphPaint.d.ts +14 -2
  40. package/esm/marks/glyphPaint.js +16 -2
  41. package/esm/marks/index.d.ts +4 -2
  42. package/esm/marks/index.js +3 -2
  43. package/esm/marks/markBackend.d.ts +2 -2
  44. package/esm/marks/markBackend.js +2 -2
  45. package/esm/marks/markHit.d.ts +2 -2
  46. package/esm/marks/markHit.js +2 -2
  47. package/esm/marks/markPlan.d.ts +2 -2
  48. package/esm/marks/markPlan.js +2 -2
  49. package/esm/marks/markRamp.d.ts +1 -3
  50. package/esm/marks/markRamp.js +1 -5
  51. package/esm/marks/nearestMarkHit.d.ts +49 -0
  52. package/esm/marks/nearestMarkHit.js +126 -0
  53. package/esm/marks/pointMark.d.ts +9 -2
  54. package/esm/marks/pointMark.js +36 -31
  55. package/esm/marks/rowLane.d.ts +17 -0
  56. package/esm/marks/rowLane.js +11 -0
  57. package/esm/marks/spanMark.d.ts +6 -26
  58. package/esm/marks/spanMark.js +34 -31
  59. package/esm/marks/types.d.ts +51 -181
  60. package/esm/marks/types.js +91 -131
  61. package/esm/namedReactions.d.ts +3 -4
  62. package/esm/namedReactions.js +3 -4
  63. package/esm/scoreScale.d.ts +20 -0
  64. package/esm/scoreScale.js +77 -0
  65. package/esm/shaders/barMark.generated.d.ts +2 -3
  66. package/esm/shaders/barMark.generated.js +4 -3
  67. package/esm/shaders/barMark.glsl.generated.d.ts +2 -0
  68. package/esm/shaders/barMark.glsl.generated.js +4 -0
  69. package/esm/shaders/barMark.iface.generated.d.ts +12 -6
  70. package/esm/shaders/barMark.iface.generated.js +27 -15
  71. package/esm/shaders/barMark.wgsl.generated.d.ts +1 -0
  72. package/esm/shaders/barMark.wgsl.generated.js +3 -0
  73. package/esm/shaders/coverageBar.generated.d.ts +2 -3
  74. package/esm/shaders/coverageBar.generated.js +4 -3
  75. package/esm/shaders/coverageBar.glsl.generated.d.ts +2 -0
  76. package/esm/shaders/coverageBar.glsl.generated.js +4 -0
  77. package/esm/shaders/coverageBar.iface.generated.js +1 -1
  78. package/esm/shaders/coverageBar.wgsl.generated.d.ts +1 -0
  79. package/esm/shaders/coverageBar.wgsl.generated.js +3 -0
  80. package/esm/shaders/coverageIndicator.generated.d.ts +2 -3
  81. package/esm/shaders/coverageIndicator.generated.js +4 -3
  82. package/esm/shaders/coverageIndicator.glsl.generated.d.ts +2 -0
  83. package/esm/shaders/coverageIndicator.glsl.generated.js +4 -0
  84. package/esm/shaders/coverageIndicator.iface.generated.js +1 -1
  85. package/esm/shaders/coverageIndicator.wgsl.generated.d.ts +1 -0
  86. package/esm/shaders/coverageIndicator.wgsl.generated.js +3 -0
  87. package/esm/shaders/coverageInterbase.generated.d.ts +2 -3
  88. package/esm/shaders/coverageInterbase.generated.js +4 -3
  89. package/esm/shaders/coverageInterbase.glsl.generated.d.ts +2 -0
  90. package/esm/shaders/coverageInterbase.glsl.generated.js +4 -0
  91. package/esm/shaders/coverageInterbase.iface.generated.js +1 -1
  92. package/esm/shaders/coverageInterbase.wgsl.generated.d.ts +1 -0
  93. package/esm/shaders/coverageInterbase.wgsl.generated.js +3 -0
  94. package/esm/shaders/coverageMod.generated.d.ts +2 -3
  95. package/esm/shaders/coverageMod.generated.js +4 -3
  96. package/esm/shaders/coverageMod.glsl.generated.d.ts +2 -0
  97. package/esm/shaders/coverageMod.glsl.generated.js +4 -0
  98. package/esm/shaders/coverageMod.iface.generated.js +1 -1
  99. package/esm/shaders/coverageMod.wgsl.generated.d.ts +1 -0
  100. package/esm/shaders/coverageMod.wgsl.generated.js +3 -0
  101. package/esm/shaders/coverageSnp.generated.d.ts +2 -3
  102. package/esm/shaders/coverageSnp.generated.js +4 -3
  103. package/esm/shaders/coverageSnp.glsl.generated.d.ts +2 -0
  104. package/esm/shaders/coverageSnp.glsl.generated.js +4 -0
  105. package/esm/shaders/coverageSnp.iface.generated.js +1 -1
  106. package/esm/shaders/coverageSnp.wgsl.generated.d.ts +1 -0
  107. package/esm/shaders/coverageSnp.wgsl.generated.js +3 -0
  108. package/esm/shaders/pointMark.generated.d.ts +2 -3
  109. package/esm/shaders/pointMark.generated.js +4 -3
  110. package/esm/shaders/pointMark.glsl.generated.d.ts +2 -0
  111. package/esm/shaders/pointMark.glsl.generated.js +4 -0
  112. package/esm/shaders/pointMark.iface.generated.d.ts +11 -3
  113. package/esm/shaders/pointMark.iface.generated.js +23 -9
  114. package/esm/shaders/pointMark.js.generated.d.ts +1 -1
  115. package/esm/shaders/pointMark.js.generated.js +51 -29
  116. package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -0
  117. package/esm/shaders/pointMark.wgsl.generated.js +3 -0
  118. package/esm/shaders/scoreScale.js.generated.js +46 -22
  119. package/esm/shaders/spanMark.generated.d.ts +2 -3
  120. package/esm/shaders/spanMark.generated.js +4 -3
  121. package/esm/shaders/spanMark.glsl.generated.d.ts +2 -0
  122. package/esm/shaders/spanMark.glsl.generated.js +4 -0
  123. package/esm/shaders/spanMark.iface.generated.js +1 -1
  124. package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -0
  125. package/esm/shaders/spanMark.wgsl.generated.js +3 -0
  126. package/esm/slangPass.d.ts +9 -10
  127. package/esm/slangPass.js +9 -3
  128. package/esm/useGraphicsCapabilities.d.ts +1 -1
  129. package/esm/useGraphicsCapabilities.js +1 -1
  130. package/esm/webgpuUtils.d.ts +3 -11
  131. package/esm/webgpuUtils.js +12 -33
  132. package/package.json +3 -2
  133. package/src/shaders/barMark.generated.ts +6 -5
  134. package/src/shaders/barMark.glsl.generated.ts +6 -0
  135. package/src/shaders/barMark.iface.generated.ts +31 -15
  136. package/src/shaders/barMark.slang +11 -3
  137. package/src/shaders/barMark.wgsl.generated.ts +4 -0
  138. package/src/shaders/colorPack.slang +3 -2
  139. package/src/shaders/coverageBand.slang +5 -11
  140. package/src/shaders/coverageBar.generated.ts +6 -5
  141. package/src/shaders/coverageBar.glsl.generated.ts +6 -0
  142. package/src/shaders/coverageBar.iface.generated.ts +1 -1
  143. package/src/shaders/coverageBar.slang +1 -1
  144. package/src/shaders/coverageBar.wgsl.generated.ts +4 -0
  145. package/src/shaders/coverageIndicator.generated.ts +6 -5
  146. package/src/shaders/coverageIndicator.glsl.generated.ts +6 -0
  147. package/src/shaders/coverageIndicator.iface.generated.ts +1 -1
  148. package/src/shaders/coverageIndicator.wgsl.generated.ts +4 -0
  149. package/src/shaders/coverageInterbase.generated.ts +6 -5
  150. package/src/shaders/coverageInterbase.glsl.generated.ts +6 -0
  151. package/src/shaders/coverageInterbase.iface.generated.ts +1 -1
  152. package/src/shaders/coverageInterbase.wgsl.generated.ts +4 -0
  153. package/src/shaders/coverageMod.generated.ts +6 -5
  154. package/src/shaders/coverageMod.glsl.generated.ts +6 -0
  155. package/src/shaders/coverageMod.iface.generated.ts +1 -1
  156. package/src/shaders/coverageMod.wgsl.generated.ts +4 -0
  157. package/src/shaders/coverageSnp.generated.ts +6 -5
  158. package/src/shaders/coverageSnp.glsl.generated.ts +6 -0
  159. package/src/shaders/coverageSnp.iface.generated.ts +1 -1
  160. package/src/shaders/coverageSnp.wgsl.generated.ts +4 -0
  161. package/src/shaders/hpmath.slang +18 -6
  162. package/src/shaders/pointMark.generated.ts +6 -6
  163. package/src/shaders/pointMark.glsl.generated.ts +6 -0
  164. package/src/shaders/pointMark.iface.generated.ts +28 -9
  165. package/src/shaders/pointMark.js.generated.ts +48 -29
  166. package/src/shaders/pointMark.slang +18 -5
  167. package/src/shaders/pointMark.wgsl.generated.ts +4 -0
  168. package/src/shaders/scoreScale.js.generated.ts +43 -22
  169. package/src/shaders/scoreScale.slang +37 -53
  170. package/src/shaders/spanMark.generated.ts +6 -5
  171. package/src/shaders/spanMark.glsl.generated.ts +6 -0
  172. package/src/shaders/spanMark.iface.generated.ts +1 -1
  173. package/src/shaders/spanMark.wgsl.generated.ts +4 -0
  174. package/src/shaders/valueScale.slang +8 -45
@@ -3,8 +3,8 @@ import { syncCanvasSize } from '../canvas2dUtils.js';
3
3
  import { canvasConfiguredBy, canvasContextError, noteCanvasConfigured, noteCanvasContext, } from '../canvasContext.js';
4
4
  import { createGpuSurfaceLostError } from '../gpuContextLostError.js';
5
5
  import { getGpuDevice } from '../gpuDevice.js';
6
- import { STANDARD_BLEND_STATE, createUniformOnlyBindGroup, createVertexBuffer, toGpuVertexFormat, } from '../webgpuUtils.js';
7
- import { getDeviceLayouts, getOrBuildPipeline } from './deviceGpuCache.js';
6
+ import { createVertexBuffer } from '../webgpuUtils.js';
7
+ import { bindGroupLayoutEntries, getOrBuildPipeline, getPassLayout, pipelineRecipe, } from './deviceGpuCache.js';
8
8
  import { GpuHalBase } from './gpuHalBase.js';
9
9
  class ShaderCompileError extends Error {
10
10
  constructor(passId, details) {
@@ -13,22 +13,16 @@ class ShaderCompileError extends Error {
13
13
  }
14
14
  }
15
15
  // Maximum number of writeUniforms() calls per frame. Each call occupies one
16
- // aligned slot in the uniform ring buffer.
17
- //
18
- // The ring is allocated eagerly at this many slots, as a GPU buffer AND a CPU
19
- // staging array, whether a renderer writes 4 slots or 1900 — so the cost is per
20
- // display, and both terms are in it. A display whose uniform fits the 256-byte
21
- // minimum alignment pays 512 KB each side; alignments' 640-byte uniform aligns
22
- // to 768, so 1.5 MiB each side, per track. A frame is measured using 2-4 of
23
- // those slots, which is what makes the count above the oversized term rather
24
- // than any struct:
25
- // agent-docs/ideas/size-the-uniform-ring-to-its-measured-occupancy.md.
16
+ // aligned slot in the uniform ring, allocated eagerly per display as a GPU
17
+ // buffer and a CPU staging array. A frame is measured using 2-4 slots, so the
18
+ // count rather than any struct is the oversized term:
19
+ // agent-docs/reference/ARCHITECTURAL_LIMITS.md §"The uniform ring is allocated
20
+ // at 2048 slots".
26
21
  //
27
22
  // Exhausting it does not throw: the write is dropped and its draws render
28
- // against the previous batch's uniforms, which is wrong data rather than stale
29
- // data. If we ever hit the cap, switch to a dynamic-growth buffer (recreate
30
- // buffer + every region's bind group) rather than just bumping the constant
31
- // again.
23
+ // against another batch's uniforms. If we ever hit the cap, grow the buffer and
24
+ // recreate every bind group in `passBindGroups` rather than bumping the
25
+ // constant again.
32
26
  const MAX_UNIFORM_SLOTS = 2048;
33
27
  // Warn while there is still headroom, because the cap itself is not a place to
34
28
  // find out. A renderer's per-frame write count is rarely one number: alignments
@@ -38,58 +32,22 @@ const MAX_UNIFORM_SLOTS = 2048;
38
32
  // the "~50 writes/frame" this file used to claim, and nothing reported the
39
33
  // difference between 50 and 1900.
40
34
  const UNIFORM_SLOT_WARN_AT = MAX_UNIFORM_SLOTS / 2;
41
- function gpuBlendState(bs) {
42
- // Max ignores its factors, but WebGPU still validates them and rejects
43
- // anything but 'one' on both channels ("Destination blend factor ... is
44
- // defined and not BlendFactor::One when blend operation is
45
- // BlendOperation::Max"). Same blend as webgl2Hal's bare glBlendEquation(MAX).
46
- const max = { srcFactor: 'one', dstFactor: 'one', operation: 'max' };
47
- // Otherwise RGB uses the caller-supplied factors and alpha always accumulates
48
- // through ONE / ONE_MINUS_SRC_ALPHA (matches webgl2Hal.applyBlendState).
49
- return bs.op === 'max'
50
- ? { color: max, alpha: max }
51
- : {
52
- color: { srcFactor: bs.srcFactor, dstFactor: bs.dstFactor },
53
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
54
- };
55
- }
56
- // Every pass reads per-instance data from a vertex buffer (no storage binding
57
- // at 0) because Slang-generated shaders cross-compile to GLSL ES, which has no
58
- // SSBOs. The two @group(0) layouts a pass is built against — uniform-only, and
59
- // uniform + texture + sampler — belong to the device, not to this HAL, and live
60
- // in `deviceGpuCache.ts` with the pipelines.
61
- async function buildPipeline(device, desc, layouts, sampleCount) {
62
- const module = device.createShaderModule({ code: desc.wgslSource });
35
+ // Every pass reads per-instance data from a vertex buffer, because Slang's
36
+ // GLSL ES target has no SSBOs.
37
+ async function buildPipeline(device, recipe, passId) {
38
+ const module = device.createShaderModule({ code: recipe.wgslSource });
63
39
  const info = await module.getCompilationInfo();
64
40
  const errors = info.messages.filter(m => m.type === 'error');
65
41
  if (errors.length > 0) {
66
42
  const details = errors
67
43
  .map(m => `line ${m.lineNum}: ${m.message}`)
68
44
  .join('; ');
69
- throw new ShaderCompileError(desc.id, details);
70
- }
71
- const blend = desc.blend
72
- ? desc.blendState
73
- ? gpuBlendState(desc.blendState)
74
- : STANDARD_BLEND_STATE
75
- : undefined;
76
- // Every pass feeds @location(N) inputs from a bound vertex buffer.
77
- const vertexBuffers = [
78
- {
79
- arrayStride: desc.instanceStride,
80
- stepMode: 'instance',
81
- attributes: desc.vertexAttributes.map((attr, i) => ({
82
- shaderLocation: i,
83
- offset: attr.offsetBytes,
84
- format: toGpuVertexFormat(attr),
85
- })),
86
- },
87
- ];
45
+ throw new ShaderCompileError(passId, details);
46
+ }
47
+ const { blend } = recipe;
88
48
  return device.createRenderPipelineAsync({
89
- layout: desc.textures?.length
90
- ? layouts.texturedPipelineLayout
91
- : layouts.uniformOnlyPipelineLayout,
92
- vertex: { module, entryPoint: 'vs_main', buffers: vertexBuffers },
49
+ layout: getPassLayout(device, recipe.bindGroupLayout).pipelineLayout,
50
+ vertex: { module, entryPoint: 'vs_main', buffers: [recipe.vertexBuffer] },
93
51
  fragment: {
94
52
  module,
95
53
  entryPoint: 'fs_main',
@@ -100,17 +58,18 @@ async function buildPipeline(device, desc, layouts, sampleCount) {
100
58
  },
101
59
  ],
102
60
  },
103
- primitive: { topology: desc.topology ?? 'triangle-list' },
104
- multisample: sampleCount === 1 ? undefined : { count: sampleCount },
61
+ primitive: { topology: recipe.topology },
62
+ multisample: recipe.sampleCount === 1 ? undefined : { count: recipe.sampleCount },
105
63
  });
106
64
  }
107
- // Resolve every declared pass to its pipeline, taking the device-wide cache's
108
- // answer wherever another display of this type already built one. Still every
109
- // pass up front rather than on first draw — the WebGL2 side compiles lazily
110
- // (`getPass`) and this side does not; see ARCHITECTURAL_LIMITS.md §"Every
111
- // WebGPU display resolves its whole pass list before it can paint".
65
+ // Every declared pass up front, unlike WebGL2's first-draw link; see
66
+ // ARCHITECTURAL_LIMITS.md §"Every WebGPU display resolves its whole pass list
67
+ // before it can paint". Each pass compiles as soon as its own WGSL arrives.
112
68
  async function resolvePipelines(device, descriptors, sampleCount) {
113
- const built = await Promise.all(descriptors.map(desc => getOrBuildPipeline(device, desc, sampleCount, (layouts, count) => buildPipeline(device, desc, layouts, count))));
69
+ const built = await Promise.all(descriptors.map(async (desc) => {
70
+ const { WGSL_SOURCE } = await desc.source.wgsl();
71
+ return getOrBuildPipeline(device, pipelineRecipe(desc, WGSL_SOURCE, sampleCount), recipe => buildPipeline(device, recipe, desc.id));
72
+ }));
114
73
  return new Map(descriptors.map((desc, i) => [desc.id, built[i]]));
115
74
  }
116
75
  // Behavioral parity with WebGL2Hal is enforced by tests, not by this file:
@@ -128,14 +87,13 @@ export class WebGPUHal extends GpuHalBase {
128
87
  context;
129
88
  pipelines;
130
89
  passTextures = new Map();
131
- // One bind group per textured pass, built lazily by `getBindGroup` and
132
- // dropped when `uploadTexture` swaps that pass's texture. Uniform-only passes
133
- // are not stored here — they all share `uniformOnlyBindGroup`.
90
+ // Every pass's bind group, by the pass drawn. A pass that binds no texture
91
+ // has one from construction, shared with each pass over the same layout; a
92
+ // textured pass gets its own when its texture arrives, dropped when
93
+ // `uploadTexture` replaces the texture. The layouts are the device's, shared
94
+ // with every display on it; the groups are this HAL's, since they name its
95
+ // uniform ring buffer.
134
96
  passBindGroups = new Map();
135
- // The device's layouts, not this HAL's — shared with every other display on
136
- // the same device. The bind GROUPS above stay per-HAL: they reference this
137
- // HAL's uniform ring buffer.
138
- layouts;
139
97
  // Uniform ring buffer: holds up to MAX_UNIFORM_SLOTS sets of uniforms so
140
98
  // that all draw calls in a frame can reference different uniform data via
141
99
  // dynamic offsets, enabling a single command encoder + submit per frame.
@@ -147,10 +105,6 @@ export class WebGPUHal extends GpuHalBase {
147
105
  // console the developer stops reading, and the fact is about the renderer
148
106
  // rather than about this frame.
149
107
  warnedUniformSlots = false;
150
- // Shared bind group for every uniform-only pass — only references
151
- // `uniformRingBuffer` (via dynamic offset at draw time), so one instance
152
- // serves all passes/regions instead of allocating a fresh one per upload.
153
- uniformOnlyBindGroup;
154
108
  // Samples per pixel this display renders at, stated by whoever built it (see
155
109
  // `RenderingBackendOptions.sampleCount`). Every render-pass, texture and
156
110
  // pipeline decision below reads it, so the two shapes cannot mismatch — and
@@ -178,7 +132,7 @@ export class WebGPUHal extends GpuHalBase {
178
132
  * selection spanning two sections re-uploads `OVERLAY_REGION` once per
179
133
  * section, inside the block loop.
180
134
  *
181
- * Deferring is what makes mid-frame replacement legal instead of merely
135
+ * Deferring the destroy makes mid-frame replacement legal instead of merely
182
136
  * warned about; synteny's `ensureUploaded` has always deleted a pass mid-frame
183
137
  * that the open pass never referenced, and that case was never the bug.
184
138
  */
@@ -200,13 +154,12 @@ export class WebGPUHal extends GpuHalBase {
200
154
  warnedSwapChainLoss = false;
201
155
  swapChainUnrecoverable = false;
202
156
  reportedSurfaceFailure = false;
203
- constructor(device, canvas, context, descriptors, sampleCount, pipelines, layouts) {
157
+ constructor(device, canvas, context, descriptors, sampleCount, pipelines) {
204
158
  super(descriptors, 'WebGPUHal');
205
159
  this.device = device;
206
160
  this.canvas = canvas;
207
161
  this.context = context;
208
162
  this.pipelines = pipelines;
209
- this.layouts = layouts;
210
163
  this.sampleCount = sampleCount;
211
164
  // Align uniform slots to device requirements for dynamic offsets
212
165
  const alignment = device.limits.minUniformBufferOffsetAlignment;
@@ -218,9 +171,51 @@ export class WebGPUHal extends GpuHalBase {
218
171
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
219
172
  });
220
173
  this.uniformStaging = new Uint8Array(ringSize);
221
- this.uniformOnlyBindGroup = createUniformOnlyBindGroup(device, layouts.uniformOnlyBindGroupLayout, this.uniformRingBuffer, this.alignedUniformSize);
174
+ const byLayout = new Map();
175
+ for (const desc of descriptors) {
176
+ if (!desc.bindings.some(b => b.kind === 'texture')) {
177
+ const layout = this.bindGroupLayoutOf(desc.bindings);
178
+ let group = byLayout.get(layout);
179
+ if (!group) {
180
+ group = this.createBindGroup(desc.bindings, layout);
181
+ byLayout.set(layout, group);
182
+ }
183
+ this.passBindGroups.set(desc.id, group);
184
+ }
185
+ }
222
186
  this.configureContext();
223
187
  }
188
+ bindGroupLayoutOf(bindings) {
189
+ return getPassLayout(this.device, bindGroupLayoutEntries(bindings))
190
+ .bindGroupLayout;
191
+ }
192
+ // One entry per binding in the pass's table, the same table its layout was
193
+ // built from, so the group cannot name a binding the layout lacks.
194
+ createBindGroup(bindings, layout, texture) {
195
+ return this.device.createBindGroup({
196
+ layout,
197
+ entries: bindings.map(b => ({
198
+ binding: b.index,
199
+ resource: this.bindingResource(b, texture),
200
+ })),
201
+ });
202
+ }
203
+ bindingResource(b, texture) {
204
+ if (b.kind === 'uniform') {
205
+ return {
206
+ buffer: this.uniformRingBuffer,
207
+ offset: 0,
208
+ size: this.alignedUniformSize,
209
+ };
210
+ }
211
+ if (texture && b.kind === 'texture') {
212
+ return texture.texture.createView();
213
+ }
214
+ if (texture && b.kind === 'sampler') {
215
+ return texture.sampler;
216
+ }
217
+ throw new Error(`[WebGPUHal] nothing to bind for the ${b.kind} '${b.name}' at binding ${b.index}`);
218
+ }
224
219
  limits() {
225
220
  const { maxBufferSize, maxTextureDimension2D } = this.device.limits;
226
221
  return {
@@ -256,11 +251,10 @@ export class WebGPUHal extends GpuHalBase {
256
251
  return null;
257
252
  }
258
253
  // Resolve pipelines BEFORE acquiring the canvas's webgpu context. A canvas's
259
- // context type is permanent once acquired, so if shader compilation throws
260
- // here the canvas stays pristine and createGpuHal's WebGL2 fallback can
261
- // still claim it — otherwise a partial WebGPU init would drop us all the way
262
- // to Canvas2D on a WebGL2-capable machine.
263
- const layouts = getDeviceLayouts(device);
254
+ // context type is permanent once acquired, so if a shader fails to load or
255
+ // compile here the canvas stays pristine and createGpuHal's WebGL2 fallback
256
+ // can still claim it — otherwise a partial WebGPU init would drop us all the
257
+ // way to Canvas2D on a WebGL2-capable machine.
264
258
  const pipelines = await resolvePipelines(device, descriptors, sampleCount);
265
259
  const context = canvas.getContext('webgpu');
266
260
  if (!context) {
@@ -271,7 +265,7 @@ export class WebGPUHal extends GpuHalBase {
271
265
  throw canvasContextError(canvas, 'webgpu');
272
266
  }
273
267
  noteCanvasContext(canvas, 'webgpu');
274
- return new WebGPUHal(device, canvas, context, descriptors, sampleCount, pipelines, layouts);
268
+ return new WebGPUHal(device, canvas, context, descriptors, sampleCount, pipelines);
275
269
  }
276
270
  resize(width, height) {
277
271
  const { changed, scale } = syncCanvasSize(this.canvas, width, height);
@@ -314,43 +308,22 @@ export class WebGPUHal extends GpuHalBase {
314
308
  * (and a dropped draw) the moment one side samples a texture.
315
309
  *
316
310
  * Nothing in a bind group varies per region either: it references the
317
- * HAL-wide uniform ring buffer plus the pass's own texture/sampler. So
318
- * uniform-only passes all share `uniformOnlyBindGroup`, and a textured pass
319
- * gets exactly one, cached until `uploadTexture` replaces its texture.
320
- * Returns undefined when a pass needs a texture that hasn't arrived yet;
321
- * drawPass skips those.
311
+ * HAL-wide uniform ring buffer plus the pass's own texture/sampler. Returns
312
+ * undefined when a pass needs a texture that hasn't arrived yet; drawPass
313
+ * skips those.
322
314
  */
323
315
  getBindGroup(passId) {
324
- const tb = this.descriptors.get(passId)?.textures?.[0];
325
- if (tb) {
326
- let bindGroup = this.passBindGroups.get(passId);
327
- if (!bindGroup) {
328
- const texState = this.passTextures.get(passId);
329
- if (texState) {
330
- bindGroup = this.device.createBindGroup({
331
- layout: this.layouts.texturedBindGroupLayout,
332
- entries: [
333
- {
334
- binding: 1,
335
- resource: {
336
- buffer: this.uniformRingBuffer,
337
- offset: 0,
338
- size: this.alignedUniformSize,
339
- },
340
- },
341
- {
342
- binding: tb.textureBinding,
343
- resource: texState.texture.createView(),
344
- },
345
- { binding: tb.samplerBinding, resource: texState.sampler },
346
- ],
347
- });
348
- this.passBindGroups.set(passId, bindGroup);
349
- }
350
- }
351
- return bindGroup;
316
+ return this.passBindGroups.get(passId) ?? this.bindTexture(passId);
317
+ }
318
+ bindTexture(passId) {
319
+ const texture = this.passTextures.get(passId);
320
+ const desc = this.descriptors.get(passId);
321
+ if (!texture || !desc) {
322
+ return undefined;
352
323
  }
353
- return this.uniformOnlyBindGroup;
324
+ const group = this.createBindGroup(desc.bindings, this.bindGroupLayoutOf(desc.bindings), texture);
325
+ this.passBindGroups.set(passId, group);
326
+ return group;
354
327
  }
355
328
  /**
356
329
  * The one place a GPU resource this HAL owns is released — see
@@ -5,7 +5,8 @@ import type { GpuHal, PipelineDescriptor } from './hal';
5
5
  * A pass id, the packer that fills its buffer, and the upload that joins them
6
6
  * are three statements of one fact, and renderers kept them in three places —
7
7
  * so a pass could be registered and drawn while nothing uploaded to it, which
8
- * paints nothing, silently, on the GPU backend only. Here they are one object:
8
+ * paints nothing, on the GPU backend only. `InstancePass`
9
+ * makes them one object:
9
10
  *
10
11
  * ```ts
11
12
  * export const GAP_PASS = {
@@ -9,8 +9,8 @@ export interface MapUploadTarget<K, T, B> {
9
9
  * re-uploads), and re-uploads everything when the backend identity changes —
10
10
  * a context-loss recovery hands a fresh backend with empty GPU buffers.
11
11
  *
12
- * Returns whether anything reached the backend, which is what lets the upload
13
- * autorun skip the redraw it would otherwise force after every run.
12
+ * Returns whether anything reached the backend, so the upload autorun can skip
13
+ * the redraw it would otherwise force after every run.
14
14
  *
15
15
  * For the skip to fire, the map must keep **stable references** for unchanged
16
16
  * entries across recomputes. With an always-fresh map every entry re-uploads,
@@ -5,8 +5,8 @@
5
5
  * re-uploads), and re-uploads everything when the backend identity changes —
6
6
  * a context-loss recovery hands a fresh backend with empty GPU buffers.
7
7
  *
8
- * Returns whether anything reached the backend, which is what lets the upload
9
- * autorun skip the redraw it would otherwise force after every run.
8
+ * Returns whether anything reached the backend, so the upload autorun can skip
9
+ * the redraw it would otherwise force after every run.
10
10
  *
11
11
  * For the skip to fire, the map must keep **stable references** for unchanged
12
12
  * entries across recomputes. With an always-fresh map every entry re-uploads,
@@ -1,16 +1,17 @@
1
1
  import type { ColorChannel } from './markRamp.ts';
2
+ import type { RowChannel, RowParams } from './rowLane.ts';
2
3
  import type { MarkRamp, MarkShape, MarkValueScaleType } from './types.ts';
3
4
  /**
4
5
  * The `bar` shape's channels: a rectangle from `x` to `x2` standing between
5
- * the baseline and `y` on the value scale.
6
+ * the baseline and `y` on the value scale, in the band of its `row`.
6
7
  */
7
- export interface BarChannels extends ColorChannel {
8
+ export interface BarChannels extends ColorChannel, RowChannel {
8
9
  x: Uint32Array;
9
10
  x2: Uint32Array;
10
11
  y: Float32Array;
11
12
  count: number;
12
13
  }
13
- export interface BarParams {
14
+ export interface BarParams extends RowParams {
14
15
  /** `[min, max]` `y` and `origin` are read through. */
15
16
  domain: [number, number];
16
17
  /** How that domain is read; linear when absent. */
@@ -24,5 +25,7 @@ export interface BarParams {
24
25
  * a no-op for a caller whose bars tile.
25
26
  */
26
27
  minWidthPx: number;
28
+ /** CSS px the painter alone adds to the drawn width; see `SpanParams.seamPx`. */
29
+ seamPx: number;
27
30
  }
28
31
  export declare const barMark: MarkShape<BarChannels, BarParams>;
@@ -1,21 +1,23 @@
1
1
  import { bpRangeXTuple } from '../blockClipUtils.js';
2
2
  import { getDpr, makeBpMapper, spanLeft } from '../canvas2dUtils.js';
3
+ import { scaleTypeCode } from '../scoreScale.js';
3
4
  import * as shader from '../shaders/barMark.generated.js';
4
5
  import { valueToYPxScaled } from '../shaders/pointMark.js.generated.js';
5
6
  import { slangPass } from '../slangPass.js';
6
7
  import { makeAbgrFill } from './colorFill.js';
7
- import { colorBits, paintColors, rampUniforms, valueScaleTypeCode, } from './markRamp.js';
8
- import { blockPx } from './spanMark.js';
8
+ import { colorBits, paintColors, rampUniforms } from './markRamp.js';
9
+ import { valueWindow } from './nearestMarkHit.js';
10
+ import { bandHeightPx, bandTopPx, rowLane } from './rowLane.js';
9
11
  // The rect one instance paints, in the frame's CSS px, or undefined for a bar
10
12
  // with no height — which draws nothing and so cannot be hovered.
11
- function barRect(bpToPx, x, x2, y, params, canvasHeight) {
13
+ function barRect(bpToPx, x, x2, y, bandTop, band, params) {
12
14
  const xa = bpToPx(x);
13
15
  const xb = bpToPx(x2);
14
16
  const width = Math.max(params.minWidthPx, Math.abs(xb - xa));
15
17
  const [domainMin, domainMax] = params.domain;
16
- const st = valueScaleTypeCode(params.scaleType);
17
- const valueY = valueToYPxScaled(y, domainMin, domainMax, canvasHeight, st);
18
- const originY = valueToYPxScaled(params.origin, domainMin, domainMax, canvasHeight, st);
18
+ const st = scaleTypeCode(params.scaleType);
19
+ const valueY = bandTop + valueToYPxScaled(y, domainMin, domainMax, band, st);
20
+ const originY = bandTop + valueToYPxScaled(params.origin, domainMin, domainMax, band, st);
19
21
  const top = Math.min(valueY, originY);
20
22
  const height = Math.abs(valueY - originY);
21
23
  return height === 0
@@ -26,7 +28,7 @@ export const barMark = {
26
28
  id: 'bar',
27
29
  pass: {
28
30
  ...slangPass({ id: 'bar', mod: shader }),
29
- pack: c => shader.packInstances({ ...c, color: colorBits(c) }, c.count),
31
+ pack: c => shader.packInstances({ ...c, color: colorBits(c), row: rowLane(c.row, c.count) }, c.count),
30
32
  },
31
33
  writeUniforms(scratch, clip, block, frame, params) {
32
34
  shader.writeUniforms(scratch, {
@@ -34,9 +36,10 @@ export const barMark = {
34
36
  canvasHeight: frame.canvasHeight,
35
37
  domainMin: params.domain[0],
36
38
  domainMax: params.domain[1],
37
- valueScaleType: valueScaleTypeCode(params.scaleType),
39
+ valueScaleType: scaleTypeCode(params.scaleType),
38
40
  ...rampUniforms(params.ramp),
39
41
  origin: params.origin,
42
+ rowHeight: bandHeightPx(params, frame.canvasHeight),
40
43
  zero: 0,
41
44
  minCellDenomPx: clip.scissorW,
42
45
  minWidthPx: params.minWidthPx,
@@ -44,20 +47,23 @@ export const barMark = {
44
47
  });
45
48
  },
46
49
  paintBlock(ctx, channels, block, frame, params) {
47
- const { x, x2, y, count } = channels;
50
+ const { x, x2, y, row, count } = channels;
48
51
  const color = paintColors(channels, count, params.ramp);
49
52
  const bpToPx = makeBpMapper(block);
50
53
  const setFill = makeAbgrFill(ctx);
54
+ const band = bandHeightPx(params, frame.canvasHeight);
51
55
  for (let i = 0; i < count; i++) {
52
- const r = barRect(bpToPx, x[i], x2[i], y[i], params, frame.canvasHeight);
56
+ const r = barRect(bpToPx, x[i], x2[i], y[i], bandTopPx(row, i, band), band, params);
53
57
  if (r) {
54
58
  setFill(color[i]);
55
- ctx.fillRect(r.left, r.top, r.width, r.height);
59
+ ctx.fillRect(r.left, r.top, r.width + params.seamPx, r.height);
56
60
  }
57
61
  }
58
62
  },
59
63
  ink(channels, block, frame, params, i) {
60
- const { x, x2, y } = channels;
61
- return barRect(bp => blockPx(block, bp), x[i], x2[i], y[i], params, frame.canvasHeight);
64
+ const { x, x2, y, row } = channels;
65
+ const band = bandHeightPx(params, frame.canvasHeight);
66
+ return barRect(makeBpMapper(block), x[i], x2[i], y[i], bandTopPx(row, i, band), band, params);
62
67
  },
68
+ valueWindow,
63
69
  };
@@ -1,5 +1,5 @@
1
1
  import type { RenderBlock } from '../renderBlock.ts';
2
- import type { MarkContext2D, MarkFrame, MarkShape } from './types.ts';
2
+ import type { Mark, MarkContext2D, MarkFrame } from './types.ts';
3
3
  export interface RecordedRect {
4
4
  x: number;
5
5
  y: number;
@@ -12,8 +12,8 @@ export interface RecordedRect {
12
12
  * in CANVAS coordinates — `fillRect` and path `rect` alike, so a shape that
13
13
  * batches by colour records the same list as one that fills per instance. A
14
14
  * path traced with `moveTo`/`lineTo`/`bezierCurveTo` and filled records its
15
- * bounding box, which is what a shape whose hit test answers a polygon's box
16
- * (the variant inversion triangle) is held to; a curve contributes its control
15
+ * bounding box, the extent a shape whose hit test answers a polygon's box
16
+ * (the variant inversion triangle) is checked against; a curve contributes its control
17
17
  * points, whose hull contains it.
18
18
  *
19
19
  * A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
@@ -60,59 +60,78 @@ export declare function recordingContext(): {
60
60
  };
61
61
  calls: RecordedRect[];
62
62
  };
63
+ /** The extent of a painting, in canvas px. */
64
+ export interface Box {
65
+ x: number;
66
+ y: number;
67
+ w: number;
68
+ h: number;
69
+ }
70
+ interface Counted {
71
+ count: number;
72
+ }
73
+ export interface SweepOptions<C> {
74
+ maxDistSq?: number;
75
+ step?: number;
76
+ sliceOne?: (channels: C, index: number) => C;
77
+ inkSlackPx?: number;
78
+ contains?: (index: number, xPx: number, yPx: number) => boolean;
79
+ }
80
+ /**
81
+ * The ink clause of {@link sweepMarkAgainstHit} alone, for a mark with no
82
+ * `hitNearest`: `boxes[i]` is what the painter recorded for instance `i`.
83
+ */
84
+ export declare function inkViolations<C, S extends MarkFrame>(mark: Mark<C, S>, channels: C, block: RenderBlock, state: S, boxes: readonly (Box | undefined)[], inkSlackPx: number): string[];
63
85
  /**
64
86
  * The draw-against-hit gate: paint the block, then walk it in `step` px and
65
87
  * hold `hitNearest` to what the painter actually put on the canvas.
66
88
  *
67
89
  * **What the painter drew for instance `i` has to be attributed to `i`,** and
68
- * there are two ways to get that. A shape whose every instance is one
69
- * `fillRect` gets it positionally, which is the default and needs nothing. A
70
- * shape that batches a colour run into one path (`point`'s glyphs), or that
71
- * skips an instance the view has scrolled past (`cell`'s off-canvas rows),
72
- * cannot be read that way at all — the batch has fewer rects than instances, or
73
- * the wrong ones. `sliceOne` is the way in for those: hand back one instance's
74
- * channels and the sweep paints it alone, so the attribution holds by
75
- * construction and a culled instance is visibly an empty painting.
90
+ * there are two ways to get that. A mark whose every instance is one `fillRect`
91
+ * gets it positionally, which is the default and needs nothing. A mark that
92
+ * batches a colour run into one path (`point`'s glyphs), or that skips an
93
+ * instance the view has scrolled past (`cell`'s off-canvas rows), cannot be read
94
+ * that way at all — the batch has fewer rects than instances, or the wrong ones.
95
+ * `sliceOne` is the way in for those: hand back one instance's channels and the
96
+ * sweep paints it alone, so the attribution holds by construction and a culled
97
+ * instance is visibly an empty painting.
76
98
  *
77
- * Four claims, all of them one-directional and true of every shape:
99
+ * Four claims, all of them one-directional and true of every mark:
78
100
  *
79
101
  * - an instance that painted nothing is never the answer,
80
- * - `hit.x`/`hit.y` — where the shape says its ink is — lies inside the box the
102
+ * - `hit.x`/`hit.y` — where the mark says its ink is — lies inside the box the
81
103
  * painter drew for the instance the hit names,
82
104
  * - `hit.distSq` is no smaller than the distance to that box, since the box
83
105
  * contains the ink and cannot be farther than it,
84
106
  * - `hit.distSq` IS the distance to `hit.x`/`hit.y`, which is `MarkHit`'s own
85
- * contract rather than a fact about the painting: a shape that answers a
107
+ * contract rather than a fact about the painting: a mark that answers a
86
108
  * distance its own reported point does not support has two spellings of where
87
109
  * its ink is, and the nearest-wins walk is resolved by the one the caller
88
110
  * cannot see.
89
111
  *
90
- * The fourth claim is the strongest and is not universal: **a point on the box
91
- * has to answer that box**, the last-painted one where boxes overlap, since the
92
- * sweep hands candidates back to front. It holds only where the painted box IS
93
- * the hit target — every `fillRect` shape, and `cell`'s inversion triangle,
94
- * which answers as its bounding box on purpose — and not for a glyph, whose box
95
- * is a superset of its ink. So it runs exactly when the batch painted one rect
96
- * per instance, which is that set.
112
+ * The fifth is the containment rule: **a point the rule contains answers the
113
+ * nearest painted instance it contains**, the last-painted where tied, since the
114
+ * sweep hands candidates back to front. The default rule is the painted box, so
115
+ * a point on a box answers that box, and off every box a hit target wider than
116
+ * its ink may still answer. The default holds only where the box IS the hit
117
+ * target — every `fillRect` mark, and `cell`'s inversion triangle, which answers
118
+ * as its bounding box on purpose — and not for a glyph, whose box is a superset
119
+ * of its ink, so it runs exactly when the batch painted one rect per instance. A
120
+ * mark whose hit rule is not its box (the pileup's bp containment) passes
121
+ * `contains`, which is then the whole rule: the sweep covers the whole canvas,
122
+ * and a point the rule contains nowhere answers nothing.
97
123
  *
98
- * A shape declaring `ink` is held to a second clause, per instance: the box
99
- * the painter recorded for `i` lies inside `ink(i)`, and each edge of `ink(i)`
100
- * lies within `inkSlackPx` (default 1) of the painting's — so the rect is
101
- * tight to the ink within a pixel on every side, which admits a seam overdraw
102
- * or a pixel snap and rejects a rect that is the wrong row, the wrong width or
103
- * the whole canvas. An instance that painted nothing must answer no ink, and
104
- * one with ink must have painted.
124
+ * A mark declaring `ink` is held to one more clause, per instance: the box the
125
+ * painter recorded for `i` lies inside `ink(i)`, and each edge of `ink(i)` lies
126
+ * within `inkSlackPx` (default 1) of the painting's — so the rect is tight to
127
+ * the ink within a pixel on every side, which admits a seam overdraw or a pixel
128
+ * snap and rejects a rect that is the wrong row, the wrong width or the whole
129
+ * canvas. An instance that painted nothing must answer no ink, and one with ink
130
+ * must have painted.
105
131
  *
106
132
  * Returns the violations rather than asserting, so a test reads as
107
- * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
108
- * shape whose hit target is wider than its ink (`point`) needs a bound that
109
- * admits the whole rect.
133
+ * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the bound the hit test is
134
+ * asked with, `Infinity` unless the caller's differs.
110
135
  */
111
- export declare function sweepDrawAgainstHit<C extends {
112
- count: number;
113
- }, P>(shape: MarkShape<C, P>, channels: C, block: RenderBlock, frame: MarkFrame, params: P, { maxDistSq, step, sliceOne, inkSlackPx, }: {
114
- maxDistSq: number;
115
- step?: number;
116
- sliceOne?: (channels: C, index: number) => C;
117
- inkSlackPx?: number;
118
- }): string[];
136
+ export declare function sweepMarkAgainstHit<C extends Counted, S extends MarkFrame>(mark: Mark<NoInfer<C>, S>, channels: C, block: RenderBlock, state: S, { maxDistSq, step, sliceOne, inkSlackPx, contains: rule, }?: SweepOptions<C>): string[];
137
+ export {};