@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,98 @@
|
|
|
1
|
+
export interface GraphicsCapabilities {
|
|
2
|
+
webgpu: boolean;
|
|
3
|
+
/**
|
|
4
|
+
* `undefined` when the WebGL2 probe was skipped, which is what
|
|
5
|
+
* `getGraphicsCapabilities` does whenever WebGPU answered first — see there.
|
|
6
|
+
*/
|
|
7
|
+
webgl2?: boolean;
|
|
8
|
+
gpuVendor?: string;
|
|
9
|
+
gpuArchitecture?: string;
|
|
10
|
+
/**
|
|
11
|
+
* `UNMASKED_RENDERER_WEBGL` from the WebGL2 probe — the real driver string
|
|
12
|
+
* ("SwiftShader Device", "Mesa Intel(R) UHD Graphics 630"), not the masked
|
|
13
|
+
* one, which is a fixed placeholder. Present only when the probe ran (no
|
|
14
|
+
* WebGPU) and the browser exposes `WEBGL_debug_renderer_info`; Firefox with
|
|
15
|
+
* `privacy.resistFingerprinting` does not. It identifies hardware far more
|
|
16
|
+
* precisely than `gpuVendor`/`gpuArchitecture` do, so like them it stays
|
|
17
|
+
* local — the stack-trace dialog the user chooses to copy — and never goes to
|
|
18
|
+
* analytics. `softwareWebgl` is the coarse bit that does.
|
|
19
|
+
*/
|
|
20
|
+
glRenderer?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether that driver string names a software rasterizer. `undefined` means
|
|
23
|
+
* unknown (no probe, or no extension), which is deliberately not `false`: the
|
|
24
|
+
* distinction is the whole value of the field.
|
|
25
|
+
*/
|
|
26
|
+
softwareWebgl?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The rungs of the rendering ladder, as the names a person reads — the About
|
|
30
|
+
* widget's "Graphics:" line, the stack-trace dialog, the analytics field.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately *not* the {@link GpuOverride} vocabulary, which is what a user
|
|
33
|
+
* types into `?renderer=` and therefore carries lowercase spellings and the
|
|
34
|
+
* `webgl`/`canvas` aliases. One is an input to parse, the other an answer to
|
|
35
|
+
* display, and collapsing them would mean either showing "canvas2d" in the UI
|
|
36
|
+
* or accepting "Canvas2D" in a URL.
|
|
37
|
+
*/
|
|
38
|
+
export type RendererName = 'WebGPU' | 'WebGL2' | 'Canvas2D';
|
|
39
|
+
/**
|
|
40
|
+
* Whether a `UNMASKED_RENDERER_WEBGL` string names a software rasterizer, where
|
|
41
|
+
* WebGL costs ~25x Canvas2D on the main thread for the same session (measured;
|
|
42
|
+
* on real hardware the ordering reverses at ~2x). Exported for its test — the
|
|
43
|
+
* marker list is the part that can be wrong.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isSoftwareRenderer(glRenderer: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* What this machine can do, memoized for the page: capabilities cannot change
|
|
48
|
+
* within a session, and every call that probed again was another WebGL2 context
|
|
49
|
+
* (each reopening of the About widget or the stack-trace dialog made one).
|
|
50
|
+
*
|
|
51
|
+
* **The WebGL2 probe is skipped when WebGPU is available**, leaving `webgl2`
|
|
52
|
+
* undefined, because it is the rung below and nothing reads it:
|
|
53
|
+
* {@link effectiveRenderer} returns WebGPU regardless. So a WebGPU machine
|
|
54
|
+
* creates no WebGL2 context at all.
|
|
55
|
+
*
|
|
56
|
+
* Nothing needs the skipped rung resolved, and a "probe everything" variant was
|
|
57
|
+
* deleted for it: {@link effectiveRenderer} already encodes the whole capability
|
|
58
|
+
* vector, since `Canvas2D` means neither rung exists and `WebGL2` means WebGPU
|
|
59
|
+
* is missing. The only bit a full probe adds is whether a WebGPU machine *also*
|
|
60
|
+
* has WebGL2 — which no shipping browser answers no to, and which nothing would
|
|
61
|
+
* do anything with. Report the effective renderer instead of a list.
|
|
62
|
+
*
|
|
63
|
+
* This is capabilities alone. What will actually draw is
|
|
64
|
+
* {@link effectiveRenderer}, which is this plus the page-wide pin.
|
|
65
|
+
*/
|
|
66
|
+
export declare function getGraphicsCapabilities(): Promise<GraphicsCapabilities>;
|
|
67
|
+
/**
|
|
68
|
+
* Which rung will actually draw — the ladder in `createGpuHal`, read as an
|
|
69
|
+
* answer rather than run. This is the question every consumer asks (the About
|
|
70
|
+
* widget's "Graphics:" line, the stack-trace dialog's environment block, the
|
|
71
|
+
* analytics field), and answering it needs both halves: what the machine can do
|
|
72
|
+
* *and* the page-wide pin from `?renderer=` or the GPU-error banner's "disable
|
|
73
|
+
* GPU" button.
|
|
74
|
+
*
|
|
75
|
+
* The pin is checked first and wins outright, because **a pin never falls
|
|
76
|
+
* through to the next rung** — `createGpuHal` throws instead, on the reasoning
|
|
77
|
+
* that a pinned renderer which silently substitutes another makes any
|
|
78
|
+
* comparison against it wrong. So the pinned rung *is* the answer, and a pin
|
|
79
|
+
* that cannot be honored surfaces as a `renderError` rather than as a different
|
|
80
|
+
* string here. That is what makes this exact rather than a guess: the one case
|
|
81
|
+
* it cannot resolve from capabilities alone (`?renderer=webgl` on a WebGPU
|
|
82
|
+
* machine, where the WebGL2 probe was skipped) is also the case where the pin
|
|
83
|
+
* decides on its own.
|
|
84
|
+
*
|
|
85
|
+
* **`softwareWebgl` skips the WebGL2 rung here because it skips it there**, and
|
|
86
|
+
* only where the ladder skips it — unpinned. Leaving it out is not a rounding
|
|
87
|
+
* error on a rare machine: the VM, the locked-down laptop and the remote desktop
|
|
88
|
+
* are exactly the population the rung-skip exists for, so the one group that
|
|
89
|
+
* newly falls back was the one group still reported as WebGL2, in the About box,
|
|
90
|
+
* in the stack traces users paste into bug reports, and in the analytics field
|
|
91
|
+
* whose stated purpose is counting that fallback.
|
|
92
|
+
*
|
|
93
|
+
* Keeping this beside `gpuDevice` is the point of the module living in
|
|
94
|
+
* render-core: it was previously in `@jbrowse/core`, which cannot see the
|
|
95
|
+
* override, so every consumer reported the capability answer and a user who had
|
|
96
|
+
* clicked "Use Canvas2D" was still reported as WebGL2.
|
|
97
|
+
*/
|
|
98
|
+
export declare function effectiveRenderer(c: GraphicsCapabilities): RendererName;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { getGpuOverride, isGpuRenderingDisabled } from './gpuDevice.js';
|
|
2
|
+
// Substrings of UNMASKED_RENDERER_WEBGL that mean "no GPU is involved". Kept
|
|
3
|
+
// conservative and specific — a wrong `true` here would describe a real GPU as
|
|
4
|
+
// software, and the reports built from it are what a later ladder decision would
|
|
5
|
+
// rest on. Chrome's SwiftShader arrives as an ANGLE string ("ANGLE (Google,
|
|
6
|
+
// Vulkan 1.3.0 (SwiftShader Device ...))"), which is why these are substring
|
|
7
|
+
// tests rather than equality.
|
|
8
|
+
const SOFTWARE_RENDERER_MARKERS = [
|
|
9
|
+
'swiftshader',
|
|
10
|
+
'llvmpipe',
|
|
11
|
+
'lavapipe',
|
|
12
|
+
'softpipe',
|
|
13
|
+
'software rasterizer',
|
|
14
|
+
'software renderer',
|
|
15
|
+
'microsoft basic render driver',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Whether a `UNMASKED_RENDERER_WEBGL` string names a software rasterizer, where
|
|
19
|
+
* WebGL costs ~25x Canvas2D on the main thread for the same session (measured;
|
|
20
|
+
* on real hardware the ordering reverses at ~2x). Exported for its test — the
|
|
21
|
+
* marker list is the part that can be wrong.
|
|
22
|
+
*/
|
|
23
|
+
export function isSoftwareRenderer(glRenderer) {
|
|
24
|
+
const lower = glRenderer.toLowerCase();
|
|
25
|
+
return SOFTWARE_RENDERER_MARKERS.some(marker => lower.includes(marker));
|
|
26
|
+
}
|
|
27
|
+
async function probeWebgpu() {
|
|
28
|
+
try {
|
|
29
|
+
// navigator.gpu is typed non-nullable but is undefined without WebGPU
|
|
30
|
+
// support, so the try/catch guards that access as well as adapter failures
|
|
31
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
32
|
+
return adapter
|
|
33
|
+
? {
|
|
34
|
+
webgpu: true,
|
|
35
|
+
// coarse, non-fingerprinting fields (e.g. "nvidia"/"apple") the
|
|
36
|
+
// browser exposes on purpose — surfaced only in the local stack-trace
|
|
37
|
+
// dialog
|
|
38
|
+
gpuVendor: adapter.info.vendor,
|
|
39
|
+
gpuArchitecture: adapter.info.architecture,
|
|
40
|
+
}
|
|
41
|
+
: { webgpu: false };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { webgpu: false };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a WebGL2 context can be created, which costs one. The context is not
|
|
49
|
+
* released with `WEBGL_lose_context.loseContext()`, deliberately: on Firefox
|
|
50
|
+
* that call is effectively driver-wide (ADR-005 removed it from
|
|
51
|
+
* `WebGL2Hal.dispose()` for the same reason), so probing while tracks were on
|
|
52
|
+
* screen knocked out their live sibling contexts — and both browsers log the
|
|
53
|
+
* loss to the console, which is what a user sees. The canvas is unreachable the
|
|
54
|
+
* moment this returns and the browser reclaims the context on GC, the same
|
|
55
|
+
* release the HAL relies on. A page that then reaches the 16-context ceiling
|
|
56
|
+
* evicts this one first (it is the oldest, and nothing draws to it or
|
|
57
|
+
* re-acquires it), so the eviction cascade in GPU_CONTEXT_BUDGET.md cannot
|
|
58
|
+
* start here.
|
|
59
|
+
*
|
|
60
|
+
* The driver string comes off the same context, so it is free where it matters:
|
|
61
|
+
* this only runs when WebGPU is absent, which is the population that renders on
|
|
62
|
+
* WebGL2 and so the only one whose rasterizer changes anything.
|
|
63
|
+
*/
|
|
64
|
+
function probeWebgl2() {
|
|
65
|
+
const canvas = document.createElement('canvas');
|
|
66
|
+
// 1x1 rather than the default 300x150: nothing is ever drawn, so the drawing
|
|
67
|
+
// buffer is pure waste until GC takes it
|
|
68
|
+
canvas.width = 1;
|
|
69
|
+
canvas.height = 1;
|
|
70
|
+
const gl = canvas.getContext('webgl2');
|
|
71
|
+
if (!gl) {
|
|
72
|
+
return { webgl2: false };
|
|
73
|
+
}
|
|
74
|
+
// UNMASKED_RENDERER_WEBGL, not RENDERER — the masked one is a fixed
|
|
75
|
+
// placeholder string ("WebKit WebGL") that names nothing
|
|
76
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
77
|
+
const glRenderer = ext
|
|
78
|
+
? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
|
|
79
|
+
: null;
|
|
80
|
+
return glRenderer
|
|
81
|
+
? {
|
|
82
|
+
webgl2: true,
|
|
83
|
+
glRenderer,
|
|
84
|
+
softwareWebgl: isSoftwareRenderer(glRenderer),
|
|
85
|
+
}
|
|
86
|
+
: { webgl2: true };
|
|
87
|
+
}
|
|
88
|
+
let capabilities;
|
|
89
|
+
/**
|
|
90
|
+
* What this machine can do, memoized for the page: capabilities cannot change
|
|
91
|
+
* within a session, and every call that probed again was another WebGL2 context
|
|
92
|
+
* (each reopening of the About widget or the stack-trace dialog made one).
|
|
93
|
+
*
|
|
94
|
+
* **The WebGL2 probe is skipped when WebGPU is available**, leaving `webgl2`
|
|
95
|
+
* undefined, because it is the rung below and nothing reads it:
|
|
96
|
+
* {@link effectiveRenderer} returns WebGPU regardless. So a WebGPU machine
|
|
97
|
+
* creates no WebGL2 context at all.
|
|
98
|
+
*
|
|
99
|
+
* Nothing needs the skipped rung resolved, and a "probe everything" variant was
|
|
100
|
+
* deleted for it: {@link effectiveRenderer} already encodes the whole capability
|
|
101
|
+
* vector, since `Canvas2D` means neither rung exists and `WebGL2` means WebGPU
|
|
102
|
+
* is missing. The only bit a full probe adds is whether a WebGPU machine *also*
|
|
103
|
+
* has WebGL2 — which no shipping browser answers no to, and which nothing would
|
|
104
|
+
* do anything with. Report the effective renderer instead of a list.
|
|
105
|
+
*
|
|
106
|
+
* This is capabilities alone. What will actually draw is
|
|
107
|
+
* {@link effectiveRenderer}, which is this plus the page-wide pin.
|
|
108
|
+
*/
|
|
109
|
+
export function getGraphicsCapabilities() {
|
|
110
|
+
capabilities ??= probeWebgpu().then(gpu => gpu.webgpu ? gpu : { ...gpu, ...probeWebgl2() });
|
|
111
|
+
return capabilities;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Which rung will actually draw — the ladder in `createGpuHal`, read as an
|
|
115
|
+
* answer rather than run. This is the question every consumer asks (the About
|
|
116
|
+
* widget's "Graphics:" line, the stack-trace dialog's environment block, the
|
|
117
|
+
* analytics field), and answering it needs both halves: what the machine can do
|
|
118
|
+
* *and* the page-wide pin from `?renderer=` or the GPU-error banner's "disable
|
|
119
|
+
* GPU" button.
|
|
120
|
+
*
|
|
121
|
+
* The pin is checked first and wins outright, because **a pin never falls
|
|
122
|
+
* through to the next rung** — `createGpuHal` throws instead, on the reasoning
|
|
123
|
+
* that a pinned renderer which silently substitutes another makes any
|
|
124
|
+
* comparison against it wrong. So the pinned rung *is* the answer, and a pin
|
|
125
|
+
* that cannot be honored surfaces as a `renderError` rather than as a different
|
|
126
|
+
* string here. That is what makes this exact rather than a guess: the one case
|
|
127
|
+
* it cannot resolve from capabilities alone (`?renderer=webgl` on a WebGPU
|
|
128
|
+
* machine, where the WebGL2 probe was skipped) is also the case where the pin
|
|
129
|
+
* decides on its own.
|
|
130
|
+
*
|
|
131
|
+
* **`softwareWebgl` skips the WebGL2 rung here because it skips it there**, and
|
|
132
|
+
* only where the ladder skips it — unpinned. Leaving it out is not a rounding
|
|
133
|
+
* error on a rare machine: the VM, the locked-down laptop and the remote desktop
|
|
134
|
+
* are exactly the population the rung-skip exists for, so the one group that
|
|
135
|
+
* newly falls back was the one group still reported as WebGL2, in the About box,
|
|
136
|
+
* in the stack traces users paste into bug reports, and in the analytics field
|
|
137
|
+
* whose stated purpose is counting that fallback.
|
|
138
|
+
*
|
|
139
|
+
* Keeping this beside `gpuDevice` is the point of the module living in
|
|
140
|
+
* render-core: it was previously in `@jbrowse/core`, which cannot see the
|
|
141
|
+
* override, so every consumer reported the capability answer and a user who had
|
|
142
|
+
* clicked "Use Canvas2D" was still reported as WebGL2.
|
|
143
|
+
*/
|
|
144
|
+
export function effectiveRenderer(c) {
|
|
145
|
+
if (isGpuRenderingDisabled()) {
|
|
146
|
+
return 'Canvas2D';
|
|
147
|
+
}
|
|
148
|
+
const override = getGpuOverride();
|
|
149
|
+
if (override === 'webgl') {
|
|
150
|
+
return 'WebGL2';
|
|
151
|
+
}
|
|
152
|
+
if (override === 'webgpu') {
|
|
153
|
+
return 'WebGPU';
|
|
154
|
+
}
|
|
155
|
+
if (c.webgpu) {
|
|
156
|
+
return 'WebGPU';
|
|
157
|
+
}
|
|
158
|
+
// `softwareWebgl` is `undefined` where the browser withholds
|
|
159
|
+
// WEBGL_debug_renderer_info, and an unknown rasterizer keeps the rung — the
|
|
160
|
+
// same reading `createGpuHal` gives it.
|
|
161
|
+
if (c.webgl2 && !c.softwareWebgl) {
|
|
162
|
+
return 'WebGL2';
|
|
163
|
+
}
|
|
164
|
+
return 'Canvas2D';
|
|
165
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GpuHal, PipelineDescriptor, SampleCount } from './types.ts';
|
|
2
|
+
export interface GpuHalOptions {
|
|
3
|
+
passes: PipelineDescriptor[];
|
|
4
|
+
uniformByteSize: number;
|
|
5
|
+
/**
|
|
6
|
+
* Reaches the WebGPU rung only. The WebGL2 rung draws to the default
|
|
7
|
+
* framebuffer with `antialias: true`, so its multisampling is the browser's
|
|
8
|
+
* own and there is nothing here to thread into it.
|
|
9
|
+
*/
|
|
10
|
+
sampleCount: SampleCount;
|
|
11
|
+
/**
|
|
12
|
+
* Out-parameter collecting why each rung declined, in ladder order. Falling
|
|
13
|
+
* through a rung is normal (no WebGPU on this machine) and stays a
|
|
14
|
+
* `console.warn` — but when the *last* rung fails too, the reasons the earlier
|
|
15
|
+
* ones gave are the diagnosis, and they used to exist only in a console the
|
|
16
|
+
* user reporting the bug never opens. `createRenderingBackend` attaches this
|
|
17
|
+
* to the error it throws, and the error UI's stack-trace dialog already walks
|
|
18
|
+
* `AggregateError.errors` (core's `formatErrorStack`), so they arrive with it.
|
|
19
|
+
*/
|
|
20
|
+
failures?: unknown[];
|
|
21
|
+
}
|
|
22
|
+
export declare function createGpuHal(canvas: HTMLCanvasElement, { passes, uniformByteSize, sampleCount, failures }: GpuHalOptions): Promise<GpuHal | null>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getGpuOverride, isGpuRenderingDisabled } from '../gpuDevice.js';
|
|
2
|
+
import { getGraphicsCapabilities } from '../graphicsCapabilities.js';
|
|
3
|
+
import { WebGL2Hal } from './webgl2Hal.js';
|
|
4
|
+
import { WebGPUHal } from './webgpuHal.js';
|
|
5
|
+
// Per copy of this module rather than on the globalThis cell, deliberately: a
|
|
6
|
+
// second bundled copy warning a second time is a duplicate console line, which
|
|
7
|
+
// is the "a memo per copy is fine" case ADR-030's amendment carves out. Nothing
|
|
8
|
+
// reads it.
|
|
9
|
+
let warnedSoftwareRasterizer = false;
|
|
10
|
+
function warnSoftwareRasterizerOnce(glRenderer) {
|
|
11
|
+
if (warnedSoftwareRasterizer) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
warnedSoftwareRasterizer = true;
|
|
15
|
+
console.warn(`[GPU] WebGL2 here is software-rendered (${glRenderer ?? 'unknown driver'}), where it costs several times Canvas2D on the main thread — rendering with Canvas2D instead. Pass ?renderer=webgl to use WebGL2 anyway.`);
|
|
16
|
+
}
|
|
17
|
+
// Ladder: WebGPU → WebGL2 → Canvas2D (null). The `?renderer=` URL param pins it
|
|
18
|
+
// to a single rung for debugging — `webgpu`, `webgl`, or `canvas2d`/`canvas` —
|
|
19
|
+
// and a pin never falls through to the next rung. See GPU_OVERRIDES.
|
|
20
|
+
export async function createGpuHal(canvas, { passes, uniformByteSize, sampleCount, failures }) {
|
|
21
|
+
if (isGpuRenderingDisabled()) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const override = getGpuOverride();
|
|
25
|
+
if (override !== 'webgl') {
|
|
26
|
+
try {
|
|
27
|
+
const webgpu = await WebGPUHal.create(canvas, passes, uniformByteSize, sampleCount);
|
|
28
|
+
if (webgpu) {
|
|
29
|
+
return webgpu;
|
|
30
|
+
}
|
|
31
|
+
// `create` returns null only for "no WebGPU device on this machine",
|
|
32
|
+
// which is the ordinary path on most hardware and not worth recording as
|
|
33
|
+
// a failure; every other way it can decline now throws.
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.warn('[GPU] WebGPU init failed, falling back to WebGL2:', e);
|
|
37
|
+
failures?.push(e);
|
|
38
|
+
}
|
|
39
|
+
// A pin is not a preference. `?renderer=webgpu` used to be indistinguishable
|
|
40
|
+
// from passing nothing — WebGPU is the first rung either way — so on a
|
|
41
|
+
// machine where it declined, the page rendered on WebGL2 and said nothing,
|
|
42
|
+
// and anyone using the flag to compare the two backends measured WebGL2
|
|
43
|
+
// twice. Fail where it can be seen instead. The other pins already behave
|
|
44
|
+
// this way: `webgl` skips this rung, `canvas2d` returns null above.
|
|
45
|
+
if (override === 'webgpu') {
|
|
46
|
+
const message = 'WebGPU was pinned with ?renderer=webgpu but no WebGPU backend could be created. Not falling back to WebGL2 — a pinned renderer that silently substitutes another makes any comparison against it wrong. Drop the parameter for the default WebGPU → WebGL2 → Canvas2D ladder.';
|
|
47
|
+
throw failures?.length
|
|
48
|
+
? new AggregateError(failures, message)
|
|
49
|
+
: new Error(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// The one machine where the WebGL2 rung is the wrong answer even though it
|
|
53
|
+
// works: a software rasterizer. Measured on one ordinary view — 1 view, 3
|
|
54
|
+
// volvox tracks, and *no scroll churn at all* — headless Chrome (SwiftShader),
|
|
55
|
+
// three runs each: WebGL2 blocks the main thread for 1.3-5.5 s in a single
|
|
56
|
+
// task and 2.1-8.8 s in total, while Canvas2D never exceeds 339 ms and never
|
|
57
|
+
// once produces a task over 500 ms. GPU_CONTEXT_BUDGET.md has the churn case
|
|
58
|
+
// at ~25x; this is the floor, and it is the load-time pipeline build rather
|
|
59
|
+
// than the per-pass rebuild the churn number measures.
|
|
60
|
+
//
|
|
61
|
+
// Stepping over the rung rather than pinning the page is the difference that
|
|
62
|
+
// matters: `gpuOverride` means "a human asked for this", and spending it on a
|
|
63
|
+
// decision the app made would leave nothing able to tell the two apart —
|
|
64
|
+
// including the About widget and the bug report the user is about to send.
|
|
65
|
+
//
|
|
66
|
+
// Only when nothing was pinned. `?renderer=webgl` means that rung, and the
|
|
67
|
+
// browser-test runner sets it for every GPU arm (`appendGpuParam`), so the
|
|
68
|
+
// cross-backend gate keeps comparing canvas2d against a real WebGL2 render
|
|
69
|
+
// rather than against a second Canvas2D one — it runs under SwiftShader, so
|
|
70
|
+
// without that pin this check would quietly make it compare a backend with
|
|
71
|
+
// itself and pass.
|
|
72
|
+
//
|
|
73
|
+
// `softwareWebgl` is `undefined` wherever the browser withholds
|
|
74
|
+
// WEBGL_debug_renderer_info (Firefox under privacy.resistFingerprinting), and
|
|
75
|
+
// that must not read as `true`: an unknown rasterizer keeps the WebGL2 rung.
|
|
76
|
+
if (override === null) {
|
|
77
|
+
const { softwareWebgl, glRenderer } = await getGraphicsCapabilities();
|
|
78
|
+
if (softwareWebgl) {
|
|
79
|
+
warnSoftwareRasterizerOnce(glRenderer);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return new WebGL2Hal(canvas, passes, uniformByteSize);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.warn('[GPU] WebGL2 unavailable, falling back to Canvas2D:', e);
|
|
88
|
+
failures?.push(e);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PipelineDescriptor, SampleCount } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The two pipeline layouts every pass is built against. Both are created
|
|
4
|
+
* eagerly now — the textured pair used to be lazy so that a display drawing no
|
|
5
|
+
* textured pass paid nothing, which was worth two objects per *display* and is
|
|
6
|
+
* not worth the null-bundling for two objects per *device*.
|
|
7
|
+
*/
|
|
8
|
+
export interface DeviceLayouts {
|
|
9
|
+
uniformOnlyBindGroupLayout: GPUBindGroupLayout;
|
|
10
|
+
uniformOnlyPipelineLayout: GPUPipelineLayout;
|
|
11
|
+
texturedBindGroupLayout: GPUBindGroupLayout;
|
|
12
|
+
texturedPipelineLayout: GPUPipelineLayout;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The bind group and pipeline layouts for `device`, built once per device.
|
|
16
|
+
*
|
|
17
|
+
* A device that is lost drops out of the map with itself — `gpuDevice.ts`
|
|
18
|
+
* releases its reference in the `.lost` handler and the next acquisition is a
|
|
19
|
+
* new object, so the layouts and pipelines built against the dead one are
|
|
20
|
+
* unreachable and there is no cache to invalidate by hand.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getDeviceLayouts(device: GPUDevice): DeviceLayouts;
|
|
23
|
+
/**
|
|
24
|
+
* The pipeline for `desc` at `sampleCount` on `device`, building it through
|
|
25
|
+
* `build` on the first ask and handing every later one the same promise.
|
|
26
|
+
*
|
|
27
|
+
* `build` receives the sample count rather than closing over one, so the key
|
|
28
|
+
* and the pipeline stored under it cannot disagree.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getOrBuildPipeline(device: GPUDevice, desc: PipelineDescriptor, sampleCount: SampleCount, build: (layouts: DeviceLayouts, sampleCount: SampleCount) => Promise<GPURenderPipeline>): Promise<GPURenderPipeline>;
|
|
31
|
+
/**
|
|
32
|
+
* Drop every device's entry. Tests only — nothing in the app invalidates this
|
|
33
|
+
* cache, because losing the device is what invalidates it (see
|
|
34
|
+
* {@link getDeviceLayouts}).
|
|
35
|
+
*/
|
|
36
|
+
export declare function resetDeviceGpuCacheForTests(device: GPUDevice): void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/// <reference types="@webgpu/types" />
|
|
2
|
+
/**
|
|
3
|
+
* The GPU objects that belong to the **device** rather than to one `WebGPUHal`,
|
|
4
|
+
* memoized so the second display of a track type builds none of them.
|
|
5
|
+
*
|
|
6
|
+
* WebGL2 has no equivalent and wants none: a program is owned by the context
|
|
7
|
+
* that linked it, and each display owns a context, so nothing there is
|
|
8
|
+
* shareable. WebGPU inverts that — `gpuDevice.ts` hands every display one
|
|
9
|
+
* device, and a `GPURenderPipeline` is a device object — and until this module
|
|
10
|
+
* the HAL kept building per-display copies anyway. Ten alignments tracks
|
|
11
|
+
* compiled 230 pipelines for 23 distinct programs.
|
|
12
|
+
*
|
|
13
|
+
* **The key is the descriptor object, and that is what makes it correct.** A
|
|
14
|
+
* plugin's pass list is a module-level const (`ALIGNMENTS_PASSES`) and
|
|
15
|
+
* `slangPass` reads `wgslSource` off a generated module const, so every display
|
|
16
|
+
* of a type hands the HAL the *same* `PipelineDescriptor` objects by reference.
|
|
17
|
+
* Identity therefore means "same shader, same layout, same blend, same
|
|
18
|
+
* topology" without comparing any of them, and two passes that merely share a
|
|
19
|
+
* `.slang` shape module (MAF and multi-row both draw `rowRect`) get separate
|
|
20
|
+
* entries because `slangPass` built them separate objects. The **sample count**
|
|
21
|
+
* is the one pipeline input the descriptor cannot express, so it is the inner
|
|
22
|
+
* key; see the `pipelines` field.
|
|
23
|
+
*
|
|
24
|
+
* The one field of a pipeline that is not on the descriptor is the uniform
|
|
25
|
+
* size, and it cannot make two HALs disagree here:
|
|
26
|
+
* `createUniformOnlyBindGroupLayout` declares no `minBindingSize`, so the
|
|
27
|
+
* layout — and thus the pipeline built against it — is the same whatever a
|
|
28
|
+
* display's `uniformByteSize` is. The size reaches the GPU through the bind
|
|
29
|
+
* group's `size` and the dynamic offset, both of which stay per-HAL.
|
|
30
|
+
*
|
|
31
|
+
* Per copy of this module rather than on the `globalThis` cell, for the reason
|
|
32
|
+
* `createHal.ts` gives for `warnedSoftwareRasterizer`: a second bundled copy
|
|
33
|
+
* builds a second set, which costs a duplicate and never a wrong answer.
|
|
34
|
+
* Layouts compare structurally in WebGPU, so a bind group from one copy is
|
|
35
|
+
* valid against a pipeline from the other.
|
|
36
|
+
*/
|
|
37
|
+
const perDevice = new WeakMap();
|
|
38
|
+
function createLayouts(device) {
|
|
39
|
+
const uniformEntry = {
|
|
40
|
+
binding: 1,
|
|
41
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
42
|
+
buffer: { type: 'uniform', hasDynamicOffset: true },
|
|
43
|
+
};
|
|
44
|
+
const uniformOnlyBindGroupLayout = device.createBindGroupLayout({
|
|
45
|
+
entries: [uniformEntry],
|
|
46
|
+
});
|
|
47
|
+
const texturedBindGroupLayout = device.createBindGroupLayout({
|
|
48
|
+
entries: [
|
|
49
|
+
uniformEntry,
|
|
50
|
+
{
|
|
51
|
+
binding: 2,
|
|
52
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
53
|
+
texture: { sampleType: 'float' },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
binding: 3,
|
|
57
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
58
|
+
sampler: { type: 'filtering' },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
uniformOnlyBindGroupLayout,
|
|
64
|
+
uniformOnlyPipelineLayout: device.createPipelineLayout({
|
|
65
|
+
bindGroupLayouts: [uniformOnlyBindGroupLayout],
|
|
66
|
+
}),
|
|
67
|
+
texturedBindGroupLayout,
|
|
68
|
+
texturedPipelineLayout: device.createPipelineLayout({
|
|
69
|
+
bindGroupLayouts: [texturedBindGroupLayout],
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function cacheFor(device) {
|
|
74
|
+
let entry = perDevice.get(device);
|
|
75
|
+
if (!entry) {
|
|
76
|
+
entry = { layouts: createLayouts(device), pipelines: new WeakMap() };
|
|
77
|
+
perDevice.set(device, entry);
|
|
78
|
+
}
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The bind group and pipeline layouts for `device`, built once per device.
|
|
83
|
+
*
|
|
84
|
+
* A device that is lost drops out of the map with itself — `gpuDevice.ts`
|
|
85
|
+
* releases its reference in the `.lost` handler and the next acquisition is a
|
|
86
|
+
* new object, so the layouts and pipelines built against the dead one are
|
|
87
|
+
* unreachable and there is no cache to invalidate by hand.
|
|
88
|
+
*/
|
|
89
|
+
export function getDeviceLayouts(device) {
|
|
90
|
+
return cacheFor(device).layouts;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The pipeline for `desc` at `sampleCount` on `device`, building it through
|
|
94
|
+
* `build` on the first ask and handing every later one the same promise.
|
|
95
|
+
*
|
|
96
|
+
* `build` receives the sample count rather than closing over one, so the key
|
|
97
|
+
* and the pipeline stored under it cannot disagree.
|
|
98
|
+
*/
|
|
99
|
+
export function getOrBuildPipeline(device, desc, sampleCount, build) {
|
|
100
|
+
const { layouts, pipelines } = cacheFor(device);
|
|
101
|
+
let bySampleCount = pipelines.get(desc);
|
|
102
|
+
if (!bySampleCount) {
|
|
103
|
+
bySampleCount = new Map();
|
|
104
|
+
pipelines.set(desc, bySampleCount);
|
|
105
|
+
}
|
|
106
|
+
let pipeline = bySampleCount.get(sampleCount);
|
|
107
|
+
if (!pipeline) {
|
|
108
|
+
pipeline = build(layouts, sampleCount);
|
|
109
|
+
bySampleCount.set(sampleCount, pipeline);
|
|
110
|
+
}
|
|
111
|
+
return pipeline;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Drop every device's entry. Tests only — nothing in the app invalidates this
|
|
115
|
+
* cache, because losing the device is what invalidates it (see
|
|
116
|
+
* {@link getDeviceLayouts}).
|
|
117
|
+
*/
|
|
118
|
+
export function resetDeviceGpuCacheForTests(device) {
|
|
119
|
+
perDevice.delete(device);
|
|
120
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { OomReporter } from './oomReporter.ts';
|
|
2
|
+
import { RegionRegistry } from './regionRegistry.ts';
|
|
3
|
+
import type { PipelineDescriptor, TextureBinding } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The device ceilings the shared upload shells refuse past. WebGPU asks the
|
|
6
|
+
* device, WebGL2 has no such parameter for buffers and carries its own constant
|
|
7
|
+
* (see `MAX_VERTEX_BUFFER_BYTES`), and `MockHal` has none at all.
|
|
8
|
+
*/
|
|
9
|
+
export interface HalLimits {
|
|
10
|
+
maxBufferBytes: number;
|
|
11
|
+
maxTextureDimensionPx: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The half of a HAL that is not per-backend: the descriptor map, the
|
|
15
|
+
* `(region, pass)` buffer registry and the upload/delete semantics over it, the
|
|
16
|
+
* over-limit refusals and their wording, and the once-only `dispose` guard.
|
|
17
|
+
*
|
|
18
|
+
* `packages/render-core/CLAUDE.md` states HAL parity as a rule ("a behavior
|
|
19
|
+
* change to one HAL lands in the other and in `MockHal`"); this class is the
|
|
20
|
+
* structural half of it. What stays in the leaves is what genuinely differs —
|
|
21
|
+
* the frame bracket, uniform ring vs UBO, scissor Y-flip, MSAA, lazy vs eager
|
|
22
|
+
* pipeline build, and WebGPU's deferred destroy, which the `destroyBuffer` hook
|
|
23
|
+
* keeps WebGPU's alone.
|
|
24
|
+
*
|
|
25
|
+
* A backend's `Buf` carries its own leaf handle plus the instance `count` every
|
|
26
|
+
* caller reads through `getBufferCount`.
|
|
27
|
+
*/
|
|
28
|
+
export declare abstract class GpuHalBase<Buf extends {
|
|
29
|
+
count: number;
|
|
30
|
+
}> {
|
|
31
|
+
protected descriptors: Map<string, PipelineDescriptor>;
|
|
32
|
+
protected regions: RegionRegistry<Buf>;
|
|
33
|
+
protected oom: OomReporter;
|
|
34
|
+
protected disposed: boolean;
|
|
35
|
+
constructor(descriptors: PipelineDescriptor[], halName: string);
|
|
36
|
+
protected abstract limits(): HalLimits;
|
|
37
|
+
protected abstract createBuffer(data: ArrayBuffer | ArrayBufferView, count: number): Buf;
|
|
38
|
+
protected abstract destroyBuffer(buf: Buf): void;
|
|
39
|
+
/**
|
|
40
|
+
* Replace the texture bound to `passId`, releasing whatever was there. Only
|
|
41
|
+
* reached for a pass that declares a binding, and only within
|
|
42
|
+
* `maxTextureDimensionPx`.
|
|
43
|
+
*/
|
|
44
|
+
protected abstract createTexture(passId: string, binding: TextureBinding, data: Uint8Array, width: number, height: number): void;
|
|
45
|
+
/** Release this HAL's own GPU objects. Runs exactly once, from `dispose`. */
|
|
46
|
+
protected abstract releaseResources(): void;
|
|
47
|
+
setErrorHandler(handler: (error: Error) => void): void;
|
|
48
|
+
uploadBuffer(regionKey: number, passId: string, data: ArrayBuffer | ArrayBufferView, count: number): void;
|
|
49
|
+
getBufferCount(regionKey: number, passId: string): number;
|
|
50
|
+
deleteBuffer(regionKey: number, passId: string): void;
|
|
51
|
+
deleteRegion(regionKey: number): void;
|
|
52
|
+
pruneRegions(active: Iterable<number>): void;
|
|
53
|
+
uploadTexture(passId: string, data: Uint8Array, width: number, height: number): void;
|
|
54
|
+
dispose(): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { OomReporter } from './oomReporter.js';
|
|
2
|
+
import { RegionRegistry } from './regionRegistry.js';
|
|
3
|
+
/**
|
|
4
|
+
* The half of a HAL that is not per-backend: the descriptor map, the
|
|
5
|
+
* `(region, pass)` buffer registry and the upload/delete semantics over it, the
|
|
6
|
+
* over-limit refusals and their wording, and the once-only `dispose` guard.
|
|
7
|
+
*
|
|
8
|
+
* `packages/render-core/CLAUDE.md` states HAL parity as a rule ("a behavior
|
|
9
|
+
* change to one HAL lands in the other and in `MockHal`"); this class is the
|
|
10
|
+
* structural half of it. What stays in the leaves is what genuinely differs —
|
|
11
|
+
* the frame bracket, uniform ring vs UBO, scissor Y-flip, MSAA, lazy vs eager
|
|
12
|
+
* pipeline build, and WebGPU's deferred destroy, which the `destroyBuffer` hook
|
|
13
|
+
* keeps WebGPU's alone.
|
|
14
|
+
*
|
|
15
|
+
* A backend's `Buf` carries its own leaf handle plus the instance `count` every
|
|
16
|
+
* caller reads through `getBufferCount`.
|
|
17
|
+
*/
|
|
18
|
+
export class GpuHalBase {
|
|
19
|
+
descriptors;
|
|
20
|
+
regions;
|
|
21
|
+
oom;
|
|
22
|
+
// Guards dispose() against double invocation (pagehide + React cleanup can
|
|
23
|
+
// both fire).
|
|
24
|
+
disposed = false;
|
|
25
|
+
constructor(descriptors, halName) {
|
|
26
|
+
this.descriptors = new Map(descriptors.map(d => [d.id, d]));
|
|
27
|
+
this.oom = new OomReporter(halName);
|
|
28
|
+
this.regions = new RegionRegistry(buf => {
|
|
29
|
+
this.destroyBuffer(buf);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
setErrorHandler(handler) {
|
|
33
|
+
this.oom.setHandler(handler);
|
|
34
|
+
}
|
|
35
|
+
uploadBuffer(regionKey, passId, data, count) {
|
|
36
|
+
// The prior buffer goes before the count is looked at, which is what makes
|
|
37
|
+
// an empty upload the release rather than a no-op.
|
|
38
|
+
this.regions.deleteBuffer(regionKey, passId);
|
|
39
|
+
if (count > 0) {
|
|
40
|
+
const { maxBufferBytes } = this.limits();
|
|
41
|
+
if (data.byteLength > maxBufferBytes) {
|
|
42
|
+
this.oom.report(`This region has too much data to render on this GPU — zoom in. (vertex buffer ${data.byteLength} bytes exceeds the ${maxBufferBytes}-byte limit)`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.regions.set(regionKey, passId, this.createBuffer(data, count));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
getBufferCount(regionKey, passId) {
|
|
50
|
+
return this.regions.get(regionKey, passId)?.count ?? 0;
|
|
51
|
+
}
|
|
52
|
+
deleteBuffer(regionKey, passId) {
|
|
53
|
+
this.regions.deleteBuffer(regionKey, passId);
|
|
54
|
+
}
|
|
55
|
+
deleteRegion(regionKey) {
|
|
56
|
+
this.regions.deleteRegion(regionKey);
|
|
57
|
+
}
|
|
58
|
+
pruneRegions(active) {
|
|
59
|
+
this.regions.prune(active);
|
|
60
|
+
}
|
|
61
|
+
uploadTexture(passId, data, width, height) {
|
|
62
|
+
// Read the binding off the descriptor, so a pass with no texture is
|
|
63
|
+
// answered without touching the backend's pipeline state at all.
|
|
64
|
+
const binding = this.descriptors.get(passId)?.textures?.[0];
|
|
65
|
+
if (binding) {
|
|
66
|
+
const { maxTextureDimensionPx } = this.limits();
|
|
67
|
+
if (width > maxTextureDimensionPx || height > maxTextureDimensionPx) {
|
|
68
|
+
this.oom.report(`This region is too large to render on this GPU — zoom in. (texture ${width}×${height} exceeds max texture size ${maxTextureDimensionPx})`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.createTexture(passId, binding, data, width, height);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
dispose() {
|
|
76
|
+
if (!this.disposed) {
|
|
77
|
+
this.disposed = true;
|
|
78
|
+
this.releaseResources();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|