@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,97 @@
1
+ /**
2
+ * `loading` is the only display-specific axis: MultiRegion passes
3
+ * "!isReady || stale viewport", Global passes "fetch in flight", sequence
4
+ * layers a zoom gate on top. The terminal precedence above it is identical for
5
+ * every display, so it lives here — pass the display's loading condition and
6
+ * get the resolved phase back.
7
+ *
8
+ * `loading` is a **thunk**, evaluated only after the three terminal states are
9
+ * ruled out. This is load-bearing for MobX, not a micro-optimization: the
10
+ * loading condition typically reads the containing view (`visibleRegions`,
11
+ * `loadedRegions`, …), a large reactive dependency set. Evaluating it eagerly
12
+ * would make every reader of `displayPhase` subscribe to all of it even while a
13
+ * banner is up — and `DisplayChrome`'s observer, re-firing on that churn during
14
+ * a terminal state, then fails to commit the banner subtree to the DOM (the
15
+ * React-reconciliation hazard documented in DisplayChrome.tsx). Short-circuiting
16
+ * keeps the tracked set to just the terminal flags when one is active, matching
17
+ * the old direct-read early-returns.
18
+ */
19
+ export function computeDisplayPhase({ renderError, regionTooLarge, error }, loading) {
20
+ return renderError
21
+ ? 'renderError'
22
+ : computeDisplayStatusPhase({ regionTooLarge, error }, loading);
23
+ }
24
+ /**
25
+ * The `loading` term itself — the one axis `computeDisplayPhase` leaves to its
26
+ * caller — expressed once for both LGV display families.
27
+ *
28
+ * It was two hand-written expressions, and they had already drifted apart in
29
+ * three ways that were equivalent only by accident: the per-region family spelled
30
+ * the cancel term out by hand (`!isReady || … || fetchCanceled`, over a bare
31
+ * `isLoading`) rather than reading `isLoadingOrCanceled`, and each family carried
32
+ * exactly one of the two suppression hooks — `fetchInert` on per-region,
33
+ * `rendersCanvas` on global — so a display in the other family could express its
34
+ * case only by overriding `displayPhase`, which is the thing both hooks exist to
35
+ * prevent. Single-sourcing the expression is the same argument the precedence
36
+ * above already won, one level down: a term added here reaches every display,
37
+ * instead of reaching whichever family the author happened to be reading.
38
+ *
39
+ * The four inputs all now live on a mixin every foundation composes —
40
+ * `fetchInert` / `isLoadingOrCanceled` on `FetchMixin`, `rendersCanvas` /
41
+ * `canvasDrawn` on `RenderLifecycleMixin` — so `foundationDisplayPhase` passes
42
+ * the model straight through and the only thing a family supplies is
43
+ * `viewportCurrent`: per-region its staleness predicate, global `() => true` (it
44
+ * keeps the last frame up through a refetch rather than scrimming). Two hooks
45
+ * used to be hard-coded per family instead, which is how the second drift
46
+ * happened after the first was fixed — LD needed `fetchInert` and the
47
+ * global family did not have it.
48
+ *
49
+ * `viewportCurrent` is a **thunk** for the same MobX reason `computeDisplayPhase`'s
50
+ * `loading` is: it reads the containing view (`visibleRegions`, `loadedRegions`),
51
+ * while the flag inputs above are on the display itself. Short-circuiting keeps a
52
+ * suppressed or already-loading display from subscribing to viewport churn.
53
+ *
54
+ * `hostMounted` is a thunk for the same reason, and gates the **pre-first-paint
55
+ * term only**. A container that mounts a view's body lazily — `ViewContainer`
56
+ * does, to stay under the WebGL2 context ceiling — leaves an off-screen view
57
+ * with no canvas, so `markCanvasDrawn` is never called and `!canvasDrawn` stays
58
+ * true with nothing able to resolve it. That parked `[data-app-phase="ready"]`
59
+ * for the whole app on a view the user could not see, and every capture and e2e
60
+ * gate waiting on it burned its full timeout.
61
+ *
62
+ * It is a required flag on every view and on the duck-typed host contracts, not
63
+ * an optional one defaulted here: a stand-in that forgot it would silently
64
+ * excuse the first paint and let a readiness gate fire over a view that had not
65
+ * started.
66
+ *
67
+ * The fetch terms deliberately stay live while unmounted: an off-screen display
68
+ * still fetches, so reporting it idle would let a readiness gate fire over work
69
+ * genuinely in flight — and during cold load `visible` is false until the
70
+ * IntersectionObserver's first callback, which is exactly when a display must
71
+ * not flash `ready`. Only the paint it will never do is excused.
72
+ */
73
+ export function computeLoadingTerm({ fetchInert, viewportEmpty, isLoadingOrCanceled, rendersCanvas, canvasDrawn, }, viewportCurrent, hostMounted = () => true) {
74
+ return (!fetchInert &&
75
+ !viewportEmpty &&
76
+ (isLoadingOrCanceled ||
77
+ (rendersCanvas && hostMounted() && !canvasDrawn) ||
78
+ !viewportCurrent()));
79
+ }
80
+ /**
81
+ * The same ranking minus `renderError`, for a display with no rendering backend
82
+ * to fail (arc's main-thread SVG). `computeDisplayPhase` delegates here rather
83
+ * than restating the tail, so there is still exactly one place the order lives —
84
+ * and the narrower return type is what lets `DisplayStatusChrome` accept a
85
+ * backend-less display without either a cast or a dead branch.
86
+ *
87
+ * `loading` is a thunk here for the same reason as above.
88
+ */
89
+ export function computeDisplayStatusPhase({ regionTooLarge, error }, loading) {
90
+ return regionTooLarge
91
+ ? 'tooLarge'
92
+ : error
93
+ ? 'error'
94
+ : loading()
95
+ ? 'loading'
96
+ : 'ready';
97
+ }
@@ -0,0 +1,90 @@
1
+ import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase } from './renderingBackendBase.ts';
2
+ import type { FrameDimensions, RenderingBackend } from './renderingBackendBase.ts';
3
+ /**
4
+ * Shared contract for monolithic GPU backends (HiC, LD, multi-variant
5
+ * matrix) — displays with no region partitioning. One payload under the
6
+ * `data` key, one draw per frame. Render receives the data directly so
7
+ * Canvas2D backends stay stateless; GPU backends use `upload` to push bytes
8
+ * into a HAL buffer and ignore the `data` arg at render time.
9
+ *
10
+ * A display with a second upload beside the payload — HiC's and LD's colour
11
+ * ramp texture, derived from a setting rather than the fetch — widens `Key`
12
+ * and `Cell` so `upload` takes both cells and tells them apart, while `render`
13
+ * is still handed the payload alone.
14
+ */
15
+ export interface GlobalRenderingBackend<UploadData, RenderState, Key extends string = 'data', Cell = UploadData> extends RenderingBackend {
16
+ upload(key: Key, data: Cell): void;
17
+ release(key: Key): void;
18
+ /**
19
+ * Paint one frame and report whether real content reached the canvas — the
20
+ * monolithic twin of `PerRegionRenderingBackend.renderBlocks`, and it answers
21
+ * for the same reason. `RenderLifecycleMixin` flips `canvasDrawn` (the loading
22
+ * scrim, `data-display-drawn`, every browser test's wait) only on `true`, so a
23
+ * tick that drew nothing must say so.
24
+ *
25
+ * **This used to return `void`, and the answer was the display's to give.**
26
+ * Each of the three wrote the same six lines — "data is here, so return
27
+ * `true`" — beside a renderer whose own guard was strictly narrower: a color
28
+ * ramp that had not arrived, a zero instance count, an empty HAL buffer. Two
29
+ * expressions of one fact, and the one that could see what actually happened
30
+ * was not the one being asked. LD's model even carried a comment explaining
31
+ * the workaround, which is the usual sign that a signature is wrong rather
32
+ * than a caller.
33
+ */
34
+ render(data: UploadData | null, state: RenderState): boolean;
35
+ }
36
+ /**
37
+ * Canvas2D-side base for `GlobalRenderingBackend` implementations. Inherits the
38
+ * `canvas` + 2D context (and no-op `dispose`) from
39
+ * `Canvas2DRenderingBackendBase`; stubs `upload` (no-op — data flows through
40
+ * `render`).
41
+ *
42
+ * `render` is concrete and runs `prepareCanvas` (DPR-aware backing-store sizing)
43
+ * before handing off to the abstract `draw`, exactly as
44
+ * `Canvas2DPerRegionRenderingBackend` does — so a renderer here can no more
45
+ * forget it and paint blurry on hi-DPI than one over there can. All three
46
+ * subclasses opened with that same call and none of them had to.
47
+ *
48
+ * A null payload still prepares (and so clears) the canvas, then skips the
49
+ * draw: that is the frame where a refetch is in flight and the old picture must
50
+ * not stay up.
51
+ */
52
+ export declare abstract class Canvas2DGlobalRenderingBackend<UploadData, RenderState extends FrameDimensions, Key extends string = 'data', Cell = UploadData> extends Canvas2DRenderingBackendBase implements GlobalRenderingBackend<UploadData, RenderState, Key, Cell> {
53
+ upload(_key: Key, _cell: Cell): void;
54
+ release(_key: Key): void;
55
+ render(data: UploadData | null, state: RenderState): boolean;
56
+ /**
57
+ * Paint `data` into `this.ctx` (already DPR-prepared) and answer whether
58
+ * anything reached it. Delegates to the plugin's pure `drawXxxBlocks`
59
+ * function, the one SVG export shares.
60
+ */
61
+ protected abstract draw(data: UploadData, state: RenderState): boolean;
62
+ }
63
+ /**
64
+ * GPU-side base for `GlobalRenderingBackend` implementations. Inherits the
65
+ * `hal` reference, the pre-allocated uniform scratch buffer, and the
66
+ * HAL-delegating `dispose` from `GpuRenderingBackendBase`. Subclasses implement
67
+ * `upload` and `draw`.
68
+ *
69
+ * `render` is the frame scaffold the per-region base has always had and this
70
+ * one did not: resize the backing store, `beginFrame`/`endFrame`, and hand the
71
+ * middle to `draw`. **The pairing is the point.** `beginFrame` is what clears
72
+ * the canvas, so a frame that draws nothing still has to open and close one or
73
+ * the last picture stays up — and with each subclass writing its own scaffold,
74
+ * that invariant held three times by hand.
75
+ *
76
+ * `release` is a no-op by default: a payload leaving the map is answered by
77
+ * `render` being handed `null`, and `beginFrame` clears the last picture. A
78
+ * subclass whose buffers are worth freeing early overrides it.
79
+ */
80
+ export declare abstract class GpuGlobalRenderingBackend<UploadData, RenderState extends FrameDimensions, Key extends string = 'data', Cell = UploadData> extends GpuRenderingBackendBase implements GlobalRenderingBackend<UploadData, RenderState, Key, Cell> {
81
+ abstract upload(key: Key, data: Cell): void;
82
+ release(): void;
83
+ render(data: UploadData | null, state: RenderState): boolean;
84
+ /**
85
+ * Write uniforms and issue the draw passes for one frame, inside an already-
86
+ * open `beginFrame`/`endFrame`. Return whether anything was drawn — a buffer
87
+ * the upload has not filled yet is the ordinary reason not to.
88
+ */
89
+ protected abstract draw(data: UploadData, state: RenderState): boolean;
90
+ }
@@ -0,0 +1,53 @@
1
+ import { prepareCanvas } from './canvas2dUtils.js';
2
+ import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase, } from './renderingBackendBase.js';
3
+ /**
4
+ * Canvas2D-side base for `GlobalRenderingBackend` implementations. Inherits the
5
+ * `canvas` + 2D context (and no-op `dispose`) from
6
+ * `Canvas2DRenderingBackendBase`; stubs `upload` (no-op — data flows through
7
+ * `render`).
8
+ *
9
+ * `render` is concrete and runs `prepareCanvas` (DPR-aware backing-store sizing)
10
+ * before handing off to the abstract `draw`, exactly as
11
+ * `Canvas2DPerRegionRenderingBackend` does — so a renderer here can no more
12
+ * forget it and paint blurry on hi-DPI than one over there can. All three
13
+ * subclasses opened with that same call and none of them had to.
14
+ *
15
+ * A null payload still prepares (and so clears) the canvas, then skips the
16
+ * draw: that is the frame where a refetch is in flight and the old picture must
17
+ * not stay up.
18
+ */
19
+ export class Canvas2DGlobalRenderingBackend extends Canvas2DRenderingBackendBase {
20
+ upload(_key, _cell) { }
21
+ release(_key) { }
22
+ render(data, state) {
23
+ prepareCanvas(this.canvas, this.ctx, state.canvasWidth, state.canvasHeight);
24
+ return data !== null && this.draw(data, state);
25
+ }
26
+ }
27
+ /**
28
+ * GPU-side base for `GlobalRenderingBackend` implementations. Inherits the
29
+ * `hal` reference, the pre-allocated uniform scratch buffer, and the
30
+ * HAL-delegating `dispose` from `GpuRenderingBackendBase`. Subclasses implement
31
+ * `upload` and `draw`.
32
+ *
33
+ * `render` is the frame scaffold the per-region base has always had and this
34
+ * one did not: resize the backing store, `beginFrame`/`endFrame`, and hand the
35
+ * middle to `draw`. **The pairing is the point.** `beginFrame` is what clears
36
+ * the canvas, so a frame that draws nothing still has to open and close one or
37
+ * the last picture stays up — and with each subclass writing its own scaffold,
38
+ * that invariant held three times by hand.
39
+ *
40
+ * `release` is a no-op by default: a payload leaving the map is answered by
41
+ * `render` being handed `null`, and `beginFrame` clears the last picture. A
42
+ * subclass whose buffers are worth freeing early overrides it.
43
+ */
44
+ export class GpuGlobalRenderingBackend extends GpuRenderingBackendBase {
45
+ release() { }
46
+ render(data, state) {
47
+ this.hal.resize(state.canvasWidth, state.canvasHeight);
48
+ this.hal.beginFrame(0, 0, 0, 0);
49
+ const painted = data !== null && this.draw(data, state);
50
+ this.hal.endFrame();
51
+ return painted;
52
+ }
53
+ }
@@ -0,0 +1,91 @@
1
+ interface GpuDeviceCell {
2
+ device: GPUDevice | null;
3
+ /** Serializes concurrent calls during async init and after recovery. */
4
+ devicePromise: Promise<GPUDevice | null> | null;
5
+ /**
6
+ * Proof that this machine's WebGPU works, set the first time a device is
7
+ * acquired. It is what makes a failed acquisition readable, because the two
8
+ * causes are indistinguishable at the call site and want opposite handling:
9
+ *
10
+ * - Before it, failure means "no WebGPU here" — the ordinary path on most
11
+ * hardware. Cache it, so every later backend skips the rung for free.
12
+ * - After it, failure means the GPU stack has not come back up yet. The
13
+ * re-init that follows `device.lost` asks for an adapter within a frame of
14
+ * the loss, and on a sleep/wake or a driver reset that is exactly when
15
+ * `requestAdapter` still declines. So this is not a rare race — it is the
16
+ * expected timing of the one path that re-acquires.
17
+ *
18
+ * Caching the second kind is what `createGpuHal` then reads as "no WebGPU on
19
+ * this machine" (its own words), silently pinning the whole page to WebGL2
20
+ * until a reload. So past this flag a failure is retried and never cached.
21
+ */
22
+ hadDevice: boolean;
23
+ deviceLostListeners: Set<() => void>;
24
+ /**
25
+ * Stays `string | null` rather than {@link GpuOverride} because the cell is
26
+ * shared with other copies of this module, including older ones whose
27
+ * `setGpuOverride` took any string without checking. Everything that reads it
28
+ * treats an unrecognized value as no pin.
29
+ *
30
+ * @see setGpuOverride
31
+ */
32
+ gpuOverride: string | null;
33
+ }
34
+ declare global {
35
+ var __jbrowseRenderCoreGpuDeviceV1: GpuDeviceCell | undefined;
36
+ }
37
+ export declare function onDeviceLost(listener: () => void): () => void;
38
+ /**
39
+ * The renderers `?renderer=` can pin the page to. `canvas` is a spelling of
40
+ * `canvas2d` kept because it is in the wild.
41
+ *
42
+ * A pin means *only* that rung — `createGpuHal` does not fall past it. That is
43
+ * the whole point of the flag: it exists to compare backends, and a comparison
44
+ * whose subject silently substitutes itself is worse than no comparison. The
45
+ * failure is visible instead, as a `renderError` carrying the pin that could
46
+ * not be honored, with the banner's "disable GPU" as the way out.
47
+ */
48
+ export declare const GPU_OVERRIDES: readonly ['webgpu', 'webgl', 'canvas2d', 'canvas'];
49
+ export type GpuOverride = (typeof GPU_OVERRIDES)[number];
50
+ /**
51
+ * Pin the page to a renderer, or clear the pin with `null`. Page-wide by
52
+ * design and shared across every copy of this module — see {@link GpuDeviceCell}.
53
+ *
54
+ * Takes a bare `string` because both callers hand it one straight off a URL
55
+ * (`?renderer=` in jbrowse-web's `InitialLoad`, the same param fed by desktop's
56
+ * `--renderer` flag), so validating here is validating once at the only place
57
+ * an unchecked value enters. An unrecognized one clears the pin and says so:
58
+ * before this, `?renderer=WebGL` or a typo was indistinguishable from passing
59
+ * nothing at all, which is the one outcome a person debugging a renderer must
60
+ * not silently get.
61
+ */
62
+ export declare function setGpuOverride(value: string | null): void;
63
+ export declare function getGpuOverride(): string | null;
64
+ /**
65
+ * Whether the GPU is off for the whole page — either pinned by `?renderer=` at
66
+ * startup or switched off from a display's GPU-error banner after an
67
+ * unrecoverable WebGL context loss. `createGpuHal` returns null in that case, so
68
+ * every backend built from then on is the Canvas2D one.
69
+ *
70
+ * Note this is narrower than "a pin is in force": `webgl` and `webgpu` are pins
71
+ * too, and both leave GPU rendering on. The banner reads this to decide whether
72
+ * to offer its escape, so it must stay the Canvas2D question specifically.
73
+ */
74
+ export declare function isGpuRenderingDisabled(): boolean;
75
+ /**
76
+ * Reset the shared singleton state. For use in tests only — clears `device` and
77
+ * `devicePromise` so the next `getGpuDevice()` call starts fresh rather than
78
+ * returning the cached (possibly null) promise from a previous test.
79
+ *
80
+ * It drops references and deliberately does not destroy the device, so this is
81
+ * not the half of a pair that a `destroyGpuDevice()` completes. See the comment
82
+ * above `onDeviceLost` for what destroying one costs.
83
+ *
84
+ * Resets the cell's fields rather than replacing the cell, so a second copy of
85
+ * this module that captured it still sees the reset. `gpuOverride` is left
86
+ * alone: it is set by the host and by individual tests, and clearing it here
87
+ * would make the reset mean two different things.
88
+ */
89
+ export declare function resetGpuDeviceForTests(): void;
90
+ export declare function getGpuDevice(): Promise<GPUDevice | null>;
91
+ export {};
@@ -0,0 +1,277 @@
1
+ /// <reference types="@webgpu/types" />
2
+ /**
3
+ * The page-wide GPU state, and the one thing in this package that **must not be
4
+ * duplicated** — so it is anchored on `globalThis` rather than held in module
5
+ * locals, and every copy of this module on the page shares one cell.
6
+ *
7
+ * The rest of render-core is duplication-safe: classes, mixins and geometry
8
+ * helpers, all of which a second copy can own its own instances of. This is
9
+ * not. `device` is one physical GPU device, and `gpuOverride` is a *page-wide*
10
+ * policy written by the host — `?renderer=` at startup, and the "disable GPU"
11
+ * button on the context-loss banner, which `packages/render-core/CLAUDE.md`
12
+ * names as the only sanctioned fallback path once a backend is built.
13
+ *
14
+ * A statically-bundled plugin is the case this exists for. ADR-030 makes the
15
+ * GPU surface static-import-only, so a third-party display carries its own copy
16
+ * of this module by design; with module locals its `gpuOverride` would start
17
+ * null and stay null, and the user who pins `?renderer=canvas2d` or clicks
18
+ * "disable GPU" after a crash would watch the plugin ignore both and take
19
+ * WebGPU anyway. Sharing the cell fixes that without putting anything in the
20
+ * frozen `ReExports` ABI — which is the trade ADR-030 declined, and rightly.
21
+ *
22
+ * **The shape is a cross-version contract.** Copies at different render-core
23
+ * versions meet here, and whichever loads first wins the initialization, so a
24
+ * field added later would read `undefined` on a cell built by an older copy.
25
+ * Changing this interface therefore means bumping the `V1` in the global's
26
+ * name, which costs the sharing between the two versions — the point of the
27
+ * suffix is that losing it is a decision rather than a silent misread. Reset in
28
+ * place (see `resetGpuDeviceForTests`); never reassign the cell itself, or
29
+ * copies holding the old object diverge from the new one.
30
+ */
31
+ const cell = (globalThis.__jbrowseRenderCoreGpuDeviceV1 ??= {
32
+ device: null,
33
+ devicePromise: null,
34
+ hadDevice: false,
35
+ deviceLostListeners: new Set(),
36
+ gpuOverride: null,
37
+ });
38
+ // Enough to outlast a wake-up, and only ever spent once `hadDevice` is set — a
39
+ // machine without WebGPU declines on the first ask and waits for nothing.
40
+ const REACQUIRE_TRIES = 3;
41
+ const REACQUIRE_DELAY_MS = 700;
42
+ // Nothing here calls `device.destroy()`, and nothing should. It is not an
43
+ // omission.
44
+ //
45
+ // Destroying a device that has presented frames stops the browser delivering
46
+ // animation frames to the whole *tab*, permanently: the tab still reports
47
+ // `visible`, the browser stays responsive, and a reload lands in the same dead
48
+ // tab because the damage outlives the document. Only a new tab clears it.
49
+ // Measured on Firefox Nightly / Linux in a sibling WebGPU app (phosphene,
50
+ // `docs/adr/0004-never-destroy-a-presenting-device.md`, repro
51
+ // `scripts/devicetear.mjs`), with the controls that pin the mechanism: creating
52
+ // and destroying devices with no canvas is free, so is configuring a swapchain
53
+ // and destroying without presenting. Presenting and then destroying is the one
54
+ // arm that kills the tab. So this is about pulling a live swapchain out from
55
+ // under the compositor, not about devices being scarce.
56
+ //
57
+ // Two edits reintroduce it, and both read as tidying up rather than as a
58
+ // regression, which is why this comment is here instead of a code guard:
59
+ //
60
+ // - a `destroyGpuDevice()` mirroring `resetGpuDeviceForTests` below.
61
+ // - releasing the device on `pagehide` or on the last display unmounting. That
62
+ // exact one-liner is what made every refresh cost a tab in the app that
63
+ // measured this.
64
+ //
65
+ // Letting go is the whole recovery story instead. A lost device is already
66
+ // gone (the `.lost` handler drops the reference and the next ask acquires a
67
+ // fresh one), and an abandoned one is reclaimed when the document goes, so the
68
+ // leak is bounded by a reload rather than accumulating over a session. Its
69
+ // pipelines and shader modules live until then, while the large VRAM is
70
+ // returned either way because `WebGPUHal.dispose` destroys its own buffers and
71
+ // textures.
72
+ //
73
+ // `WebGPUHal.dispose`'s `context.unconfigure()` is not this and is fine to
74
+ // keep: dropping a swapchain is the sanctioned way to release one, and every
75
+ // track unmount already does it many times a session without consequence.
76
+ export function onDeviceLost(listener) {
77
+ cell.deviceLostListeners.add(listener);
78
+ return () => {
79
+ cell.deviceLostListeners.delete(listener);
80
+ };
81
+ }
82
+ // One-line record of what GPU a user's machine actually gave us. The whole
83
+ // point of the GPU path is scaling across hardware we can't see — when a report
84
+ // comes in of a blank/errored track, this is the difference between "which GPU
85
+ // and how much headroom" and pure guesswork. maxTextureDimension2D and
86
+ // maxBufferSize are the two limits our over-allocation guards trip on.
87
+ function logGpuCapabilities(adapter, device) {
88
+ const { vendor, architecture, description } = adapter.info;
89
+ const { maxTextureDimension2D, maxBufferSize, maxStorageBufferBindingSize } = device.limits;
90
+ console.warn(`[GPU] WebGPU device ready — vendor="${vendor}" architecture="${architecture}" description="${description}" ` +
91
+ `maxTextureDimension2D=${maxTextureDimension2D} maxBufferSize=${maxBufferSize} maxStorageBufferBindingSize=${maxStorageBufferBindingSize}`);
92
+ }
93
+ async function acquire() {
94
+ const adapter = await navigator.gpu.requestAdapter();
95
+ if (!adapter) {
96
+ console.warn('[GPU] No compatible GPU adapter available. This may indicate WebGPU is disabled, unsupported hardware, or a system limitation. Falling back to WebGL2.');
97
+ return null;
98
+ }
99
+ const d = await adapter.requestDevice({
100
+ requiredLimits: {
101
+ maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
102
+ maxBufferSize: adapter.limits.maxBufferSize,
103
+ },
104
+ });
105
+ return { adapter, device: d };
106
+ }
107
+ // One ask on a machine that has never produced a device, several on one that
108
+ // has. See `hadDevice`: only the latter is asking again worth anything, and
109
+ // only there is a decline evidence of a stack still coming up rather than of
110
+ // hardware that simply cannot do this.
111
+ async function acquireWithRetry() {
112
+ const tries = cell.hadDevice ? REACQUIRE_TRIES : 1;
113
+ for (let attempt = 1; attempt <= tries; attempt++) {
114
+ const last = attempt === tries;
115
+ try {
116
+ const got = await acquire();
117
+ if (got || last) {
118
+ return got;
119
+ }
120
+ }
121
+ catch (e) {
122
+ if (last) {
123
+ throw e;
124
+ }
125
+ console.warn(`[GPU] device acquisition failed (${attempt}/${tries}):`, e);
126
+ }
127
+ await new Promise(resolve => setTimeout(resolve, REACQUIRE_DELAY_MS));
128
+ }
129
+ return null;
130
+ }
131
+ async function createDevice() {
132
+ try {
133
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
134
+ if (!navigator.gpu) {
135
+ console.warn('[GPU] WebGPU not supported in this browser. Falling back to WebGL2.');
136
+ return null;
137
+ }
138
+ const got = await acquireWithRetry();
139
+ if (!got) {
140
+ return null;
141
+ }
142
+ const { adapter, device: d } = got;
143
+ // Best-effort diagnostic: a logging failure (e.g. an environment that
144
+ // doesn't populate adapter.info) must never abort device creation and drop
145
+ // us to WebGL2.
146
+ try {
147
+ logGpuCapabilities(adapter, d);
148
+ }
149
+ catch (e) {
150
+ console.warn('[GPU] capability logging failed (non-fatal):', e);
151
+ }
152
+ // Surface any WebGPU validation / out-of-memory / internal errors that
153
+ // would otherwise be silently swallowed. Without this, a bad draw/pipeline
154
+ // results in a blank canvas with no console output.
155
+ d.addEventListener('uncapturederror', (event) => {
156
+ console.error('[GPU] UNCAPTURED ERROR:', event.error.message);
157
+ });
158
+ void d.lost.then(info => {
159
+ // Identity check: if the shared device has already been replaced (test
160
+ // reset + re-init, or a subsequent successful createDevice), the
161
+ // resolution of the old device's `.lost` promise must NOT null out the
162
+ // newer device. Without this guard the new device's reference is
163
+ // silently cleared on the next getGpuDevice() call.
164
+ if (cell.device === d) {
165
+ console.error('[GPU] Device lost:', info.message);
166
+ cell.device = null;
167
+ cell.devicePromise = null;
168
+ for (const listener of cell.deviceLostListeners) {
169
+ listener();
170
+ }
171
+ }
172
+ });
173
+ cell.device = d;
174
+ cell.hadDevice = true;
175
+ return d;
176
+ }
177
+ catch (e) {
178
+ console.warn('[GPU] WebGPU device creation failed:', e);
179
+ return null;
180
+ }
181
+ }
182
+ /**
183
+ * The renderers `?renderer=` can pin the page to. `canvas` is a spelling of
184
+ * `canvas2d` kept because it is in the wild.
185
+ *
186
+ * A pin means *only* that rung — `createGpuHal` does not fall past it. That is
187
+ * the whole point of the flag: it exists to compare backends, and a comparison
188
+ * whose subject silently substitutes itself is worse than no comparison. The
189
+ * failure is visible instead, as a `renderError` carrying the pin that could
190
+ * not be honored, with the banner's "disable GPU" as the way out.
191
+ */
192
+ export const GPU_OVERRIDES = ['webgpu', 'webgl', 'canvas2d', 'canvas'];
193
+ function isGpuOverride(value) {
194
+ return GPU_OVERRIDES.includes(value);
195
+ }
196
+ /**
197
+ * Pin the page to a renderer, or clear the pin with `null`. Page-wide by
198
+ * design and shared across every copy of this module — see {@link GpuDeviceCell}.
199
+ *
200
+ * Takes a bare `string` because both callers hand it one straight off a URL
201
+ * (`?renderer=` in jbrowse-web's `InitialLoad`, the same param fed by desktop's
202
+ * `--renderer` flag), so validating here is validating once at the only place
203
+ * an unchecked value enters. An unrecognized one clears the pin and says so:
204
+ * before this, `?renderer=WebGL` or a typo was indistinguishable from passing
205
+ * nothing at all, which is the one outcome a person debugging a renderer must
206
+ * not silently get.
207
+ */
208
+ export function setGpuOverride(value) {
209
+ if (value !== null && !isGpuOverride(value)) {
210
+ console.warn(`[GPU] ignoring unknown renderer "${value}" — expected one of ${GPU_OVERRIDES.join(', ')}. Rendering with the default ladder (WebGPU → WebGL2 → Canvas2D).`);
211
+ cell.gpuOverride = null;
212
+ return;
213
+ }
214
+ cell.gpuOverride = value;
215
+ }
216
+ export function getGpuOverride() {
217
+ return cell.gpuOverride;
218
+ }
219
+ /**
220
+ * Whether the GPU is off for the whole page — either pinned by `?renderer=` at
221
+ * startup or switched off from a display's GPU-error banner after an
222
+ * unrecoverable WebGL context loss. `createGpuHal` returns null in that case, so
223
+ * every backend built from then on is the Canvas2D one.
224
+ *
225
+ * Note this is narrower than "a pin is in force": `webgl` and `webgpu` are pins
226
+ * too, and both leave GPU rendering on. The banner reads this to decide whether
227
+ * to offer its escape, so it must stay the Canvas2D question specifically.
228
+ */
229
+ export function isGpuRenderingDisabled() {
230
+ return cell.gpuOverride === 'canvas2d' || cell.gpuOverride === 'canvas';
231
+ }
232
+ /**
233
+ * Reset the shared singleton state. For use in tests only — clears `device` and
234
+ * `devicePromise` so the next `getGpuDevice()` call starts fresh rather than
235
+ * returning the cached (possibly null) promise from a previous test.
236
+ *
237
+ * It drops references and deliberately does not destroy the device, so this is
238
+ * not the half of a pair that a `destroyGpuDevice()` completes. See the comment
239
+ * above `onDeviceLost` for what destroying one costs.
240
+ *
241
+ * Resets the cell's fields rather than replacing the cell, so a second copy of
242
+ * this module that captured it still sees the reset. `gpuOverride` is left
243
+ * alone: it is set by the host and by individual tests, and clearing it here
244
+ * would make the reset mean two different things.
245
+ */
246
+ export function resetGpuDeviceForTests() {
247
+ cell.device = null;
248
+ cell.devicePromise = null;
249
+ cell.hadDevice = false;
250
+ cell.deviceLostListeners.clear();
251
+ }
252
+ export function getGpuDevice() {
253
+ const override = getGpuOverride();
254
+ if (override !== null && ['webgl', 'canvas2d', 'canvas'].includes(override)) {
255
+ return Promise.resolve(null);
256
+ }
257
+ if (cell.device) {
258
+ return Promise.resolve(cell.device);
259
+ }
260
+ if (cell.devicePromise) {
261
+ return cell.devicePromise;
262
+ }
263
+ const pending = createDevice();
264
+ cell.devicePromise = pending;
265
+ // Nothing re-asks on its own — every backend built from here on reads this
266
+ // memo — so a cached failure is permanent for the life of the page. That is
267
+ // the right answer for hardware that has no WebGPU and the wrong one for a
268
+ // stack that is still coming back up, and `hadDevice` is which. Drop the memo
269
+ // on the second, so the next display to build asks again instead of
270
+ // inheriting a demotion to WebGL2 that no longer reflects the machine.
271
+ void pending.then(d => {
272
+ if (!d && cell.hadDevice && cell.devicePromise === pending) {
273
+ cell.devicePromise = null;
274
+ }
275
+ });
276
+ return pending;
277
+ }