@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,83 @@
1
+ export declare const CONTEXT_LOST_REPORT_GRACE_MS = 400;
2
+ /**
3
+ * Flagged rather than matched by message or `instanceof`, so the error UI can
4
+ * offer the remedy specific to a loss (switch the page to Canvas2D) without
5
+ * offering it for render errors whose remedy differs (an over-allocation says to
6
+ * zoom in).
7
+ *
8
+ * A lost WebGPU device carries the same flag deliberately: the two causes
9
+ * differ, but the remedy on offer — take the page off the GPU — is the same one.
10
+ */
11
+ export declare function createGpuContextLostError(message?: string): Error & {
12
+ gpuContextLost: true;
13
+ };
14
+ export declare function createGpuDeviceLostError(): Error & {
15
+ gpuContextLost: true;
16
+ };
17
+ export declare function isGpuContextLostError(error: unknown): error is object & Record<"gpuContextLost", unknown>;
18
+ /**
19
+ * Duck-typed shape of an MST display model that owns a GPU backend
20
+ * lifecycle via `RenderLifecycleMixin`. Plugins pass their own
21
+ * model; the hook only touches these actions/fields.
22
+ */
23
+ export interface RenderLifecycleModel<RenderingBackendType> {
24
+ startRenderingBackend: (backend: RenderingBackendType) => void;
25
+ stopRenderingBackend: () => void;
26
+ renderNow: () => void;
27
+ renderError: unknown;
28
+ setRenderError: (error: unknown) => void;
29
+ }
30
+ /**
31
+ * Drives the GPU/Canvas2D backend lifecycle for a display: canvas
32
+ * initialization, context-loss / device-loss recovery, page-navigation
33
+ * cleanup, and retry — wiring each event directly to the model's
34
+ * `RenderLifecycleMixin` actions so the model (not React-local hook state)
35
+ * owns every terminal state.
36
+ *
37
+ * The returned `canvasRef` is a callback ref — assign it directly to a
38
+ * `<canvas ref={canvasRef} />`. React invokes it with the DOM node on mount
39
+ * and with `null` on unmount, so the initialization effect re-runs when the
40
+ * underlying canvas element is actually replaced (e.g. after regionTooLarge
41
+ * unmounts then remounts the canvas).
42
+ *
43
+ * Lifecycle:
44
+ * 1. Canvas mounts → init effect calls `factory(canvas)`; on success
45
+ * `setRenderError(undefined)` + `startRenderingBackend(backend)`, on
46
+ * failure `setRenderError(error)`.
47
+ * 2. WebGL context loss → the browser gets one grace window to fire
48
+ * `webglcontextrestored` (which bumps `contextVersion` → rebuild, no user-
49
+ * visible state); if it doesn't, the loss is reported as `renderError`
50
+ * (`createGpuContextLostError`), which unmounts the canvas and so is what
51
+ * makes a fresh context obtainable at all. Bounded auto-recovery then
52
+ * clears it. WebGPU device loss bumps `contextVersion` directly.
53
+ * 3. `retry()` clears `renderError` + bumps `contextVersion` so the next
54
+ * mount reinitializes.
55
+ *
56
+ * Page navigation fires `pagehide` on every mounted component, disposing its
57
+ * backend AND clearing `model.currentRenderingBackend` (so the render/upload
58
+ * autoruns no-op); React unmount disposes via the effect cleanup. A bfcache
59
+ * restore fires `pageshow` with `persisted`, which rebuilds the backend for the
60
+ * still-mounted canvas. No global tracking.
61
+ *
62
+ * The model argument is duck-typed to the slot mixin's contract — the listed
63
+ * actions/fields are all the hook touches.
64
+ *
65
+ * **`factory` must be referentially stable** — a module-level constant, as every
66
+ * consumer in this repo passes (`AlignmentsRenderer`, `createDotplotRenderer`,
67
+ * …). It is a dependency of the init effect, so an inline
68
+ * `canvas => new MyBackend(canvas)` rebuilds the backend on every render:
69
+ * dispose, async re-create, `startRenderingBackend`, forever. Nothing throws and
70
+ * nothing logs — the display just paints from a backend a render behind, and the
71
+ * GPU churn shows up only in a profile. Bind the factory outside the component
72
+ * (or `useCallback` it with no deps).
73
+ */
74
+ export declare function useRenderingBackend<RenderingBackendType extends {
75
+ dispose(): void;
76
+ setErrorHandler: (handler: (error: Error) => void) => void;
77
+ }>(factory: (canvas: HTMLCanvasElement) => Promise<RenderingBackendType>, model: RenderLifecycleModel<RenderingBackendType>): {
78
+ canvas: HTMLCanvasElement | null;
79
+ canvasRef: (node: HTMLCanvasElement | null) => void;
80
+ error: unknown;
81
+ retry: () => void;
82
+ canvasKey: number;
83
+ };
@@ -0,0 +1,422 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
3
+ import { onDeviceLost } from './gpuDevice.js';
4
+ import { RecoveryBudget } from './recoveryBudget.js';
5
+ import { useTabVisibilityRerender } from './useTabVisibilityRerender.js';
6
+ // Auto-recovery from WebGL context loss. The browser force-loses the oldest
7
+ // context when too many are live (and may never fire `webglcontextrestored`),
8
+ // which strands the display on the GPU error overlay. We re-init a FEW times on
9
+ // an exponential backoff so it comes back once GPU capacity frees — then stop
10
+ // and leave the manual Retry button, deliberately leaning on manual recovery
11
+ // rather than risk thrashing the page with endless re-inits. The budget is
12
+ // `RecoveryBudget`, which is windowed: a context that keeps flapping climbs to
13
+ // the cap and stops, but two losses far enough apart to not be a flap each get
14
+ // their own recovery. It resets outright only on a genuine browser restore or a
15
+ // manual retry (never on a bare re-acquire — every flap contains one).
16
+ const CONTEXT_RECOVER_BASE_MS = 1000;
17
+ // A lost context reports itself because nothing else will: calls on it are
18
+ // silent no-ops, and it can't be re-acquired on its element (see `canvasKey`
19
+ // below, which is what gets every consumer a fresh element).
20
+ //
21
+ // The report waits out a grace window first — a race against
22
+ // `webglcontextrestored`, which lands within a frame or two for the recoverable
23
+ // causes (GPU crash, driver reset) and needs no banner at all. Nothing is
24
+ // aborted when the window expires, so this is not a readiness cap.
25
+ export const CONTEXT_LOST_REPORT_GRACE_MS = 400;
26
+ const CONTEXT_LOST_MESSAGE = 'WebGL context lost. The browser reclaimed the GPU context for this display, ' +
27
+ 'usually because too many GPU-rendered views are open at once.';
28
+ const DEVICE_LOST_MESSAGE = 'WebGPU device lost. The GPU device backing this display went away and kept ' +
29
+ 'going away after several attempts to rebuild on a fresh one.';
30
+ /**
31
+ * Flagged rather than matched by message or `instanceof`, so the error UI can
32
+ * offer the remedy specific to a loss (switch the page to Canvas2D) without
33
+ * offering it for render errors whose remedy differs (an over-allocation says to
34
+ * zoom in).
35
+ *
36
+ * A lost WebGPU device carries the same flag deliberately: the two causes
37
+ * differ, but the remedy on offer — take the page off the GPU — is the same one.
38
+ */
39
+ export function createGpuContextLostError(message = CONTEXT_LOST_MESSAGE) {
40
+ return Object.assign(new Error(message), {
41
+ gpuContextLost: true,
42
+ });
43
+ }
44
+ export function createGpuDeviceLostError() {
45
+ return createGpuContextLostError(DEVICE_LOST_MESSAGE);
46
+ }
47
+ export function isGpuContextLostError(error) {
48
+ return (typeof error === 'object' && error !== null && 'gpuContextLost' in error);
49
+ }
50
+ /**
51
+ * The display's recovery budget, created on first read. Reached through the ref
52
+ * at every use site rather than unwrapped once in the hook body: the unwrapped
53
+ * value is a fresh binding each render, so an effect closing over it has to
54
+ * name it as a dependency, and then the effect re-subscribes on every render.
55
+ * The ref itself is stable and needs no dependency.
56
+ */
57
+ function budgetOf(ref) {
58
+ return (ref.current ??= new RecoveryBudget());
59
+ }
60
+ function nodeAlive(model) {
61
+ if (isStateTreeNode(model)) {
62
+ return isAlive(model);
63
+ }
64
+ return true;
65
+ }
66
+ /**
67
+ * Drives the GPU/Canvas2D backend lifecycle for a display: canvas
68
+ * initialization, context-loss / device-loss recovery, page-navigation
69
+ * cleanup, and retry — wiring each event directly to the model's
70
+ * `RenderLifecycleMixin` actions so the model (not React-local hook state)
71
+ * owns every terminal state.
72
+ *
73
+ * The returned `canvasRef` is a callback ref — assign it directly to a
74
+ * `<canvas ref={canvasRef} />`. React invokes it with the DOM node on mount
75
+ * and with `null` on unmount, so the initialization effect re-runs when the
76
+ * underlying canvas element is actually replaced (e.g. after regionTooLarge
77
+ * unmounts then remounts the canvas).
78
+ *
79
+ * Lifecycle:
80
+ * 1. Canvas mounts → init effect calls `factory(canvas)`; on success
81
+ * `setRenderError(undefined)` + `startRenderingBackend(backend)`, on
82
+ * failure `setRenderError(error)`.
83
+ * 2. WebGL context loss → the browser gets one grace window to fire
84
+ * `webglcontextrestored` (which bumps `contextVersion` → rebuild, no user-
85
+ * visible state); if it doesn't, the loss is reported as `renderError`
86
+ * (`createGpuContextLostError`), which unmounts the canvas and so is what
87
+ * makes a fresh context obtainable at all. Bounded auto-recovery then
88
+ * clears it. WebGPU device loss bumps `contextVersion` directly.
89
+ * 3. `retry()` clears `renderError` + bumps `contextVersion` so the next
90
+ * mount reinitializes.
91
+ *
92
+ * Page navigation fires `pagehide` on every mounted component, disposing its
93
+ * backend AND clearing `model.currentRenderingBackend` (so the render/upload
94
+ * autoruns no-op); React unmount disposes via the effect cleanup. A bfcache
95
+ * restore fires `pageshow` with `persisted`, which rebuilds the backend for the
96
+ * still-mounted canvas. No global tracking.
97
+ *
98
+ * The model argument is duck-typed to the slot mixin's contract — the listed
99
+ * actions/fields are all the hook touches.
100
+ *
101
+ * **`factory` must be referentially stable** — a module-level constant, as every
102
+ * consumer in this repo passes (`AlignmentsRenderer`, `createDotplotRenderer`,
103
+ * …). It is a dependency of the init effect, so an inline
104
+ * `canvas => new MyBackend(canvas)` rebuilds the backend on every render:
105
+ * dispose, async re-create, `startRenderingBackend`, forever. Nothing throws and
106
+ * nothing logs — the display just paints from a backend a render behind, and the
107
+ * GPU churn shows up only in a profile. Bind the factory outside the component
108
+ * (or `useCallback` it with no deps).
109
+ */
110
+ export function useRenderingBackend(factory, model) {
111
+ const [canvas, setCanvas] = useState(null);
112
+ const [contextVersion, setContextVersion] = useState(0);
113
+ const rendererRef = useRef(null);
114
+ // Set true the moment a `webglcontextlost` fires; gates auto-recovery so only
115
+ // a genuine context loss triggers it (not a config/render-logic error). The
116
+ // canvas often unmounts behind the error overlay before recovery runs, so
117
+ // recovery is driven by `renderError` (always observed) + this sticky flag,
118
+ // not by the canvas-bound listener.
119
+ const contextLostRef = useRef(false);
120
+ // One budget across both loss families, not one each: a display renders on a
121
+ // single HAL rung at a time, and sharing it stops a display that flaps
122
+ // between the two from spending a fresh allowance on each.
123
+ const budgetRef = useRef(null);
124
+ // Latched once the budget says stop. The recovery effect below runs on every
125
+ // render, so it needs a non-mutating way to know it has already given up —
126
+ // asking the budget again would refresh its window and it would never lapse.
127
+ const gaveUpRef = useRef(false);
128
+ const recoverTimerRef = useRef(undefined);
129
+ // Pending "the browser didn't restore it" report; see
130
+ // CONTEXT_LOST_REPORT_GRACE_MS. Separate from recoverTimerRef because the two
131
+ // are different phases: this one sets renderError, that one clears it.
132
+ const lossReportTimerRef = useRef(undefined);
133
+ // The element the component is showing RIGHT NOW, written in the commit phase
134
+ // by the ref callback — ahead of the passive effects, which is what makes it
135
+ // able to answer a question `canvas` cannot. See the init effect.
136
+ const currentCanvasRef = useRef(null);
137
+ const canvasRef = useCallback((node) => {
138
+ currentCanvasRef.current = node;
139
+ setCanvas(node);
140
+ }, []);
141
+ // A DIFFERENT model on the same component is a re-init, so it needs the fresh
142
+ // canvas element every other re-init gets, plus a clean recovery history —
143
+ // inheriting the old display's `gaveUp` latch would leave the new one unable
144
+ // to recover at all.
145
+ //
146
+ // Re-initializing on the element it was already using is a `dispose()` — which
147
+ // unconfigures the canvas's WebGPU swap chain — racing a `create()` on the same
148
+ // context, and whichever lands last decides whether the display can draw again.
149
+ //
150
+ // Defensive: no consumer reaches it today, because all three remount instead.
151
+ // A synteny band's section sits inside a Fragment keyed on `view.id`, and the
152
+ // `setViews` that replaces the levels mints fresh view ids along with them; a
153
+ // dotplot's model is its own view; and a display-type swap changes the element
154
+ // type, since no two display types registered for one track type share a
155
+ // `ReactComponent`. Each of those is one edit away from not being true, and
156
+ // what this costs is a ref and a compare.
157
+ //
158
+ // During render rather than in an effect, because the effect below must never
159
+ // observe the mismatched pair — a new model with the previous one's element.
160
+ // This is React's documented "adjust state when a prop changes": the render
161
+ // whose output is discarded is this one, before anything commits.
162
+ //
163
+ // The backoff a departing model armed is cancelled by the effect below, not
164
+ // here: a `clearTimeout` during render is a side effect, which `retry()` and
165
+ // `onRestored` can do and this cannot.
166
+ const lastModelRef = useRef(model);
167
+ if (lastModelRef.current !== model) {
168
+ lastModelRef.current = model;
169
+ contextLostRef.current = false;
170
+ gaveUpRef.current = false;
171
+ budgetOf(budgetRef).reset();
172
+ setContextVersion(v => v + 1);
173
+ }
174
+ useEffect(() => {
175
+ if (canvas) {
176
+ const onLost = (e) => {
177
+ // preventDefault is what permits `webglcontextrestored` at all; the timer
178
+ // below is the wait for it
179
+ e.preventDefault();
180
+ contextLostRef.current = true;
181
+ clearTimeout(lossReportTimerRef.current);
182
+ lossReportTimerRef.current = setTimeout(() => {
183
+ lossReportTimerRef.current = undefined;
184
+ if (nodeAlive(model)) {
185
+ model.setRenderError(createGpuContextLostError());
186
+ }
187
+ }, CONTEXT_LOST_REPORT_GRACE_MS);
188
+ };
189
+ const onRestored = () => {
190
+ // browser recovered on its own: cancel pending timers, reset, rebuild
191
+ // (every GL object from the old context is dead even though
192
+ // `isContextLost()` now reads false)
193
+ clearTimeout(lossReportTimerRef.current);
194
+ lossReportTimerRef.current = undefined;
195
+ clearTimeout(recoverTimerRef.current);
196
+ recoverTimerRef.current = undefined;
197
+ contextLostRef.current = false;
198
+ gaveUpRef.current = false;
199
+ budgetOf(budgetRef).reset();
200
+ setContextVersion(v => v + 1);
201
+ };
202
+ canvas.addEventListener('webglcontextlost', onLost);
203
+ canvas.addEventListener('webglcontextrestored', onRestored);
204
+ return () => {
205
+ // Drop a report still in its grace window: nothing left to recover, and
206
+ // `renderError` outranks every other phase, so a late one would replace a
207
+ // legitimate too-large banner with a GPU error.
208
+ clearTimeout(lossReportTimerRef.current);
209
+ lossReportTimerRef.current = undefined;
210
+ canvas.removeEventListener('webglcontextlost', onLost);
211
+ canvas.removeEventListener('webglcontextrestored', onRestored);
212
+ };
213
+ }
214
+ return undefined;
215
+ }, [canvas, model]);
216
+ // The third site that resets the recovery state, alongside `retry()` and
217
+ // `onRestored`, and like them it cancels the pending backoff first. A timer
218
+ // armed for the model that left bumps `contextVersion` when it fires, which
219
+ // rebuilds a device, pipeline set and swap chain for a display with nothing
220
+ // wrong with it — and until it fires it occupies the one-pending-timer guard
221
+ // below, so the arriving model cannot arm the recovery it is owed.
222
+ useEffect(() => {
223
+ clearTimeout(recoverTimerRef.current);
224
+ recoverTimerRef.current = undefined;
225
+ }, [model]);
226
+ // Auto-recover a context-loss-induced error: re-init on bounded backoff. Gated
227
+ // on `contextLostRef` so non-GPU render errors are never auto-retried, and on
228
+ // a one-pending-timer guard so it schedules at most one attempt at a time. The
229
+ // budget is spent only when an attempt is actually scheduled, so a context
230
+ // that keeps re-losing climbs to the cap then stops — never spins.
231
+ // Runs every render (no dep array) on purpose: the guards make it idempotent,
232
+ // and depending on `model.renderError` is unreliable here (a plain re-render
233
+ // can miss the value transition). The unmount cleanup is the separate effect
234
+ // below.
235
+ useEffect(() => {
236
+ if (model.renderError &&
237
+ contextLostRef.current &&
238
+ !gaveUpRef.current &&
239
+ recoverTimerRef.current === undefined) {
240
+ const budget = budgetOf(budgetRef);
241
+ if (budget.record(performance.now()) === 'give-up') {
242
+ gaveUpRef.current = true;
243
+ return;
244
+ }
245
+ const delay = CONTEXT_RECOVER_BASE_MS * 2 ** (budget.attempt - 1);
246
+ recoverTimerRef.current = setTimeout(() => {
247
+ recoverTimerRef.current = undefined;
248
+ if (nodeAlive(model)) {
249
+ model.setRenderError(undefined);
250
+ }
251
+ setContextVersion(v => v + 1);
252
+ }, delay);
253
+ }
254
+ });
255
+ // Clear any pending loss-report / auto-recovery timer on unmount.
256
+ useEffect(() => () => {
257
+ clearTimeout(lossReportTimerRef.current);
258
+ clearTimeout(recoverTimerRef.current);
259
+ }, []);
260
+ // WebGPU device loss. This re-inits invisibly — no grace window and no
261
+ // `renderError`, because `gpuDevice` has already dropped the dead device and
262
+ // the next `getGpuDevice()` acquires a fresh one — but it is bounded by the
263
+ // same budget as the WebGL path, and for a sharper reason. A WebGL loss that
264
+ // never recovers is at least *reported*: it sets `renderError`, which is what
265
+ // unmounts the canvas. Nothing reports this one, so an uncapped version of it
266
+ // is a display that re-initializes against a dying device forever, silently,
267
+ // for as long as the tab is open. On give-up it says so instead.
268
+ useEffect(() => onDeviceLost(() => {
269
+ if (gaveUpRef.current) {
270
+ return;
271
+ }
272
+ if (budgetOf(budgetRef).record(performance.now()) === 'give-up') {
273
+ gaveUpRef.current = true;
274
+ if (nodeAlive(model)) {
275
+ model.setRenderError(createGpuDeviceLostError());
276
+ }
277
+ return;
278
+ }
279
+ setContextVersion(v => v + 1);
280
+ }), [model]);
281
+ useEffect(() => {
282
+ const handleGlobalPageHide = () => {
283
+ rendererRef.current?.dispose();
284
+ rendererRef.current = null;
285
+ // On a bfcache freeze this timer thaws AFTER pageshow rebuilt the backend,
286
+ // bannering a working canvas.
287
+ clearTimeout(lossReportTimerRef.current);
288
+ lossReportTimerRef.current = undefined;
289
+ // Also clear the model's backend reference — not just dispose the GPU
290
+ // object. On a bfcache navigate-away the component is frozen, not
291
+ // unmounted, so the effect cleanup never runs; leaving
292
+ // currentRenderingBackend pointing at the disposed backend would let the
293
+ // upload/render autoruns fire against dead GPU state on restore. With it
294
+ // cleared, those autoruns no-op (they guard `backend === undefined`) until
295
+ // pageshow rebuilds a fresh one.
296
+ if (nodeAlive(model)) {
297
+ model.stopRenderingBackend();
298
+ }
299
+ };
300
+ const handleGlobalPageShow = (e) => {
301
+ // Restored from bfcache (persisted): the canvas DOM node survived but its
302
+ // backend was disposed on pagehide. Bump contextVersion so the init effect
303
+ // re-runs and builds a fresh backend for the same canvas.
304
+ if (e.persisted) {
305
+ setContextVersion(v => v + 1);
306
+ }
307
+ };
308
+ window.addEventListener('pagehide', handleGlobalPageHide, true);
309
+ window.addEventListener('pageshow', handleGlobalPageShow, true);
310
+ return () => {
311
+ window.removeEventListener('pagehide', handleGlobalPageHide, true);
312
+ window.removeEventListener('pageshow', handleGlobalPageShow, true);
313
+ };
314
+ }, [model]);
315
+ useEffect(() => {
316
+ // Initialize the element the component is CURRENTLY showing, which the
317
+ // `canvas` state alone does not identify. Bumping `contextVersion` remounts
318
+ // the canvas, and the ref callback's `setCanvas(node2)` lands in a later
319
+ // render than the commit that swapped the element — so this effect runs once
320
+ // with the new version still paired to the previous, now-superseded element.
321
+ // Initializing there builds an entire backend (device, pipelines, swap
322
+ // chain) on an element nothing can see, attaches it, and discards it on the
323
+ // next render. That was the second live init behind the WebGPU swap-chain
324
+ // race: two overlapping `factory` calls whose teardown order decided which
325
+ // context was left configured.
326
+ //
327
+ // Compared against the ref rather than `canvas.isConnected`, which answers
328
+ // this only for a host that mounts into the document and would silently
329
+ // refuse to initialize for one that does not.
330
+ if (canvas && canvas === currentCanvasRef.current) {
331
+ let cancelled = false;
332
+ let backend = null;
333
+ factory(canvas)
334
+ .then(r => {
335
+ if (cancelled) {
336
+ r.dispose();
337
+ }
338
+ else {
339
+ backend = r;
340
+ rendererRef.current = r;
341
+ // Route HAL out-of-memory / over-device-limit allocation failures to
342
+ // renderError so an over-large view shows an error overlay (with a
343
+ // manual Retry) instead of a silently-blank canvas. Not gated on
344
+ // contextLostRef, so it never auto-retries — an OOM recurs on retry.
345
+ r.setErrorHandler(e => {
346
+ if (nodeAlive(model)) {
347
+ model.setRenderError(e);
348
+ }
349
+ });
350
+ // init produced a backend: clear the context-loss scoping flag so a
351
+ // later non-GPU error isn't mistaken for a context loss. The attempt
352
+ // counter is deliberately NOT reset here — a context that resolves
353
+ // then immediately re-loses must keep climbing toward the cap rather
354
+ // than spin forever; only a real restore / manual retry resets it.
355
+ contextLostRef.current = false;
356
+ if (nodeAlive(model)) {
357
+ model.setRenderError(undefined);
358
+ model.startRenderingBackend(r);
359
+ }
360
+ }
361
+ })
362
+ .catch((e) => {
363
+ if (!cancelled && nodeAlive(model)) {
364
+ model.setRenderError(e);
365
+ }
366
+ });
367
+ return () => {
368
+ cancelled = true;
369
+ backend?.dispose();
370
+ rendererRef.current = null;
371
+ if (nodeAlive(model)) {
372
+ model.stopRenderingBackend();
373
+ }
374
+ };
375
+ }
376
+ return undefined;
377
+ }, [canvas, contextVersion, factory, model]);
378
+ useTabVisibilityRerender(() => {
379
+ if (nodeAlive(model)) {
380
+ model.renderNow();
381
+ }
382
+ });
383
+ function retry() {
384
+ // manual retry = fresh start: cancel pending timers and reset the budget
385
+ clearTimeout(lossReportTimerRef.current);
386
+ lossReportTimerRef.current = undefined;
387
+ clearTimeout(recoverTimerRef.current);
388
+ recoverTimerRef.current = undefined;
389
+ contextLostRef.current = false;
390
+ gaveUpRef.current = false;
391
+ budgetOf(budgetRef).reset();
392
+ if (nodeAlive(model)) {
393
+ model.setRenderError(undefined);
394
+ }
395
+ setContextVersion(v => v + 1);
396
+ }
397
+ // `error` is sourced from model volatile, not React-local hook state: the
398
+ // model owns the terminal state, the hook only writes/reads it. Returned for
399
+ // standalone consumers (dotplot, synteny) that render their own banner;
400
+ // DisplayChrome reads `model.renderError` directly.
401
+ //
402
+ // `canvasKey` must reach the `<canvas key=…>` of EVERY consumer, not just the
403
+ // standalone ones. A re-init needs an element that never held a context:
404
+ // `getContext('webgl2')` keeps returning the lost one, `getContext('2d')`
405
+ // returns null on any element that once had WebGL, and more generally a
406
+ // canvas's context kind is permanent — so a re-init whose ladder lands on a
407
+ // different rung than last time cannot bind at all.
408
+ //
409
+ // This used to say DisplayChrome consumers got it free, because `renderError`
410
+ // unmounts the canvas. That covers only a *reported* loss: `onRestored`,
411
+ // `onDeviceLost` and the bfcache `pageshow` above all bump `contextVersion`
412
+ // and deliberately set no `renderError`, and those reused the element.
413
+ // `DisplayChromeBase` now keys its render-prop body on this value, and the
414
+ // standalone consumers get it from `RenderCanvas`.
415
+ return {
416
+ canvas,
417
+ canvasRef,
418
+ error: model.renderError,
419
+ retry,
420
+ canvasKey: contextVersion,
421
+ };
422
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Re-renders GPU canvases when the browser tab becomes visible again.
3
+ *
4
+ * WebGPU (and some WebGL configurations) discard the swap-chain texture when
5
+ * a tab is hidden. MobX autoruns only re-fire when observables change, so
6
+ * returning to the tab shows a black canvas. This hook triggers one extra
7
+ * render on `visibilitychange` to restore the content.
8
+ *
9
+ * Pass whatever function your component uses to issue GPU draw calls. It is
10
+ * captured via `useEventCallback` so it always sees the latest closure values
11
+ * without needing to be listed as a dependency.
12
+ */
13
+ export declare function useTabVisibilityRerender(renderFn: () => void): void;
@@ -0,0 +1,39 @@
1
+ import { useEffect } from 'react';
2
+ import { useEventCallback } from './useEventCallback.js';
3
+ /**
4
+ * Re-renders GPU canvases when the browser tab becomes visible again.
5
+ *
6
+ * WebGPU (and some WebGL configurations) discard the swap-chain texture when
7
+ * a tab is hidden. MobX autoruns only re-fire when observables change, so
8
+ * returning to the tab shows a black canvas. This hook triggers one extra
9
+ * render on `visibilitychange` to restore the content.
10
+ *
11
+ * Pass whatever function your component uses to issue GPU draw calls. It is
12
+ * captured via `useEventCallback` so it always sees the latest closure values
13
+ * without needing to be listed as a dependency.
14
+ */
15
+ export function useTabVisibilityRerender(renderFn) {
16
+ const stableRender = useEventCallback(renderFn);
17
+ useEffect(() => {
18
+ let rafId = null;
19
+ const handle = () => {
20
+ if (!document.hidden) {
21
+ // Schedule the render inside requestAnimationFrame so that
22
+ // getCurrentTexture() is called within a proper frame. Calling it
23
+ // directly from a visibilitychange handler can return a detached
24
+ // texture in WebGPU, causing the GPU timeline to hang.
25
+ rafId = requestAnimationFrame(() => {
26
+ rafId = null;
27
+ stableRender();
28
+ });
29
+ }
30
+ };
31
+ document.addEventListener('visibilitychange', handle);
32
+ return () => {
33
+ document.removeEventListener('visibilitychange', handle);
34
+ if (rafId !== null) {
35
+ cancelAnimationFrame(rafId);
36
+ }
37
+ };
38
+ }, [stableRender]);
39
+ }
@@ -0,0 +1,23 @@
1
+ export declare const STANDARD_BLEND_STATE: GPUBlendState;
2
+ /**
3
+ * Creates a bind group for uniforms at binding 1.
4
+ * This bind group is intended for use with shaders generated by Slang,
5
+ * which typically place uniforms at binding index 1.
6
+ * @param device - The GPUDevice.
7
+ * @param layout - `DeviceLayouts.uniformOnlyBindGroupLayout`.
8
+ * @param uniformBuffer - The GPUBuffer containing the uniform data.
9
+ * @param uniformVisibleSize - The visible size of the uniform buffer.
10
+ * @returns The GPUBindGroup.
11
+ */
12
+ export declare function createUniformOnlyBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, uniformBuffer: GPUBuffer, uniformVisibleSize: number): GPUBindGroup;
13
+ export declare function createVertexBuffer(device: GPUDevice, data: ArrayBuffer | ArrayBufferView): GPUBuffer;
14
+ /**
15
+ * Converts one entry of a pass's vertex input layout to a WebGPU vertex format
16
+ * string. Example: { components: 2, type: 'float' } -> 'float32x2'
17
+ * @param attr - Vertex attribute descriptor.
18
+ * @returns GPUVertexFormat string.
19
+ */
20
+ export declare function toGpuVertexFormat(attr: {
21
+ components: number;
22
+ type: 'float' | 'uint' | 'int';
23
+ }): GPUVertexFormat;
@@ -0,0 +1,76 @@
1
+ /// <reference types="@webgpu/types" />
2
+ // Standard alpha blend state:
3
+ // - Uses src-alpha, one-minus-src-alpha for color.
4
+ // - Converts straight (non-premultiplied) alpha output from shaders
5
+ // into premultiplied alpha in the framebuffer.
6
+ // Shaders MUST output straight alpha: vec4(rgb, alpha).
7
+ //
8
+ // Some passes use a 'premultiplied' blend state ({ srcFactor: 'one' }).
9
+ // Those shaders MUST output premultiplied alpha: vec4(rgb*alpha, alpha).
10
+ export const STANDARD_BLEND_STATE = {
11
+ color: {
12
+ srcFactor: 'src-alpha',
13
+ dstFactor: 'one-minus-src-alpha',
14
+ operation: 'add',
15
+ },
16
+ alpha: {
17
+ srcFactor: 'one',
18
+ dstFactor: 'one-minus-src-alpha',
19
+ operation: 'add',
20
+ },
21
+ };
22
+ // Per-instance data is always fed through a vertex buffer (matching Slang's
23
+ // vertex-attribute input semantics). The bind group carries only the uniform
24
+ // at binding 1 (and, optionally, a texture + sampler at 2/3). Binding index 1
25
+ // matches what the codegen emits so shaders share the same @binding(1)
26
+ // annotation regardless of whether they sample textures, and
27
+ // `assertRenderBindingShape` is what checks that rather than promising it.
28
+ //
29
+ // The layouts themselves are built once per device in `hal/deviceGpuCache.ts`;
30
+ // a bind GROUP is per-HAL, because it names that HAL's uniform ring buffer.
31
+ /**
32
+ * Creates a bind group for uniforms at binding 1.
33
+ * This bind group is intended for use with shaders generated by Slang,
34
+ * which typically place uniforms at binding index 1.
35
+ * @param device - The GPUDevice.
36
+ * @param layout - `DeviceLayouts.uniformOnlyBindGroupLayout`.
37
+ * @param uniformBuffer - The GPUBuffer containing the uniform data.
38
+ * @param uniformVisibleSize - The visible size of the uniform buffer.
39
+ * @returns The GPUBindGroup.
40
+ */
41
+ export function createUniformOnlyBindGroup(device, layout, uniformBuffer, uniformVisibleSize) {
42
+ return device.createBindGroup({
43
+ layout,
44
+ entries: [
45
+ {
46
+ binding: 1, // Standard binding for uniforms generated by Slang codegen.
47
+ resource: {
48
+ buffer: uniformBuffer,
49
+ offset: 0,
50
+ size: uniformVisibleSize,
51
+ },
52
+ },
53
+ ],
54
+ });
55
+ }
56
+ export function createVertexBuffer(device, data) {
57
+ const buf = device.createBuffer({
58
+ // WebGPU buffers must be at least 4 bytes in size.
59
+ size: Math.max(data.byteLength, 4),
60
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
61
+ });
62
+ device.queue.writeBuffer(buf, 0, data);
63
+ return buf;
64
+ }
65
+ /**
66
+ * Converts one entry of a pass's vertex input layout to a WebGPU vertex format
67
+ * string. Example: { components: 2, type: 'float' } -> 'float32x2'
68
+ * @param attr - Vertex attribute descriptor.
69
+ * @returns GPUVertexFormat string.
70
+ */
71
+ export function toGpuVertexFormat(attr) {
72
+ const base = attr.type === 'uint' ? 'uint32' : attr.type === 'int' ? 'sint32' : 'float32';
73
+ return attr.components === 1
74
+ ? base
75
+ : `${base}x${attr.components}`;
76
+ }