@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.
- package/LICENSE +201 -0
- package/esm/OverlayCanvas.d.ts +29 -0
- package/esm/OverlayCanvas.js +43 -0
- package/esm/RenderCanvas.d.ts +76 -0
- package/esm/RenderCanvas.js +38 -0
- package/esm/RenderLifecycleMixin.d.ts +180 -0
- package/esm/RenderLifecycleMixin.js +279 -0
- package/esm/ScrollLockedOverlay.d.ts +28 -0
- package/esm/ScrollLockedOverlay.js +42 -0
- package/esm/blockClipUtils.d.ts +27 -0
- package/esm/blockClipUtils.js +101 -0
- package/esm/canvas2dUtils.d.ts +372 -0
- package/esm/canvas2dUtils.js +564 -0
- package/esm/canvasContext.d.ts +68 -0
- package/esm/canvasContext.js +74 -0
- package/esm/colorRampLut.d.ts +17 -0
- package/esm/colorRampLut.js +25 -0
- package/esm/coverageBand.d.ts +107 -0
- package/esm/coverageBand.js +109 -0
- package/esm/coverageBandBuffers.d.ts +30 -0
- package/esm/coverageBandBuffers.js +13 -0
- package/esm/createRenderingBackend.d.ts +52 -0
- package/esm/createRenderingBackend.js +58 -0
- package/esm/displayPhase.d.ts +155 -0
- package/esm/displayPhase.js +97 -0
- package/esm/globalRenderingBackend.d.ts +90 -0
- package/esm/globalRenderingBackend.js +53 -0
- package/esm/gpuDevice.d.ts +91 -0
- package/esm/gpuDevice.js +277 -0
- package/esm/graphicsCapabilities.d.ts +98 -0
- package/esm/graphicsCapabilities.js +165 -0
- package/esm/hal/createHal.d.ts +22 -0
- package/esm/hal/createHal.js +91 -0
- package/esm/hal/deviceGpuCache.d.ts +36 -0
- package/esm/hal/deviceGpuCache.js +120 -0
- package/esm/hal/gpuHalBase.d.ts +55 -0
- package/esm/hal/gpuHalBase.js +81 -0
- package/esm/hal/index.d.ts +7 -0
- package/esm/hal/index.js +5 -0
- package/esm/hal/mockHal.d.ts +153 -0
- package/esm/hal/mockHal.js +297 -0
- package/esm/hal/oomReporter.d.ts +15 -0
- package/esm/hal/oomReporter.js +22 -0
- package/esm/hal/passIds.d.ts +22 -0
- package/esm/hal/passIds.js +36 -0
- package/esm/hal/regionRegistry.d.ts +11 -0
- package/esm/hal/regionRegistry.js +66 -0
- package/esm/hal/types.d.ts +116 -0
- package/esm/hal/types.js +1 -0
- package/esm/hal/webgl2Hal.d.ts +47 -0
- package/esm/hal/webgl2Hal.js +490 -0
- package/esm/hal/webgpuHal.d.ts +169 -0
- package/esm/hal/webgpuHal.js +722 -0
- package/esm/installUpload.d.ts +91 -0
- package/esm/installUpload.js +94 -0
- package/esm/instanceCache.d.ts +43 -0
- package/esm/instanceCache.js +53 -0
- package/esm/instancePass.d.ts +50 -0
- package/esm/instancePass.js +34 -0
- package/esm/keyedRenderingBackend.d.ts +41 -0
- package/esm/keyedRenderingBackend.js +19 -0
- package/esm/mapUploadSync.d.ts +31 -0
- package/esm/mapUploadSync.js +45 -0
- package/esm/namedReactions.d.ts +29 -0
- package/esm/namedReactions.js +62 -0
- package/esm/perRegionRenderingBackend.d.ts +114 -0
- package/esm/perRegionRenderingBackend.js +93 -0
- package/esm/recoveryBudget.d.ts +40 -0
- package/esm/recoveryBudget.js +56 -0
- package/esm/regionDataMap.d.ts +60 -0
- package/esm/regionDataMap.js +98 -0
- package/esm/renderBlock.d.ts +25 -0
- package/esm/renderBlock.js +10 -0
- package/esm/renderingBackendBase.d.ts +71 -0
- package/esm/renderingBackendBase.js +50 -0
- package/esm/shaders/antialias.js.generated.d.ts +4 -0
- package/esm/shaders/antialias.js.generated.js +25 -0
- package/esm/shaders/capsule.generated.d.ts +1 -0
- package/esm/shaders/capsule.generated.js +3 -0
- package/esm/shaders/colorRampLut.generated.d.ts +1 -0
- package/esm/shaders/colorRampLut.generated.js +3 -0
- package/esm/shaders/coverageBand.generated.d.ts +2 -0
- package/esm/shaders/coverageBand.generated.js +4 -0
- package/esm/shaders/coverageBar.generated.d.ts +4 -0
- package/esm/shaders/coverageBar.generated.js +6 -0
- package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageBar.iface.generated.js +114 -0
- package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
- package/esm/shaders/coverageIndicator.generated.js +6 -0
- package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
- package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
- package/esm/shaders/coverageInterbase.generated.js +6 -0
- package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
- package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
- package/esm/shaders/coverageMod.generated.d.ts +4 -0
- package/esm/shaders/coverageMod.generated.js +6 -0
- package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageMod.iface.generated.js +144 -0
- package/esm/shaders/coverageSnp.generated.d.ts +4 -0
- package/esm/shaders/coverageSnp.generated.js +6 -0
- package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageSnp.iface.generated.js +144 -0
- package/esm/shaders/hpmath.js.generated.d.ts +3 -0
- package/esm/shaders/hpmath.js.generated.js +48 -0
- package/esm/shaders/pointGlyph.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.generated.js +3 -0
- package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.js.generated.js +7 -0
- package/esm/shaders/rowRect.generated.d.ts +3 -0
- package/esm/shaders/rowRect.generated.js +5 -0
- package/esm/shaders/rowRect.js.generated.d.ts +2 -0
- package/esm/shaders/rowRect.js.generated.js +13 -0
- package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
- package/esm/shaders/scoreScale.js.generated.js +46 -0
- package/esm/slangPass.d.ts +49 -0
- package/esm/slangPass.js +22 -0
- package/esm/useEventCallback.d.ts +1 -0
- package/esm/useEventCallback.js +19 -0
- package/esm/useGraphicsCapabilities.d.ts +17 -0
- package/esm/useGraphicsCapabilities.js +38 -0
- package/esm/useRenderingBackend.d.ts +83 -0
- package/esm/useRenderingBackend.js +422 -0
- package/esm/useTabVisibilityRerender.d.ts +13 -0
- package/esm/useTabVisibilityRerender.js +39 -0
- package/esm/webgpuUtils.d.ts +23 -0
- package/esm/webgpuUtils.js +76 -0
- package/package.json +91 -0
- package/src/shaders/antialias.js.generated.ts +33 -0
- package/src/shaders/antialias.slang +112 -0
- package/src/shaders/capsule.generated.ts +4 -0
- package/src/shaders/capsule.slang +62 -0
- package/src/shaders/colorPack.slang +26 -0
- package/src/shaders/colorRampLut.generated.ts +4 -0
- package/src/shaders/colorRampLut.slang +51 -0
- package/src/shaders/coverageBand.generated.ts +6 -0
- package/src/shaders/coverageBand.slang +276 -0
- package/src/shaders/coverageBar.generated.ts +10 -0
- package/src/shaders/coverageBar.iface.generated.ts +172 -0
- package/src/shaders/coverageBar.slang +37 -0
- package/src/shaders/coverageIndicator.generated.ts +10 -0
- package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
- package/src/shaders/coverageIndicator.slang +61 -0
- package/src/shaders/coverageInterbase.generated.ts +10 -0
- package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
- package/src/shaders/coverageInterbase.slang +63 -0
- package/src/shaders/coverageMod.generated.ts +10 -0
- package/src/shaders/coverageMod.iface.generated.ts +211 -0
- package/src/shaders/coverageMod.slang +36 -0
- package/src/shaders/coverageSnp.generated.ts +10 -0
- package/src/shaders/coverageSnp.iface.generated.ts +211 -0
- package/src/shaders/coverageSnp.slang +52 -0
- package/src/shaders/diagonalGrid.slang +25 -0
- package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
- package/src/shaders/hpmath.js.generated.ts +47 -0
- package/src/shaders/hpmath.slang +226 -0
- package/src/shaders/pointGlyph.generated.ts +4 -0
- package/src/shaders/pointGlyph.js.generated.ts +9 -0
- package/src/shaders/pointGlyph.slang +93 -0
- package/src/shaders/rowRect.generated.ts +8 -0
- package/src/shaders/rowRect.js.generated.ts +17 -0
- package/src/shaders/rowRect.slang +160 -0
- package/src/shaders/scoreScale.js.generated.ts +51 -0
- 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 {};
|
package/esm/gpuDevice.js
ADDED
|
@@ -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
|
+
}
|