@jbrowse/render-core 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +201 -0
  2. package/esm/OverlayCanvas.d.ts +29 -0
  3. package/esm/OverlayCanvas.js +43 -0
  4. package/esm/RenderCanvas.d.ts +76 -0
  5. package/esm/RenderCanvas.js +38 -0
  6. package/esm/RenderLifecycleMixin.d.ts +180 -0
  7. package/esm/RenderLifecycleMixin.js +279 -0
  8. package/esm/ScrollLockedOverlay.d.ts +28 -0
  9. package/esm/ScrollLockedOverlay.js +42 -0
  10. package/esm/blockClipUtils.d.ts +27 -0
  11. package/esm/blockClipUtils.js +101 -0
  12. package/esm/canvas2dUtils.d.ts +372 -0
  13. package/esm/canvas2dUtils.js +564 -0
  14. package/esm/canvasContext.d.ts +68 -0
  15. package/esm/canvasContext.js +74 -0
  16. package/esm/colorRampLut.d.ts +17 -0
  17. package/esm/colorRampLut.js +25 -0
  18. package/esm/coverageBand.d.ts +107 -0
  19. package/esm/coverageBand.js +109 -0
  20. package/esm/coverageBandBuffers.d.ts +30 -0
  21. package/esm/coverageBandBuffers.js +13 -0
  22. package/esm/createRenderingBackend.d.ts +52 -0
  23. package/esm/createRenderingBackend.js +58 -0
  24. package/esm/displayPhase.d.ts +155 -0
  25. package/esm/displayPhase.js +97 -0
  26. package/esm/globalRenderingBackend.d.ts +90 -0
  27. package/esm/globalRenderingBackend.js +53 -0
  28. package/esm/gpuDevice.d.ts +91 -0
  29. package/esm/gpuDevice.js +277 -0
  30. package/esm/graphicsCapabilities.d.ts +98 -0
  31. package/esm/graphicsCapabilities.js +165 -0
  32. package/esm/hal/createHal.d.ts +22 -0
  33. package/esm/hal/createHal.js +91 -0
  34. package/esm/hal/deviceGpuCache.d.ts +36 -0
  35. package/esm/hal/deviceGpuCache.js +120 -0
  36. package/esm/hal/gpuHalBase.d.ts +55 -0
  37. package/esm/hal/gpuHalBase.js +81 -0
  38. package/esm/hal/index.d.ts +7 -0
  39. package/esm/hal/index.js +5 -0
  40. package/esm/hal/mockHal.d.ts +153 -0
  41. package/esm/hal/mockHal.js +297 -0
  42. package/esm/hal/oomReporter.d.ts +15 -0
  43. package/esm/hal/oomReporter.js +22 -0
  44. package/esm/hal/passIds.d.ts +22 -0
  45. package/esm/hal/passIds.js +36 -0
  46. package/esm/hal/regionRegistry.d.ts +11 -0
  47. package/esm/hal/regionRegistry.js +66 -0
  48. package/esm/hal/types.d.ts +116 -0
  49. package/esm/hal/types.js +1 -0
  50. package/esm/hal/webgl2Hal.d.ts +47 -0
  51. package/esm/hal/webgl2Hal.js +490 -0
  52. package/esm/hal/webgpuHal.d.ts +169 -0
  53. package/esm/hal/webgpuHal.js +722 -0
  54. package/esm/installUpload.d.ts +91 -0
  55. package/esm/installUpload.js +94 -0
  56. package/esm/instanceCache.d.ts +43 -0
  57. package/esm/instanceCache.js +53 -0
  58. package/esm/instancePass.d.ts +50 -0
  59. package/esm/instancePass.js +34 -0
  60. package/esm/keyedRenderingBackend.d.ts +41 -0
  61. package/esm/keyedRenderingBackend.js +19 -0
  62. package/esm/mapUploadSync.d.ts +31 -0
  63. package/esm/mapUploadSync.js +45 -0
  64. package/esm/namedReactions.d.ts +29 -0
  65. package/esm/namedReactions.js +62 -0
  66. package/esm/perRegionRenderingBackend.d.ts +114 -0
  67. package/esm/perRegionRenderingBackend.js +93 -0
  68. package/esm/recoveryBudget.d.ts +40 -0
  69. package/esm/recoveryBudget.js +56 -0
  70. package/esm/regionDataMap.d.ts +60 -0
  71. package/esm/regionDataMap.js +98 -0
  72. package/esm/renderBlock.d.ts +25 -0
  73. package/esm/renderBlock.js +10 -0
  74. package/esm/renderingBackendBase.d.ts +71 -0
  75. package/esm/renderingBackendBase.js +50 -0
  76. package/esm/shaders/antialias.js.generated.d.ts +4 -0
  77. package/esm/shaders/antialias.js.generated.js +25 -0
  78. package/esm/shaders/capsule.generated.d.ts +1 -0
  79. package/esm/shaders/capsule.generated.js +3 -0
  80. package/esm/shaders/colorRampLut.generated.d.ts +1 -0
  81. package/esm/shaders/colorRampLut.generated.js +3 -0
  82. package/esm/shaders/coverageBand.generated.d.ts +2 -0
  83. package/esm/shaders/coverageBand.generated.js +4 -0
  84. package/esm/shaders/coverageBar.generated.d.ts +4 -0
  85. package/esm/shaders/coverageBar.generated.js +6 -0
  86. package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
  87. package/esm/shaders/coverageBar.iface.generated.js +114 -0
  88. package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
  89. package/esm/shaders/coverageIndicator.generated.js +6 -0
  90. package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
  91. package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
  92. package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
  93. package/esm/shaders/coverageInterbase.generated.js +6 -0
  94. package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
  95. package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
  96. package/esm/shaders/coverageMod.generated.d.ts +4 -0
  97. package/esm/shaders/coverageMod.generated.js +6 -0
  98. package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
  99. package/esm/shaders/coverageMod.iface.generated.js +144 -0
  100. package/esm/shaders/coverageSnp.generated.d.ts +4 -0
  101. package/esm/shaders/coverageSnp.generated.js +6 -0
  102. package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
  103. package/esm/shaders/coverageSnp.iface.generated.js +144 -0
  104. package/esm/shaders/hpmath.js.generated.d.ts +3 -0
  105. package/esm/shaders/hpmath.js.generated.js +48 -0
  106. package/esm/shaders/pointGlyph.generated.d.ts +1 -0
  107. package/esm/shaders/pointGlyph.generated.js +3 -0
  108. package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
  109. package/esm/shaders/pointGlyph.js.generated.js +7 -0
  110. package/esm/shaders/rowRect.generated.d.ts +3 -0
  111. package/esm/shaders/rowRect.generated.js +5 -0
  112. package/esm/shaders/rowRect.js.generated.d.ts +2 -0
  113. package/esm/shaders/rowRect.js.generated.js +13 -0
  114. package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
  115. package/esm/shaders/scoreScale.js.generated.js +46 -0
  116. package/esm/slangPass.d.ts +49 -0
  117. package/esm/slangPass.js +22 -0
  118. package/esm/useEventCallback.d.ts +1 -0
  119. package/esm/useEventCallback.js +19 -0
  120. package/esm/useGraphicsCapabilities.d.ts +17 -0
  121. package/esm/useGraphicsCapabilities.js +38 -0
  122. package/esm/useRenderingBackend.d.ts +83 -0
  123. package/esm/useRenderingBackend.js +422 -0
  124. package/esm/useTabVisibilityRerender.d.ts +13 -0
  125. package/esm/useTabVisibilityRerender.js +39 -0
  126. package/esm/webgpuUtils.d.ts +23 -0
  127. package/esm/webgpuUtils.js +76 -0
  128. package/package.json +91 -0
  129. package/src/shaders/antialias.js.generated.ts +33 -0
  130. package/src/shaders/antialias.slang +112 -0
  131. package/src/shaders/capsule.generated.ts +4 -0
  132. package/src/shaders/capsule.slang +62 -0
  133. package/src/shaders/colorPack.slang +26 -0
  134. package/src/shaders/colorRampLut.generated.ts +4 -0
  135. package/src/shaders/colorRampLut.slang +51 -0
  136. package/src/shaders/coverageBand.generated.ts +6 -0
  137. package/src/shaders/coverageBand.slang +276 -0
  138. package/src/shaders/coverageBar.generated.ts +10 -0
  139. package/src/shaders/coverageBar.iface.generated.ts +172 -0
  140. package/src/shaders/coverageBar.slang +37 -0
  141. package/src/shaders/coverageIndicator.generated.ts +10 -0
  142. package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
  143. package/src/shaders/coverageIndicator.slang +61 -0
  144. package/src/shaders/coverageInterbase.generated.ts +10 -0
  145. package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
  146. package/src/shaders/coverageInterbase.slang +63 -0
  147. package/src/shaders/coverageMod.generated.ts +10 -0
  148. package/src/shaders/coverageMod.iface.generated.ts +211 -0
  149. package/src/shaders/coverageMod.slang +36 -0
  150. package/src/shaders/coverageSnp.generated.ts +10 -0
  151. package/src/shaders/coverageSnp.iface.generated.ts +211 -0
  152. package/src/shaders/coverageSnp.slang +52 -0
  153. package/src/shaders/diagonalGrid.slang +25 -0
  154. package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
  155. package/src/shaders/hpmath.js.generated.ts +47 -0
  156. package/src/shaders/hpmath.slang +226 -0
  157. package/src/shaders/pointGlyph.generated.ts +4 -0
  158. package/src/shaders/pointGlyph.js.generated.ts +9 -0
  159. package/src/shaders/pointGlyph.slang +93 -0
  160. package/src/shaders/rowRect.generated.ts +8 -0
  161. package/src/shaders/rowRect.js.generated.ts +17 -0
  162. package/src/shaders/rowRect.slang +160 -0
  163. package/src/shaders/scoreScale.js.generated.ts +51 -0
  164. package/src/shaders/scoreScale.slang +68 -0
