@jbrowse/render-core 5.0.0-beta.8 → 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
@@ -47,11 +47,11 @@ export default function RenderCanvas({ handle, drawn, phase, ...canvasProps }: {
47
47
  /**
48
48
  * Whether this canvas holds finished content — each view's own `settled`, the
49
49
  * same flag its `*_canvas_done` testid is built from. Published as
50
- * `data-display-drawn`, which is what `PENDING_DISPLAYS`
50
+ * `data-display-drawn`, the attribute `PENDING_DISPLAYS`
51
51
  * (`@jbrowse/browser-test-utils`) selects on, so these views answer "has
52
52
  * everything painted?" with the same attribute every LGV display does.
53
53
  *
54
- * Required, and that is the point: `PENDING_DISPLAYS` used to name
54
+ * The prop is required because `PENDING_DISPLAYS` used to name
55
55
  * `synteny_canvas` explicitly and simply **forgot dotplot**, so an unpainted
56
56
  * dotplot counted as finished and a capture could land on it blank. A new
57
57
  * drop-to-primitive view would have been forgotten the same way.
@@ -142,8 +142,8 @@ export declare function RenderLifecycleMixin(): import("@jbrowse/mobx-state-tree
142
142
  * #getter
143
143
  * **The first-paint answer every consumer outside the display should
144
144
  * read**, `canvasDrawn` being only the raw flag: a display that is
145
- * deliberately not painting a canvas has finished, and saying otherwise
146
- * is a lie that never resolves.
145
+ * deliberately not painting a canvas has finished, and reporting it
146
+ * unfinished leaves every waiter on it waiting forever.
147
147
  *
148
148
  * The two `rendersCanvas: false` states each had three of their four
149
149
  * consumers wired by hand — the loading scrim (`rendersCanvas` /
@@ -151,10 +151,11 @@ export declare function RenderLifecycleMixin(): import("@jbrowse/mobx-state-tree
151
151
  * while the fourth, `data-display-drawn`, went on publishing `"false"`
152
152
  * forever off the raw flag. That attribute is what `PENDING_DISPLAYS`
153
153
  * (`@jbrowse/browser-test-utils`) selects on, so a zoomed-out reference
154
- * sequence track made every `waitForDisplaysDone` on the page burn its
155
- * full timeout — silently, since that wait swallows its own. Same shape
156
- * as `fetchInert` on the comparative side: the reader you forget is the
157
- * one outside the display, so the display has to publish one name for it.
154
+ * sequence track made every `waitForDisplaysDone` on the page run to its
155
+ * full timeout, and that wait swallows its own timeout without reporting
156
+ * it. `fetchInert` on the comparative side has the same problem: the
157
+ * forgotten reader is the one outside the display, so the display has to
158
+ * publish one name for it.
158
159
  *
159
160
  * `paintInert` is the third term and the same argument once more, for the
160
161
  * state where a display *would* paint a canvas and never gets to — a fetch
@@ -164,8 +164,8 @@ export function RenderLifecycleMixin() {
164
164
  * #getter
165
165
  * **The first-paint answer every consumer outside the display should
166
166
  * read**, `canvasDrawn` being only the raw flag: a display that is
167
- * deliberately not painting a canvas has finished, and saying otherwise
168
- * is a lie that never resolves.
167
+ * deliberately not painting a canvas has finished, and reporting it
168
+ * unfinished leaves every waiter on it waiting forever.
169
169
  *
170
170
  * The two `rendersCanvas: false` states each had three of their four
171
171
  * consumers wired by hand — the loading scrim (`rendersCanvas` /
@@ -173,10 +173,11 @@ export function RenderLifecycleMixin() {
173
173
  * while the fourth, `data-display-drawn`, went on publishing `"false"`
174
174
  * forever off the raw flag. That attribute is what `PENDING_DISPLAYS`
175
175
  * (`@jbrowse/browser-test-utils`) selects on, so a zoomed-out reference
176
- * sequence track made every `waitForDisplaysDone` on the page burn its
177
- * full timeout — silently, since that wait swallows its own. Same shape
178
- * as `fetchInert` on the comparative side: the reader you forget is the
179
- * one outside the display, so the display has to publish one name for it.
176
+ * sequence track made every `waitForDisplaysDone` on the page run to its
177
+ * full timeout, and that wait swallows its own timeout without reporting
178
+ * it. `fetchInert` on the comparative side has the same problem: the
179
+ * forgotten reader is the one outside the display, so the display has to
180
+ * publish one name for it.
180
181
  *
181
182
  * `paintInert` is the third term and the same argument once more, for the
182
183
  * state where a display *would* paint a canvas and never gets to — a fetch
@@ -109,7 +109,7 @@ export interface ClipContext2D {
109
109
  * Clip to one rect, paint, unclip — the `save`/`clip`/`restore` triple with the
110
110
  * `restore` in a `finally`.
111
111
  *
112
- * The `finally` is the whole point, and the cost of missing it is not local: an
112
+ * The `finally` is required, and missing it breaks more than the one frame: an
113
113
  * on-screen ctx outlives the frame and `prepareCanvas`'s setTransform/clearRect
114
114
  * do **not** reset clip state, so one painter throwing once leaves every later
115
115
  * frame drawing through a stale clip. A transient error becomes permanent
@@ -120,7 +120,7 @@ export interface ClipContext2D {
120
120
  * own. Reach for this one for the clips *nested inside* a block — a display
121
121
  * whose bands each own a horizontal strip of the canvas (alignments clips its
122
122
  * coverage, pileup and arc bands separately within one block clip), where the
123
- * nesting is what makes a hand-rolled pairing easy to get wrong: an early
123
+ * nesting makes a hand-rolled pairing easy to get wrong: an early
124
124
  * `return` inside the band body unbalances the block's `save` too.
125
125
  */
126
126
  export declare function withClip(ctx: ClipContext2D, x: number, y: number, w: number, h: number, paint: () => void): void;
@@ -229,8 +229,31 @@ export declare function spanRect(toX: (bp: number) => number, startBp: number, e
229
229
  width: number;
230
230
  };
231
231
  export declare function makeRampFillStyleLut(ramp: Uint8Array): (t: number) => string;
232
- export declare function bpToScreenPx(absBp: number, regionStart: number, regionEnd: number, screenStartPx: number, screenEndPx: number, reversed?: boolean): number;
232
+ /**
233
+ * A block's bp→px projection as four numbers, for a placement function reading
234
+ * its block off a frame object. `makeBpMapper` closes over the same four.
235
+ */
236
+ export interface BpProjection {
237
+ originPx: number;
238
+ startBp: number;
239
+ spanBp: number;
240
+ signedSpanPx: number;
241
+ }
242
+ export declare function bpProjection(bounds: BpRegionBounds): BpProjection;
243
+ export declare function projectBp(p: BpProjection, bp: number): number;
244
+ /**
245
+ * One closure literal for both orientations. Split per orientation, a paint
246
+ * loop's mapper call goes megamorphic on a view mixing them, and the loop ran
247
+ * 1.9-2.3x slower (`benches/bpMapper.bench.ts`).
248
+ */
233
249
  export declare function makeBpMapper(bounds: BpRegionBounds): (bp: number) => number;
250
+ /**
251
+ * Screen px one base occupies in this region: the scale factor, unsigned, with
252
+ * the block's direction left to `makeBpMapper` and `spanLeft`. Shared so a mark
253
+ * sized against it in a draw pass and hit-tested against it in a component
254
+ * read one number.
255
+ */
256
+ export declare function pxPerBpOf(bounds: BpRegionBounds): number;
234
257
  /**
235
258
  * Left edge of the **1bp cell** covering `bp` — for painters that fill a rect
236
259
  * per base (MAF alignment cells, the alignments pileup's mismatch /
@@ -255,18 +278,6 @@ export declare function makeBpMapper(bounds: BpRegionBounds): (bp: number) => nu
255
278
  * bp-scale; the pileup floors at 1px and adds a seam fudge). Only the pivot is
256
279
  * shared.
257
280
  */
258
- /**
259
- * Screen px one base occupies in this region — the scale factor, unsigned, with
260
- * the block's direction deliberately left out (that lives in `makeBpMapper` and
261
- * `spanLeft`, and folding it in here would make a *scale* carry an orientation).
262
- *
263
- * Shared because it is usually an input to something a second painter has to
264
- * reproduce exactly: a mark sized against it in a draw pass and hit-tested
265
- * against it in a component is two spellings of one number, and the hit target
266
- * silently stops covering the mark if they ever drift. Trivial arithmetic, but
267
- * the drift is not.
268
- */
269
- export declare function pxPerBpOf(bounds: BpRegionBounds): number;
270
281
  export declare function makeCellLeftMapper(bounds: BpRegionBounds): (bp: number) => number;
271
282
  /**
272
283
  * Fill the rect covering genomic `[startBp, endBp)`, given a bare
@@ -276,7 +287,7 @@ export declare function makeCellLeftMapper(bounds: BpRegionBounds): (bp: number)
276
287
  *
277
288
  * Prefer this to `makeCellLeftMapper` + a caller-computed width for anything
278
289
  * wider than one base. The pivot in `makeCellLeftMapper` is exactly one base,
279
- * which is right for a 1bp cell and silently wrong for an N-bp span — it
290
+ * which is right for a 1bp cell and wrong for an N-bp span — it
280
291
  * anchors the rect one base from the correct edge and then grows it the wrong
281
292
  * way. `fillBpSpan(bp, bp + 1)` is *identical* to the cell-mapper spelling, so
282
293
  * a per-base painter can use this too and never think about orientation again.
@@ -333,7 +344,7 @@ export declare function fillBpSpan(ctx: {
333
344
  * `14468.9936` — and rounding the *offset* and then adding such an anchor
334
345
  * returns the anchor's fraction untouched. The result is then not a base at
335
346
  * all, and every caller that indexes one by equality (a mismatch column, a
336
- * coverage bin, the sort-at-this-column anchor) misses silently: the pileup
347
+ * coverage bin, the sort-at-this-column anchor) finds no match: the pileup
337
348
  * offered no SNP submenu over a mismatch it was drawing. Rounding
338
349
  * `whole + fraction + offset` in one step instead fixes that and hands back the
339
350
  * genome-scale addend the paragraph above exists to keep out, so the whole base
@@ -395,3 +406,33 @@ export declare function regionAtPixel<R extends {
395
406
  screenStartPx: number;
396
407
  screenEndPx: number;
397
408
  }>(regions: readonly R[], px: number): R | undefined;
409
+ /**
410
+ * CSS px a Canvas2D painter adds to the drawn width of a rect whose neighbours
411
+ * abut it. `fillRect` antialiases each edge on its own, so two rects meeting on
412
+ * a fractional column each take partial coverage of it and a hairline of
413
+ * background shows through; abutting GPU quads share an exact clip-space edge
414
+ * and the rasterizer fills it once, so a shader grows no matching pad.
415
+ */
416
+ export declare const CANVAS_SEAM_PX = 0.8;
417
+ export interface PathPaintContext {
418
+ beginPath(): void;
419
+ fill(): void;
420
+ stroke(): void;
421
+ }
422
+ /**
423
+ * One batched Canvas2D path of many shapes, painted in pieces a browser will
424
+ * rasterize. Opens a path on construction; call `add()` before appending each
425
+ * shape and `flush()` to paint what it holds and reopen.
426
+ */
427
+ export declare class CappedPath {
428
+ private shapes;
429
+ private readonly ctx;
430
+ private readonly paint;
431
+ constructor(ctx: PathPaintContext, paint: 'fill' | 'stroke');
432
+ /**
433
+ * True when the path was full and has been painted and reopened, so a
434
+ * polyline must `moveTo` its pen again.
435
+ */
436
+ add(): boolean;
437
+ flush(): void;
438
+ }
@@ -216,7 +216,7 @@ export function clipBlockForCanvas(block, canvasWidth) {
216
216
  * Clip to one rect, paint, unclip — the `save`/`clip`/`restore` triple with the
217
217
  * `restore` in a `finally`.
218
218
  *
219
- * The `finally` is the whole point, and the cost of missing it is not local: an
219
+ * The `finally` is required, and missing it breaks more than the one frame: an
220
220
  * on-screen ctx outlives the frame and `prepareCanvas`'s setTransform/clearRect
221
221
  * do **not** reset clip state, so one painter throwing once leaves every later
222
222
  * frame drawing through a stale clip. A transient error becomes permanent
@@ -227,7 +227,7 @@ export function clipBlockForCanvas(block, canvasWidth) {
227
227
  * own. Reach for this one for the clips *nested inside* a block — a display
228
228
  * whose bands each own a horizontal strip of the canvas (alignments clips its
229
229
  * coverage, pileup and arc bands separately within one block clip), where the
230
- * nesting is what makes a hand-rolled pairing easy to get wrong: an early
230
+ * nesting makes a hand-rolled pairing easy to get wrong: an early
231
231
  * `return` inside the band body unbalances the block's `save` too.
232
232
  */
233
233
  export function withClip(ctx, x, y, w, h, paint) {
@@ -386,21 +386,36 @@ export function makeRampFillStyleLut(ramp) {
386
386
  return s;
387
387
  };
388
388
  }
389
- export function bpToScreenPx(absBp, regionStart, regionEnd, screenStartPx, screenEndPx, reversed) {
390
- const frac = (absBp - regionStart) / (regionEnd - regionStart);
389
+ export function bpProjection(bounds) {
390
+ const { start, end, screenStartPx, screenEndPx, reversed } = bounds;
391
391
  const w = screenEndPx - screenStartPx;
392
- return reversed ? screenEndPx - frac * w : screenStartPx + frac * w;
392
+ return {
393
+ originPx: reversed ? screenEndPx : screenStartPx,
394
+ startBp: start,
395
+ spanBp: end - start,
396
+ signedSpanPx: reversed ? -w : w,
397
+ };
398
+ }
399
+ export function projectBp(p, bp) {
400
+ return p.originPx + ((bp - p.startBp) / p.spanBp) * p.signedSpanPx;
393
401
  }
394
- // Closure-style bp→screen-px mapper bound to one region. Consumers that walk
395
- // many bp positions inside one block (label/peptide overlays, rect/line draws)
396
- // want a 1-arg call instead of repeating 6 args at every site.
402
+ /**
403
+ * One closure literal for both orientations. Split per orientation, a paint
404
+ * loop's mapper call goes megamorphic on a view mixing them, and the loop ran
405
+ * 1.9-2.3x slower (`benches/bpMapper.bench.ts`).
406
+ */
397
407
  export function makeBpMapper(bounds) {
398
- const { start, end, screenStartPx, screenEndPx, reversed } = bounds;
399
- const span = end - start;
400
- const w = screenEndPx - screenStartPx;
401
- return reversed
402
- ? (bp) => screenEndPx - ((bp - start) / span) * w
403
- : (bp) => screenStartPx + ((bp - start) / span) * w;
408
+ const { originPx, startBp, spanBp, signedSpanPx } = bpProjection(bounds);
409
+ return (bp) => originPx + ((bp - startBp) / spanBp) * signedSpanPx;
410
+ }
411
+ /**
412
+ * Screen px one base occupies in this region: the scale factor, unsigned, with
413
+ * the block's direction left to `makeBpMapper` and `spanLeft`. Shared so a mark
414
+ * sized against it in a draw pass and hit-tested against it in a component
415
+ * read one number.
416
+ */
417
+ export function pxPerBpOf(bounds) {
418
+ return ((bounds.screenEndPx - bounds.screenStartPx) / (bounds.end - bounds.start));
404
419
  }
405
420
  /**
406
421
  * Left edge of the **1bp cell** covering `bp` — for painters that fill a rect
@@ -426,20 +441,6 @@ export function makeBpMapper(bounds) {
426
441
  * bp-scale; the pileup floors at 1px and adds a seam fudge). Only the pivot is
427
442
  * shared.
428
443
  */
429
- /**
430
- * Screen px one base occupies in this region — the scale factor, unsigned, with
431
- * the block's direction deliberately left out (that lives in `makeBpMapper` and
432
- * `spanLeft`, and folding it in here would make a *scale* carry an orientation).
433
- *
434
- * Shared because it is usually an input to something a second painter has to
435
- * reproduce exactly: a mark sized against it in a draw pass and hit-tested
436
- * against it in a component is two spellings of one number, and the hit target
437
- * silently stops covering the mark if they ever drift. Trivial arithmetic, but
438
- * the drift is not.
439
- */
440
- export function pxPerBpOf(bounds) {
441
- return ((bounds.screenEndPx - bounds.screenStartPx) / (bounds.end - bounds.start));
442
- }
443
444
  export function makeCellLeftMapper(bounds) {
444
445
  const toX = makeBpMapper(bounds);
445
446
  const pxPerBp = pxPerBpOf(bounds);
@@ -456,7 +457,7 @@ export function makeCellLeftMapper(bounds) {
456
457
  *
457
458
  * Prefer this to `makeCellLeftMapper` + a caller-computed width for anything
458
459
  * wider than one base. The pivot in `makeCellLeftMapper` is exactly one base,
459
- * which is right for a 1bp cell and silently wrong for an N-bp span — it
460
+ * which is right for a 1bp cell and wrong for an N-bp span — it
460
461
  * anchors the rect one base from the correct edge and then grows it the wrong
461
462
  * way. `fillBpSpan(bp, bp + 1)` is *identical* to the cell-mapper spelling, so
462
463
  * a per-base painter can use this too and never think about orientation again.
@@ -515,7 +516,7 @@ export function fillBpSpan(ctx, toX, startBp, endBp, top, height, seam = 0) {
515
516
  * `14468.9936` — and rounding the *offset* and then adding such an anchor
516
517
  * returns the anchor's fraction untouched. The result is then not a base at
517
518
  * all, and every caller that indexes one by equality (a mismatch column, a
518
- * coverage bin, the sort-at-this-column anchor) misses silently: the pileup
519
+ * coverage bin, the sort-at-this-column anchor) finds no match: the pileup
519
520
  * offered no SNP submenu over a mismatch it was drawing. Rounding
520
521
  * `whole + fraction + offset` in one step instead fixes that and hands back the
521
522
  * genome-scale addend the paragraph above exists to keep out, so the whole base
@@ -589,3 +590,54 @@ export function bpAtPxExact(px, bounds) {
589
590
  export function regionAtPixel(regions, px) {
590
591
  return regions.find(r => px >= r.screenStartPx && px < r.screenEndPx);
591
592
  }
593
+ /**
594
+ * CSS px a Canvas2D painter adds to the drawn width of a rect whose neighbours
595
+ * abut it. `fillRect` antialiases each edge on its own, so two rects meeting on
596
+ * a fractional column each take partial coverage of it and a hairline of
597
+ * background shows through; abutting GPU quads share an exact clip-space edge
598
+ * and the rasterizer fills it once, so a shader grows no matching pad.
599
+ */
600
+ export const CANVAS_SEAM_PX = 0.8;
601
+ /**
602
+ * Shapes a `CappedPath` holds before it paints and reopens. Chrome's
603
+ * GPU-rasterized canvas, which is desktop Chrome with a GPU and headless Chrome
604
+ * with the GPU blocklist off, silently paints nothing for a path past about
605
+ * 16 MiB: 180,000 discs, 453,000 rects or 931,000 line segments.
606
+ * `pnpm check-canvas-path-limits` drives the painters past it in a real browser.
607
+ */
608
+ const MAX_SHAPES_PER_PATH = 10_000;
609
+ /**
610
+ * One batched Canvas2D path of many shapes, painted in pieces a browser will
611
+ * rasterize. Opens a path on construction; call `add()` before appending each
612
+ * shape and `flush()` to paint what it holds and reopen.
613
+ */
614
+ export class CappedPath {
615
+ shapes = 0;
616
+ ctx;
617
+ paint;
618
+ constructor(ctx, paint) {
619
+ this.ctx = ctx;
620
+ this.paint = paint;
621
+ ctx.beginPath();
622
+ }
623
+ /**
624
+ * True when the path was full and has been painted and reopened, so a
625
+ * polyline must `moveTo` its pen again.
626
+ */
627
+ add() {
628
+ if (this.shapes === MAX_SHAPES_PER_PATH) {
629
+ this.flush();
630
+ this.shapes = 1;
631
+ return true;
632
+ }
633
+ this.shapes++;
634
+ return false;
635
+ }
636
+ flush() {
637
+ if (this.shapes > 0) {
638
+ this.ctx[this.paint]();
639
+ this.ctx.beginPath();
640
+ this.shapes = 0;
641
+ }
642
+ }
643
+ }
@@ -1,4 +1,4 @@
1
- import type { ShaderBinding } from './hal/index.ts';
1
+ import type { ShaderBinding, ShaderSource } from './hal/index.ts';
2
2
  export interface ComputePipelineState {
3
3
  device: GPUDevice;
4
4
  pipeline: GPUComputePipeline;
@@ -8,6 +8,7 @@ export interface ComputePipelineState {
8
8
  /**
9
9
  * One compute pipeline per kernel, built on first use against the current
10
10
  * device and rebuilt after a device loss. Concurrent first callers share the
11
- * one async build.
11
+ * one async build, which is also when the kernel's WGSL is loaded; a failed
12
+ * one is retried by the next caller rather than kept.
12
13
  */
13
- export declare function makeComputePipelineCache(code: string, entryPoint: string, bindings: readonly ShaderBinding[]): (device: GPUDevice) => Promise<ComputePipelineState>;
14
+ export declare function makeComputePipelineCache(source: Pick<ShaderSource, 'wgsl'>, entryPoint: string, bindings: readonly ShaderBinding[]): (device: GPUDevice) => Promise<ComputePipelineState>;
@@ -1,5 +1,6 @@
1
1
  /// <reference types="@webgpu/types" />
2
2
  import { onDeviceLost } from './gpuDevice.js';
3
+ import { stageVisibility } from './webgpuUtils.js';
3
4
  // Built from the kernel's own reflected binding table rather than restated by
4
5
  // the caller, so a mismatch shows up as a validation failure at pipeline
5
6
  // creation rather than as a wrong buffer under a right-looking index.
@@ -12,7 +13,7 @@ function makeBindGroupLayout(device, bindings) {
12
13
  }
13
14
  return {
14
15
  binding: b.index,
15
- visibility: GPUShaderStage.COMPUTE,
16
+ visibility: stageVisibility(b.stages),
16
17
  buffer: { type: b.kind },
17
18
  };
18
19
  }),
@@ -21,9 +22,10 @@ function makeBindGroupLayout(device, bindings) {
21
22
  /**
22
23
  * One compute pipeline per kernel, built on first use against the current
23
24
  * device and rebuilt after a device loss. Concurrent first callers share the
24
- * one async build.
25
+ * one async build, which is also when the kernel's WGSL is loaded; a failed
26
+ * one is retried by the next caller rather than kept.
25
27
  */
26
- export function makeComputePipelineCache(code, entryPoint, bindings) {
28
+ export function makeComputePipelineCache(source, entryPoint, bindings) {
27
29
  let state = null;
28
30
  let statePromise = null;
29
31
  onDeviceLost(() => {
@@ -38,17 +40,22 @@ export function makeComputePipelineCache(code, entryPoint, bindings) {
38
40
  return statePromise;
39
41
  }
40
42
  statePromise = (async () => {
41
- const module = device.createShaderModule({ code });
42
- const bindGroupLayout = makeBindGroupLayout(device, bindings);
43
- const pipeline = await device.createComputePipelineAsync({
44
- layout: device.createPipelineLayout({
45
- bindGroupLayouts: [bindGroupLayout],
46
- }),
47
- compute: { module, entryPoint },
48
- });
49
- state = { device, pipeline, bindGroupLayout, bindings };
50
- statePromise = null;
51
- return state;
43
+ try {
44
+ const { WGSL_SOURCE } = await source.wgsl();
45
+ const module = device.createShaderModule({ code: WGSL_SOURCE });
46
+ const bindGroupLayout = makeBindGroupLayout(device, bindings);
47
+ const pipeline = await device.createComputePipelineAsync({
48
+ layout: device.createPipelineLayout({
49
+ bindGroupLayouts: [bindGroupLayout],
50
+ }),
51
+ compute: { module, entryPoint },
52
+ });
53
+ state = { device, pipeline, bindGroupLayout, bindings };
54
+ return state;
55
+ }
56
+ finally {
57
+ statePromise = null;
58
+ }
52
59
  })();
53
60
  return statePromise;
54
61
  };
@@ -80,7 +80,7 @@ export interface CoverageBandUniformValues {
80
80
  covHeight: number;
81
81
  covYOffset: number;
82
82
  covTop: number;
83
- /** The region's own peak depth, which is what the buffers' `relDepth` is a fraction of. */
83
+ /** The region's peak depth; the buffers' `relDepth` is a fraction of it. */
84
84
  regionMaxDepth: number;
85
85
  /** The display's autoscaled depth domain. `undefined` max = not resolved yet. */
86
86
  domainMin: number;
@@ -98,8 +98,8 @@ export interface CoverageBandUniformValues {
98
98
  * Fill the coverage band's uniform buffer. Total-write (the generated packer),
99
99
  * so a field left out is a compile error rather than last frame's value.
100
100
  *
101
- * `hpZero` is derived here rather than at each call site because getting it
102
- * wrong is a silently wrong band, not a broken one: it MUST be 0, since the HP
101
+ * `hpZero` is derived here rather than at each call site because a wrong value
102
+ * draws a plausible but wrong band and raises no error: it MUST be 0, since the HP
103
103
  * math materializes +inf as `1/hpZero` to stop the compiler folding the hi/lo
104
104
  * split it exists to preserve.
105
105
  */
@@ -69,8 +69,8 @@ export const COVERAGE_INDICATOR_PASS = {
69
69
  * Fill the coverage band's uniform buffer. Total-write (the generated packer),
70
70
  * so a field left out is a compile error rather than last frame's value.
71
71
  *
72
- * `hpZero` is derived here rather than at each call site because getting it
73
- * wrong is a silently wrong band, not a broken one: it MUST be 0, since the HP
72
+ * `hpZero` is derived here rather than at each call site because a wrong value
73
+ * draws a plausible but wrong band and raises no error: it MUST be 0, since the HP
74
74
  * math materializes +inf as `1/hpZero` to stop the compiler folding the hi/lo
75
75
  * split it exists to preserve.
76
76
  */
@@ -7,9 +7,8 @@
7
7
  *
8
8
  * Structural rather than nominal, and named after the layout rather than after
9
9
  * either display: the alignments worker's `CoverageUploadData` and the MAF
10
- * worker's `MafCoverageRegion` both satisfy it, which is what lets the passes
11
- * in `coverageBand.ts` carry their own packers instead of each plugin
12
- * restating four field reads.
10
+ * worker's `MafCoverageRegion` both satisfy it, so the passes in
11
+ * `coverageBand.ts` carry the packers and no plugin restates four field reads.
13
12
  *
14
13
  * Its own module, apart from the GPU passes in `coverageBand.ts`: a state
15
14
  * model reaching for just this shape has no reason to statically pull in
@@ -19,7 +19,7 @@ export interface RenderingBackendOptions<TRenderingBackend> {
19
19
  * panel hold 109.7 MiB nothing counts (ARCHITECTURAL_LIMITS.md §"The MSAA
20
20
  * target is the largest per-display allocation"); 1 allocates none. Whether a
21
21
  * display's pixels survive the flip is a look-at-the-pixels decision taken
22
- * one display at a time; `ideas/arc-antialiasing-without-msaa.md` is the
22
+ * one display at a time; `ideas/ready/arc-antialiasing-without-msaa.md` is the
23
23
  * survey and the captures.
24
24
  */
25
25
  sampleCount?: SampleCount;