@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,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
|
+
}
|