@@ -0,0 +1,114 @@
1
+ import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase } from './renderingBackendBase.ts';
2
+ import type { BlockClipResult } from './blockClipUtils.ts';
3
+ import type { InstancePass } from './instancePass.ts';
4
+ import type { RenderBlock } from './renderBlock.ts';
5
+ import type { FrameDimensions, RenderingBackend } from './renderingBackendBase.ts';
6
+ export type { FrameDimensions } from './renderingBackendBase.ts';
7
+ /**
8
+ * Shared contract for per-region streamed GPU backends.
9
+ *
10
+ * The model owns the data (`rpcDataMap` / `laidOutDataMap`); the backend
11
+ * uploads what's needed to the GPU (`upload`, one region per key) and draws
12
+ * against a data map passed back through `renderBlocks` each frame.
13
+ *
14
+ * `UploadData` is what the upload-side autorun pushes per region;
15
+ * `RenderData` is what the render-side reads. They default to the same
16
+ * type — MAF is the lone case where they diverge (upload payload also
17
+ * carries a pre-encoded GPU buffer that the render side doesn't need).
18
+ *
19
+ * Renderers are stateless: they keep no per-region cache. Canvas2D backends
20
+ * stub `upload` / `release` as no-ops since they read everything from the
21
+ * `regions` map at render time. GPU backends use `upload` to push bytes into
22
+ * HAL buffers, and `release` to drop a departed region's buffers.
23
+ */
24
+ export interface PerRegionRenderingBackend<UploadData, RenderState, Block = RenderBlock, RenderData = UploadData> extends RenderingBackend {
25
+ upload(displayedRegionIndex: number, data: UploadData): void;
26
+ release(displayedRegionIndex: number): void;
27
+ /**
28
+ * Paint one frame and report whether real content reached the canvas. The
29
+ * render callback forwards this straight to `RenderLifecycleMixin`, which
30
+ * flips `canvasDrawn` (the loading scrim, `data-display-drawn`, every browser
31
+ * test's wait) only on `true` — so a tick that drew nothing must answer
32
+ * `false` rather than let a display assert doneness over an empty canvas.
33
+ *
34
+ * The two bases answer at different precision on purpose. GPU knows exactly
35
+ * ("a `drawRegion` ran", after the region-absent and offscreen-clip guards);
36
+ * Canvas2D delegates clipping to the plugin's `drawXxxBlocks` and so answers
37
+ * "some block had region data", which is the same for every case a display
38
+ * can distinguish and avoids re-running `clipBlockForCanvas` purely for a
39
+ * predicate.
40
+ */
41
+ renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
42
+ }
43
+ /**
44
+ * Canvas2D-side base for `PerRegionRenderingBackend` implementations. Inherits
45
+ * the `canvas` + 2D context (and no-op `dispose`) from
46
+ * `Canvas2DRenderingBackendBase`; stubs the upload/prune hooks the backend shape
47
+ * requires but Canvas2D doesn't need (everything comes through `draw` from the
48
+ * model's data map).
49
+ *
50
+ * `renderBlocks` is concrete: it runs `prepareCanvas` (DPR-aware backing-store
51
+ * sizing — symmetric to the GPU base owning `resize`/scissor) and then calls
52
+ * the abstract `draw`. Subclasses implement only `draw`, which delegates to the
53
+ * plugin's pure `drawXxxBlocks` function (also shared with SVG export). Keeping
54
+ * `prepareCanvas` in the base means a renderer can't forget it and render
55
+ * blurry on hi-DPI.
56
+ */
57
+ export declare abstract class Canvas2DPerRegionRenderingBackend<UploadData, RenderState extends FrameDimensions, Block extends RenderBlock = RenderBlock, RenderData = UploadData> extends Canvas2DRenderingBackendBase implements PerRegionRenderingBackend<UploadData, RenderState, Block, RenderData> {
58
+ upload(): void;
59
+ release(): void;
60
+ renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
61
+ /**
62
+ * Paint the visible blocks into `this.ctx` (already DPR-prepared). Delegates
63
+ * to the plugin's pure `drawXxxBlocks(ctx, regions, blocks, state)` function.
64
+ */
65
+ protected abstract draw(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): void;
66
+ }
67
+ /**
68
+ * GPU-side base for `PerRegionRenderingBackend` implementations. Inherits the
69
+ * `hal` reference, the pre-allocated uniform scratch `ArrayBuffer`, and the
70
+ * HAL-delegating `dispose` from `GpuRenderingBackendBase`; adds the shared
71
+ * `release` (delegates to HAL).
72
+ *
73
+ * `renderBlocks` is a concrete frame scaffold shared by every per-region GPU
74
+ * renderer: resize the backing store, `beginFrame`/`endFrame`, and per block
75
+ * compute the scissor clip + set scissor/viewport. The scaffold's invariants
76
+ * (paired begin/end, cleared scissor/viewport, the skip-absent-region and
77
+ * skip-offscreen guards) are owned here so a subclass can't forget them.
78
+ * Subclasses implement only `drawRegion`, which writes uniforms and issues
79
+ * draw passes for one already-clipped block.
80
+ */
81
+ export declare abstract class GpuPerRegionRenderingBackend<UploadData, RenderState extends FrameDimensions, Block extends RenderBlock = RenderBlock, RenderData = UploadData> extends GpuRenderingBackendBase implements PerRegionRenderingBackend<UploadData, RenderState, Block, RenderData> {
82
+ release(displayedRegionIndex: number): void;
83
+ /**
84
+ * The passes this backend fills from one region's payload, each carrying its
85
+ * own packer. Usually the same array handed to `createRenderingBackend` —
86
+ * minus any pass that draws off another's buffer (wiggle's line passes,
87
+ * canvas's chevron), which is registered but never uploaded to.
88
+ */
89
+ protected abstract regionPasses: InstancePass<UploadData>[];
90
+ /**
91
+ * Pack and upload every pass for one region.
92
+ *
93
+ * Concrete for the same reason `renderBlocks` below is: the invariants are
94
+ * the base's to keep. Each subclass used to write this, and each wrote the
95
+ * empty case differently — `if (count === 0) deleteRegion() else upload()`
96
+ * five times, an unconditional leading `deleteRegion()` once. Both spellings
97
+ * exist because a pass whose data went empty must not keep drawing its last
98
+ * buffer, and `uploadPass` gets that from the HAL: an empty pack deletes that
99
+ * pass's buffer, so a multi-pass backend clears exactly the passes that went
100
+ * empty instead of dropping the whole region and rebuilding it.
101
+ *
102
+ * Override only for a payload no packer can take — none does today.
103
+ */
104
+ upload(displayedRegionIndex: number, data: UploadData): void;
105
+ renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
106
+ /**
107
+ * Write uniforms and issue draw passes for one block whose scissor/viewport
108
+ * are already set to its clipped span. `clip` carries the HP-split bp range
109
+ * and pixel dimensions; `region` is the model's per-region data (used by
110
+ * renderers that read per-region uniforms like an outline color, ignored by
111
+ * those whose draw state is fully in the uploaded buffer).
112
+ */
113
+ protected abstract drawRegion(block: Block, clip: BlockClipResult, region: RenderData, state: RenderState): void;
114
+ }
@@ -0,0 +1,93 @@
1
+ import { clipBlock } from './blockClipUtils.js';
2
+ import { prepareCanvas } from './canvas2dUtils.js';
3
+ import { uploadPass } from './instancePass.js';
4
+ import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase, } from './renderingBackendBase.js';
5
+ /**
6
+ * Canvas2D-side base for `PerRegionRenderingBackend` implementations. Inherits
7
+ * the `canvas` + 2D context (and no-op `dispose`) from
8
+ * `Canvas2DRenderingBackendBase`; stubs the upload/prune hooks the backend shape
9
+ * requires but Canvas2D doesn't need (everything comes through `draw` from the
10
+ * model's data map).
11
+ *
12
+ * `renderBlocks` is concrete: it runs `prepareCanvas` (DPR-aware backing-store
13
+ * sizing — symmetric to the GPU base owning `resize`/scissor) and then calls
14
+ * the abstract `draw`. Subclasses implement only `draw`, which delegates to the
15
+ * plugin's pure `drawXxxBlocks` function (also shared with SVG export). Keeping
16
+ * `prepareCanvas` in the base means a renderer can't forget it and render
17
+ * blurry on hi-DPI.
18
+ */
19
+ export class Canvas2DPerRegionRenderingBackend extends Canvas2DRenderingBackendBase {
20
+ upload() { }
21
+ release() { }
22
+ renderBlocks(blocks, regions, state) {
23
+ prepareCanvas(this.canvas, this.ctx, state.canvasWidth, state.canvasHeight);
24
+ this.draw(blocks, regions, state);
25
+ return blocks.some(block => regions.has(block.displayedRegionIndex));
26
+ }
27
+ }
28
+ /**
29
+ * GPU-side base for `PerRegionRenderingBackend` implementations. Inherits the
30
+ * `hal` reference, the pre-allocated uniform scratch `ArrayBuffer`, and the
31
+ * HAL-delegating `dispose` from `GpuRenderingBackendBase`; adds the shared
32
+ * `release` (delegates to HAL).
33
+ *
34
+ * `renderBlocks` is a concrete frame scaffold shared by every per-region GPU
35
+ * renderer: resize the backing store, `beginFrame`/`endFrame`, and per block
36
+ * compute the scissor clip + set scissor/viewport. The scaffold's invariants
37
+ * (paired begin/end, cleared scissor/viewport, the skip-absent-region and
38
+ * skip-offscreen guards) are owned here so a subclass can't forget them.
39
+ * Subclasses implement only `drawRegion`, which writes uniforms and issues
40
+ * draw passes for one already-clipped block.
41
+ */
42
+ export class GpuPerRegionRenderingBackend extends GpuRenderingBackendBase {
43
+ release(displayedRegionIndex) {
44
+ this.hal.deleteRegion(displayedRegionIndex);
45
+ }
46
+ /**
47
+ * Pack and upload every pass for one region.
48
+ *
49
+ * Concrete for the same reason `renderBlocks` below is: the invariants are
50
+ * the base's to keep. Each subclass used to write this, and each wrote the
51
+ * empty case differently — `if (count === 0) deleteRegion() else upload()`
52
+ * five times, an unconditional leading `deleteRegion()` once. Both spellings
53
+ * exist because a pass whose data went empty must not keep drawing its last
54
+ * buffer, and `uploadPass` gets that from the HAL: an empty pack deletes that
55
+ * pass's buffer, so a multi-pass backend clears exactly the passes that went
56
+ * empty instead of dropping the whole region and rebuilding it.
57
+ *
58
+ * Override only for a payload no packer can take — none does today.
59
+ */
60
+ upload(displayedRegionIndex, data) {
61
+ for (const pass of this.regionPasses) {
62
+ uploadPass(this.hal, displayedRegionIndex, pass, data);
63
+ }
64
+ }
65
+ renderBlocks(blocks, regions, state) {
66
+ const { canvasWidth, canvasHeight } = state;
67
+ let painted = false;
68
+ // The scale the canvas ACTUALLY got, which is `getDpr()` until the backing
69
+ // store clamps and below it after. Read from the resize rather than the
70
+ // window, so a track dragged past the clamp draws at reduced resolution
71
+ // instead of asking for a viewport its target cannot hold.
72
+ const scale = this.hal.resize(canvasWidth, canvasHeight);
73
+ // Always pair beginFrame/endFrame so the canvas clears to transparent even
74
+ // when every block is skipped (e.g. all regions pruned by a density gate).
75
+ this.hal.beginFrame(0, 0, 0, 0);
76
+ for (const block of blocks) {
77
+ const region = regions.get(block.displayedRegionIndex);
78
+ if (region !== undefined) {
79
+ const clip = clipBlock(block, canvasWidth, canvasHeight, scale);
80
+ if (clip) {
81
+ this.hal.setScissor(clip.pxX, 0, clip.pxW, clip.pxH);
82
+ this.hal.setViewport(clip.pxX, 0, clip.pxW, clip.pxH);
83
+ this.drawRegion(block, clip, region, state);
84
+ painted = true;
85
+ }
86
+ }
87
+ }
88
+ this.hal.clearScissor();
89
+ this.hal.clearViewport();
90
+ this.hal.endFrame();
91
+ return painted;
92
+ }
93
+ }
@@ -0,0 +1,40 @@
1
+ export declare const MAX_RECOVERIES = 2;
2
+ export declare const RECOVERY_WINDOW_MS = 60000;
3
+ export type RecoveryVerdict = 'recover' | 'give-up';
4
+ /**
5
+ * The recovery budget a display spends re-initializing its rendering backend
6
+ * after a lost WebGL context or a lost WebGPU device.
7
+ *
8
+ * The budget is **windowed, not lifetime**. A context that resolves and then
9
+ * immediately re-loses has to keep climbing toward the cap — that is the flap
10
+ * the cap exists for, and it happens within seconds. But two unrelated losses
11
+ * an hour apart are not a flap, and a lifetime counter cannot tell them apart:
12
+ * it spends the second loss's budget on the first one's and leaves a
13
+ * long-running session permanently unable to auto-recover. Counting over a
14
+ * window separates the two without needing to know which kind of loss it saw.
15
+ *
16
+ * `now` is passed in rather than read, so the caller owns the clock and this
17
+ * stays testable without fake timers.
18
+ */
19
+ export declare class RecoveryBudget {
20
+ private count;
21
+ private lastAt;
22
+ private readonly max;
23
+ private readonly windowMs;
24
+ constructor(max?: number, windowMs?: number);
25
+ /**
26
+ * Record a loss and say whether to recover from it. Call this **only when
27
+ * about to act** on the answer — it is a mutation, and calling it
28
+ * speculatively (this hook's recovery effect runs on every render) would
29
+ * refresh the window forever and the count would never lapse.
30
+ */
31
+ record(now: number): RecoveryVerdict;
32
+ /** Which attempt the loss just recorded is — drives the backoff exponent. */
33
+ get attempt(): number;
34
+ /**
35
+ * Back to a clean slate, for the two events that mean the trouble is over: a
36
+ * genuine `webglcontextrestored` and a manual Retry. A successful re-init is
37
+ * deliberately NOT one of them — every flap contains one.
38
+ */
39
+ reset(): void;
40
+ }
@@ -0,0 +1,56 @@
1
+ // How many recoveries a display may attempt before it stops and leaves the
2
+ // manual Retry button, and the window those attempts are counted over.
3
+ export const MAX_RECOVERIES = 2;
4
+ export const RECOVERY_WINDOW_MS = 60_000;
5
+ /**
6
+ * The recovery budget a display spends re-initializing its rendering backend
7
+ * after a lost WebGL context or a lost WebGPU device.
8
+ *
9
+ * The budget is **windowed, not lifetime**. A context that resolves and then
10
+ * immediately re-loses has to keep climbing toward the cap — that is the flap
11
+ * the cap exists for, and it happens within seconds. But two unrelated losses
12
+ * an hour apart are not a flap, and a lifetime counter cannot tell them apart:
13
+ * it spends the second loss's budget on the first one's and leaves a
14
+ * long-running session permanently unable to auto-recover. Counting over a
15
+ * window separates the two without needing to know which kind of loss it saw.
16
+ *
17
+ * `now` is passed in rather than read, so the caller owns the clock and this
18
+ * stays testable without fake timers.
19
+ */
20
+ export class RecoveryBudget {
21
+ count = 0;
22
+ // "No loss yet" — any first `record` yields 1 down either branch, so nothing
23
+ // turns on the value, but -Infinity says it in a way a 0 read off the same
24
+ // clock as `now` does not.
25
+ lastAt = -Infinity;
26
+ max;
27
+ windowMs;
28
+ constructor(max = MAX_RECOVERIES, windowMs = RECOVERY_WINDOW_MS) {
29
+ this.max = max;
30
+ this.windowMs = windowMs;
31
+ }
32
+ /**
33
+ * Record a loss and say whether to recover from it. Call this **only when
34
+ * about to act** on the answer — it is a mutation, and calling it
35
+ * speculatively (this hook's recovery effect runs on every render) would
36
+ * refresh the window forever and the count would never lapse.
37
+ */
38
+ record(now) {
39
+ this.count = now - this.lastAt > this.windowMs ? 1 : this.count + 1;
40
+ this.lastAt = now;
41
+ return this.count > this.max ? 'give-up' : 'recover';
42
+ }
43
+ /** Which attempt the loss just recorded is — drives the backoff exponent. */
44
+ get attempt() {
45
+ return this.count;
46
+ }
47
+ /**
48
+ * Back to a clean slate, for the two events that mean the trouble is over: a
49
+ * genuine `webglcontextrestored` and a manual Retry. A successful re-init is
50
+ * deliberately NOT one of them — every flap contains one.
51
+ */
52
+ reset() {
53
+ this.count = 0;
54
+ this.lastAt = -Infinity;
55
+ }
56
+ }
@@ -0,0 +1,60 @@
1
+ import type { ObservableMap } from 'mobx';
2
+ export declare function isCheckedAtTheStore(map: ReadonlyMap<unknown, unknown>): boolean;
3
+ /**
4
+ * The volatile a display keys its per-region payloads by
5
+ * (`displayedRegionIndex` → result). Every one of them in tree is built with
6
+ * this, so "how is per-region data represented" has one answer — see
7
+ * ADR-060.
8
+ *
9
+ * **Shallow, and that follows from an invariant the codebase already states**:
10
+ * per-region values are freshly constructed and never mutated (agent-docs
11
+ * CLAUDE.md; backends diff by reference identity). Nothing inside an entry can
12
+ * therefore change, so the deep enhancer's field-level atoms can never fire —
13
+ * they are unreachable reactivity, not a safety margin. What they do cost is
14
+ * paid on every insert and every read:
15
+ *
16
+ * - **Insert.** `deepEnhancer` recursively rebuilds each payload as an
17
+ * observable object graph — the stored value is not the object the worker
18
+ * produced. A multi-wiggle region is one atom per field per source, so a
19
+ * thousand-sample track pays ~18k on every pan; MAF pays the whole set again
20
+ * for every cached region on every row reorder, since `placeFetchedRows`
21
+ * re-places them all.
22
+ * - **Read.** Each field access goes through `getObservablePropValue_`. Hot
23
+ * loops that hoist their typed arrays are fine, but the ones that don't were
24
+ * paying it per iteration.
25
+ *
26
+ * Typed arrays and class instances (Flatbush) pass through the deep enhancer
27
+ * untouched, so the arrays were never the cost — the objects holding them were.
28
+ *
29
+ * Coarser tracking is the only behavioral difference, and it is unobservable
30
+ * here: an entry is only ever replaced whole, so the keys atom and the entry's
31
+ * own atom fire on exactly the same `.set`/`.delete`/`.clear`.
32
+ *
33
+ * Not for maps of primitives (`groupMaxHeightOverrides`,
34
+ * `detectedModifications`), where the enhancer is a no-op, nor for UI state
35
+ * whose values are mutated in place.
36
+ *
37
+ * **Dev-only, every entry is checked on the way in.** A fetch RPC answers a
38
+ * payload or a `regionTooLarge` refusal, and every reader of this map is
39
+ * written for the payload — so a value that is not one is a fetch that did not
40
+ * happen, stored as though it had. There is no type to catch it: `onResult`
41
+ * receives whatever the RPC resolved, typed as the payload it was supposed to
42
+ * be, and the mock in the display harness resolved `undefined` for every
43
+ * un-stubbed method for as long as the harness existed. Six reactions across
44
+ * four packages then threw `Cannot read properties of undefined` on every run,
45
+ * inside autoruns MobX catches and logs — invisible until the reaction gate
46
+ * went in.
47
+ *
48
+ * Checked HERE because this is the one constructor all of them share, so the
49
+ * store is one place while the readers are hundreds, and because a payload
50
+ * caught at the `set` names the region and the display instead of surfacing as
51
+ * a TypeError in whichever getter happened to read it first.
52
+ *
53
+ * **`T extends object` is the same statement as the check, in the one place a
54
+ * type can make it.** The runtime predicate is "a non-null object", and
55
+ * `object` is that predicate spelled as a constraint — so a map cannot be
56
+ * *declared* as holding something the check would then report, which is the
57
+ * only way the two could come apart. It costs nothing: every payload in tree is
58
+ * an interface, an array, an intersection or a class.
59
+ */
60
+ export declare function regionDataMap<T extends object>(name: string): ObservableMap<number, T>;
@@ -0,0 +1,98 @@
1
+ import { observable } from 'mobx';
2
+ // Maps built here check themselves at the `set`, so the upload seam skips them
3
+ // and a bad entry is reported once, with the field named.
4
+ //
5
+ // Module state, and duplication-safe in the only way that matters here: two
6
+ // live copies of this package would each hold their own set, so a map built by
7
+ // one and installed through the other is checked twice rather than missed.
8
+ const checkedAtTheStore = new WeakSet();
9
+ export function isCheckedAtTheStore(map) {
10
+ return checkedAtTheStore.has(map);
11
+ }
12
+ /**
13
+ * The volatile a display keys its per-region payloads by
14
+ * (`displayedRegionIndex` → result). Every one of them in tree is built with
15
+ * this, so "how is per-region data represented" has one answer — see
16
+ * ADR-060.
17
+ *
18
+ * **Shallow, and that follows from an invariant the codebase already states**:
19
+ * per-region values are freshly constructed and never mutated (agent-docs
20
+ * CLAUDE.md; backends diff by reference identity). Nothing inside an entry can
21
+ * therefore change, so the deep enhancer's field-level atoms can never fire —
22
+ * they are unreachable reactivity, not a safety margin. What they do cost is
23
+ * paid on every insert and every read:
24
+ *
25
+ * - **Insert.** `deepEnhancer` recursively rebuilds each payload as an
26
+ * observable object graph — the stored value is not the object the worker
27
+ * produced. A multi-wiggle region is one atom per field per source, so a
28
+ * thousand-sample track pays ~18k on every pan; MAF pays the whole set again
29
+ * for every cached region on every row reorder, since `placeFetchedRows`
30
+ * re-places them all.
31
+ * - **Read.** Each field access goes through `getObservablePropValue_`. Hot
32
+ * loops that hoist their typed arrays are fine, but the ones that don't were
33
+ * paying it per iteration.
34
+ *
35
+ * Typed arrays and class instances (Flatbush) pass through the deep enhancer
36
+ * untouched, so the arrays were never the cost — the objects holding them were.
37
+ *
38
+ * Coarser tracking is the only behavioral difference, and it is unobservable
39
+ * here: an entry is only ever replaced whole, so the keys atom and the entry's
40
+ * own atom fire on exactly the same `.set`/`.delete`/`.clear`.
41
+ *
42
+ * Not for maps of primitives (`groupMaxHeightOverrides`,
43
+ * `detectedModifications`), where the enhancer is a no-op, nor for UI state
44
+ * whose values are mutated in place.
45
+ *
46
+ * **Dev-only, every entry is checked on the way in.** A fetch RPC answers a
47
+ * payload or a `regionTooLarge` refusal, and every reader of this map is
48
+ * written for the payload — so a value that is not one is a fetch that did not
49
+ * happen, stored as though it had. There is no type to catch it: `onResult`
50
+ * receives whatever the RPC resolved, typed as the payload it was supposed to
51
+ * be, and the mock in the display harness resolved `undefined` for every
52
+ * un-stubbed method for as long as the harness existed. Six reactions across
53
+ * four packages then threw `Cannot read properties of undefined` on every run,
54
+ * inside autoruns MobX catches and logs — invisible until the reaction gate
55
+ * went in.
56
+ *
57
+ * Checked HERE because this is the one constructor all of them share, so the
58
+ * store is one place while the readers are hundreds, and because a payload
59
+ * caught at the `set` names the region and the display instead of surfacing as
60
+ * a TypeError in whichever getter happened to read it first.
61
+ *
62
+ * **`T extends object` is the same statement as the check, in the one place a
63
+ * type can make it.** The runtime predicate is "a non-null object", and
64
+ * `object` is that predicate spelled as a constraint — so a map cannot be
65
+ * *declared* as holding something the check would then report, which is the
66
+ * only way the two could come apart. It costs nothing: every payload in tree is
67
+ * an interface, an array, an intersection or a class.
68
+ */
69
+ export function regionDataMap(
70
+ // the field this map is stored on, so a violation names the map rather than
71
+ // the thirteen that share this constructor. No default — an omitted name is
72
+ // the anonymous message this parameter exists to remove, and it would be
73
+ // reached by forgetting rather than by deciding.
74
+ name) {
75
+ const map = observable.map(undefined, { deep: false, name });
76
+ if (process.env.NODE_ENV !== 'production') {
77
+ checkedAtTheStore.add(map);
78
+ const set = map.set.bind(map);
79
+ map.set = (key, value) => {
80
+ // as `unknown`, because the claim that this is a `T` is the thing in
81
+ // doubt — checking it against its own declared type narrows to `never`
82
+ // and checks nothing
83
+ const stored = value;
84
+ if (typeof stored !== 'object' || stored === null) {
85
+ // `console.error`, never `throw`: the store runs inside the fetch's own
86
+ // result handler, where a throw is caught and reported as a failed
87
+ // region — which would hide the violation being reported.
88
+ console.error(`[jbrowse display contract] ${name}: region ${key} stored ` +
89
+ `\`${stored === null ? 'null' : typeof stored}\`, not a ` +
90
+ 'payload. A fetch RPC answers a payload or a regionTooLarge ' +
91
+ 'refusal; storing anything else leaves every reader of this map ' +
92
+ 'reading through it.');
93
+ }
94
+ return set(key, value);
95
+ };
96
+ }
97
+ return map;
98
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A region's genomic span paired with its on-screen pixel span. The base
3
+ * geometry every backend works in: GPU clip math (`clipBlock`), Canvas2D clip
4
+ * (`clipBlockForCanvas`), the bp→px mapper (`makeBpMapper`), and canvas
5
+ * SVG/label/peptide positioning all consume this shape.
6
+ */
7
+ export interface BpRegionBounds {
8
+ start: number;
9
+ end: number;
10
+ screenStartPx: number;
11
+ screenEndPx: number;
12
+ reversed?: boolean;
13
+ }
14
+ /**
15
+ * A `BpRegionBounds` tagged with the `displayedRegionIndex` that joins it to
16
+ * `rpcDataMap` / HAL buffers. `reversed` is resolved (always present) once a
17
+ * block reaches the render path.
18
+ */
19
+ export interface RenderBlock extends BpRegionBounds {
20
+ displayedRegionIndex: number;
21
+ reversed: boolean;
22
+ }
23
+ export declare function buildRenderBlocks(regions: (BpRegionBounds & {
24
+ displayedRegionIndex: number;
25
+ })[]): RenderBlock[];
@@ -0,0 +1,10 @@
1
+ export function buildRenderBlocks(regions) {
2
+ return regions.map(r => ({
3
+ displayedRegionIndex: r.displayedRegionIndex,
4
+ start: r.start,
5
+ end: r.end,
6
+ screenStartPx: r.screenStartPx,
7
+ screenEndPx: r.screenEndPx,
8
+ reversed: r.reversed ?? false,
9
+ }));
10
+ }
@@ -0,0 +1,71 @@
1
+ import type { GpuHal } from './hal/types.ts';
2
+ /**
3
+ * Minimum render-state shape a frame scaffold needs: the CSS-pixel canvas
4
+ * dimensions used to size the backing store, and — on the per-region side —
5
+ * to clip each block.
6
+ *
7
+ * Here rather than beside either family because both bound their render state
8
+ * by it: `GpuPerRegionRenderingBackend.renderBlocks` and
9
+ * `GpuGlobalRenderingBackend.render` open with the same `hal.resize`, and the
10
+ * two Canvas2D bases with the same `prepareCanvas`.
11
+ */
12
+ export interface FrameDimensions {
13
+ canvasWidth: number;
14
+ canvasHeight: number;
15
+ }
16
+ /**
17
+ * What every rendering backend has, whatever its upload shape — the three
18
+ * shape contracts (`PerRegionRenderingBackend`, `GlobalRenderingBackend`,
19
+ * `KeyedRenderingBackend`) all extend this, and `useRenderingBackend` is
20
+ * bounded by it.
21
+ *
22
+ * `setErrorHandler` is here rather than optional at the hook because it was
23
+ * optional at the hook, and the three backends that then went without it —
24
+ * alignments, dotplot and multi-LGV synteny, which allocate the largest vertex
25
+ * buffers in the app — were the three whose over-limit allocations reached
26
+ * nobody: the HAL reported "too much data to render on this GPU, zoom in", the
27
+ * reporter's handler was null, and the view painted blank with a console line.
28
+ * The displays had their error banners built and wired the whole time.
29
+ *
30
+ * Extending `GpuRenderingBackendBase` or `Canvas2DRenderingBackendBase` below
31
+ * satisfies it, which is the intended way to get it.
32
+ */
33
+ export interface RenderingBackend {
34
+ /**
35
+ * Route a HAL over-limit allocation to the display's `renderError`. Wired by
36
+ * `useRenderingBackend` once the backend is live; a no-op on Canvas2D, which
37
+ * allocates no GPU resources.
38
+ */
39
+ setErrorHandler(handler: (error: Error) => void): void;
40
+ dispose(): void;
41
+ }
42
+ /**
43
+ * Shared GPU-side state for every GPU rendering backend — both per-region
44
+ * (`GpuPerRegionRenderingBackend`) and monolithic (`GpuGlobalRenderingBackend`).
45
+ * Owns the `hal` reference and a pre-allocated uniform scratch `ArrayBuffer`
46
+ * reused across frames to avoid per-frame GC churn. `dispose()` delegates to
47
+ * the HAL. Subclasses add only their upload/render shape.
48
+ */
49
+ export declare abstract class GpuRenderingBackendBase {
50
+ protected hal: GpuHal;
51
+ protected uniformData: ArrayBuffer;
52
+ constructor(hal: GpuHal, uniformByteSize: number);
53
+ setErrorHandler(handler: (error: Error) => void): void;
54
+ dispose(): void;
55
+ }
56
+ /**
57
+ * Shared Canvas2D-side state for every Canvas2D rendering backend — both
58
+ * per-region (`Canvas2DPerRegionRenderingBackend`) and monolithic
59
+ * (`Canvas2DGlobalRenderingBackend`). Owns the `canvas` and its 2D context
60
+ * (constructor throws if unavailable). `dispose()` is a no-op since Canvas2D
61
+ * holds no GPU resources. Subclasses add only their render shape and stub the
62
+ * upload/prune hooks their interface requires (everything flows through the
63
+ * model's data map at render time).
64
+ */
65
+ export declare abstract class Canvas2DRenderingBackendBase {
66
+ protected canvas: HTMLCanvasElement;
67
+ protected ctx: CanvasRenderingContext2D;
68
+ constructor(canvas: HTMLCanvasElement);
69
+ setErrorHandler(_handler: (error: Error) => void): void;
70
+ dispose(): void;
71
+ }
@@ -0,0 +1,50 @@
1
+ import { acquireCanvas2D } from './canvasContext.js';
2
+ /**
3
+ * Shared GPU-side state for every GPU rendering backend — both per-region
4
+ * (`GpuPerRegionRenderingBackend`) and monolithic (`GpuGlobalRenderingBackend`).
5
+ * Owns the `hal` reference and a pre-allocated uniform scratch `ArrayBuffer`
6
+ * reused across frames to avoid per-frame GC churn. `dispose()` delegates to
7
+ * the HAL. Subclasses add only their upload/render shape.
8
+ */
9
+ export class GpuRenderingBackendBase {
10
+ hal;
11
+ uniformData;
12
+ constructor(hal, uniformByteSize) {
13
+ this.hal = hal;
14
+ this.uniformData = new ArrayBuffer(uniformByteSize);
15
+ }
16
+ // Forward OOM / over-limit allocation failures from the HAL to the display's
17
+ // renderError. Wired by useRenderingBackend once the backend is live.
18
+ setErrorHandler(handler) {
19
+ this.hal.setErrorHandler(handler);
20
+ }
21
+ dispose() {
22
+ this.hal.dispose();
23
+ }
24
+ }
25
+ /**
26
+ * Shared Canvas2D-side state for every Canvas2D rendering backend — both
27
+ * per-region (`Canvas2DPerRegionRenderingBackend`) and monolithic
28
+ * (`Canvas2DGlobalRenderingBackend`). Owns the `canvas` and its 2D context
29
+ * (constructor throws if unavailable). `dispose()` is a no-op since Canvas2D
30
+ * holds no GPU resources. Subclasses add only their render shape and stub the
31
+ * upload/prune hooks their interface requires (everything flows through the
32
+ * model's data map at render time).
33
+ */
34
+ export class Canvas2DRenderingBackendBase {
35
+ canvas;
36
+ ctx;
37
+ constructor(canvas) {
38
+ // Canvas2D is the ladder's last rung, so this is where a re-init on a reused
39
+ // element lands — and a bare "not available" sends the reader looking for a
40
+ // missing browser feature instead of the committed context actually in the
41
+ // way. `acquireCanvas2D` names which (canvasContext.ts).
42
+ this.ctx = acquireCanvas2D(canvas);
43
+ this.canvas = canvas;
44
+ }
45
+ // Canvas2D allocates no GPU resources, so there is no OOM channel to forward.
46
+ // Present for symmetry with the GPU base so useRenderingBackend can wire both
47
+ // uniformly.
48
+ setErrorHandler(_handler) { }
49
+ dispose() { }
50
+ }
@@ -0,0 +1,4 @@
1
+ export declare function aaPx(dpr: number): number;
2
+ export declare function aaHalfPx(dpr: number): number;
3
+ export declare function aaRamp(signedInk: number, widthPx: number): number;
4
+ export declare function edgeCoverage(signedInkCssPx: number, dpr: number): number;
@@ -0,0 +1,25 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from antialias.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+ // Scalar twins of antialias.slang, transliterated from slangc's WGSL so
4
+ // the Canvas2D and SVG paths run the shader's own math. See adr-051.
5
+ function _clamp(x, lo, hi) {
6
+ return _min(_max(x, lo), hi);
7
+ }
8
+ function _max(a, b) {
9
+ return b > a || Number.isNaN(a) ? b : a;
10
+ }
11
+ function _min(a, b) {
12
+ return b < a || Number.isNaN(a) ? b : a;
13
+ }
14
+ export function aaPx(dpr) {
15
+ return (1.0 / _max(dpr, 1.0));
16
+ }
17
+ export function aaHalfPx(dpr) {
18
+ return (0.5 * aaPx(dpr));
19
+ }
20
+ export function aaRamp(signedInk, widthPx) {
21
+ return _clamp(((signedInk / widthPx) + 0.5), 0.0, 1.0);
22
+ }
23
+ export function edgeCoverage(signedInkCssPx, dpr) {
24
+ return aaRamp(signedInkCssPx, aaPx(dpr));
25
+ }
@@ -0,0 +1 @@
1
+ export declare const CAPSULE_MIN_LEN_PX = 0.0001;