@jbrowse/render-core 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +201 -0
  2. package/esm/OverlayCanvas.d.ts +29 -0
  3. package/esm/OverlayCanvas.js +43 -0
  4. package/esm/RenderCanvas.d.ts +76 -0
  5. package/esm/RenderCanvas.js +38 -0
  6. package/esm/RenderLifecycleMixin.d.ts +180 -0
  7. package/esm/RenderLifecycleMixin.js +279 -0
  8. package/esm/ScrollLockedOverlay.d.ts +28 -0
  9. package/esm/ScrollLockedOverlay.js +42 -0
  10. package/esm/blockClipUtils.d.ts +27 -0
  11. package/esm/blockClipUtils.js +101 -0
  12. package/esm/canvas2dUtils.d.ts +372 -0
  13. package/esm/canvas2dUtils.js +564 -0
  14. package/esm/canvasContext.d.ts +68 -0
  15. package/esm/canvasContext.js +74 -0
  16. package/esm/colorRampLut.d.ts +17 -0
  17. package/esm/colorRampLut.js +25 -0
  18. package/esm/coverageBand.d.ts +107 -0
  19. package/esm/coverageBand.js +109 -0
  20. package/esm/coverageBandBuffers.d.ts +30 -0
  21. package/esm/coverageBandBuffers.js +13 -0
  22. package/esm/createRenderingBackend.d.ts +52 -0
  23. package/esm/createRenderingBackend.js +58 -0
  24. package/esm/displayPhase.d.ts +155 -0
  25. package/esm/displayPhase.js +97 -0
  26. package/esm/globalRenderingBackend.d.ts +90 -0
  27. package/esm/globalRenderingBackend.js +53 -0
  28. package/esm/gpuDevice.d.ts +91 -0
  29. package/esm/gpuDevice.js +277 -0
  30. package/esm/graphicsCapabilities.d.ts +98 -0
  31. package/esm/graphicsCapabilities.js +165 -0
  32. package/esm/hal/createHal.d.ts +22 -0
  33. package/esm/hal/createHal.js +91 -0
  34. package/esm/hal/deviceGpuCache.d.ts +36 -0
  35. package/esm/hal/deviceGpuCache.js +120 -0
  36. package/esm/hal/gpuHalBase.d.ts +55 -0
  37. package/esm/hal/gpuHalBase.js +81 -0
  38. package/esm/hal/index.d.ts +7 -0
  39. package/esm/hal/index.js +5 -0
  40. package/esm/hal/mockHal.d.ts +153 -0
  41. package/esm/hal/mockHal.js +297 -0
  42. package/esm/hal/oomReporter.d.ts +15 -0
  43. package/esm/hal/oomReporter.js +22 -0
  44. package/esm/hal/passIds.d.ts +22 -0
  45. package/esm/hal/passIds.js +36 -0
  46. package/esm/hal/regionRegistry.d.ts +11 -0
  47. package/esm/hal/regionRegistry.js +66 -0
  48. package/esm/hal/types.d.ts +116 -0
  49. package/esm/hal/types.js +1 -0
  50. package/esm/hal/webgl2Hal.d.ts +47 -0
  51. package/esm/hal/webgl2Hal.js +490 -0
  52. package/esm/hal/webgpuHal.d.ts +169 -0
  53. package/esm/hal/webgpuHal.js +722 -0
  54. package/esm/installUpload.d.ts +91 -0
  55. package/esm/installUpload.js +94 -0
  56. package/esm/instanceCache.d.ts +43 -0
  57. package/esm/instanceCache.js +53 -0
  58. package/esm/instancePass.d.ts +50 -0
  59. package/esm/instancePass.js +34 -0
  60. package/esm/keyedRenderingBackend.d.ts +41 -0
  61. package/esm/keyedRenderingBackend.js +19 -0
  62. package/esm/mapUploadSync.d.ts +31 -0
  63. package/esm/mapUploadSync.js +45 -0
  64. package/esm/namedReactions.d.ts +29 -0
  65. package/esm/namedReactions.js +62 -0
  66. package/esm/perRegionRenderingBackend.d.ts +114 -0
  67. package/esm/perRegionRenderingBackend.js +93 -0
  68. package/esm/recoveryBudget.d.ts +40 -0
  69. package/esm/recoveryBudget.js +56 -0
  70. package/esm/regionDataMap.d.ts +60 -0
  71. package/esm/regionDataMap.js +98 -0
  72. package/esm/renderBlock.d.ts +25 -0
  73. package/esm/renderBlock.js +10 -0
  74. package/esm/renderingBackendBase.d.ts +71 -0
  75. package/esm/renderingBackendBase.js +50 -0
  76. package/esm/shaders/antialias.js.generated.d.ts +4 -0
  77. package/esm/shaders/antialias.js.generated.js +25 -0
  78. package/esm/shaders/capsule.generated.d.ts +1 -0
  79. package/esm/shaders/capsule.generated.js +3 -0
  80. package/esm/shaders/colorRampLut.generated.d.ts +1 -0
  81. package/esm/shaders/colorRampLut.generated.js +3 -0
  82. package/esm/shaders/coverageBand.generated.d.ts +2 -0
  83. package/esm/shaders/coverageBand.generated.js +4 -0
  84. package/esm/shaders/coverageBar.generated.d.ts +4 -0
  85. package/esm/shaders/coverageBar.generated.js +6 -0
  86. package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
  87. package/esm/shaders/coverageBar.iface.generated.js +114 -0
  88. package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
  89. package/esm/shaders/coverageIndicator.generated.js +6 -0
  90. package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
  91. package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
  92. package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
  93. package/esm/shaders/coverageInterbase.generated.js +6 -0
  94. package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
  95. package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
  96. package/esm/shaders/coverageMod.generated.d.ts +4 -0
  97. package/esm/shaders/coverageMod.generated.js +6 -0
  98. package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
  99. package/esm/shaders/coverageMod.iface.generated.js +144 -0
  100. package/esm/shaders/coverageSnp.generated.d.ts +4 -0
  101. package/esm/shaders/coverageSnp.generated.js +6 -0
  102. package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
  103. package/esm/shaders/coverageSnp.iface.generated.js +144 -0
  104. package/esm/shaders/hpmath.js.generated.d.ts +3 -0
  105. package/esm/shaders/hpmath.js.generated.js +48 -0
  106. package/esm/shaders/pointGlyph.generated.d.ts +1 -0
  107. package/esm/shaders/pointGlyph.generated.js +3 -0
  108. package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
  109. package/esm/shaders/pointGlyph.js.generated.js +7 -0
  110. package/esm/shaders/rowRect.generated.d.ts +3 -0
  111. package/esm/shaders/rowRect.generated.js +5 -0
  112. package/esm/shaders/rowRect.js.generated.d.ts +2 -0
  113. package/esm/shaders/rowRect.js.generated.js +13 -0
  114. package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
  115. package/esm/shaders/scoreScale.js.generated.js +46 -0
  116. package/esm/slangPass.d.ts +49 -0
  117. package/esm/slangPass.js +22 -0
  118. package/esm/useEventCallback.d.ts +1 -0
  119. package/esm/useEventCallback.js +19 -0
  120. package/esm/useGraphicsCapabilities.d.ts +17 -0
  121. package/esm/useGraphicsCapabilities.js +38 -0
  122. package/esm/useRenderingBackend.d.ts +83 -0
  123. package/esm/useRenderingBackend.js +422 -0
  124. package/esm/useTabVisibilityRerender.d.ts +13 -0
  125. package/esm/useTabVisibilityRerender.js +39 -0
  126. package/esm/webgpuUtils.d.ts +23 -0
  127. package/esm/webgpuUtils.js +76 -0
  128. package/package.json +91 -0
  129. package/src/shaders/antialias.js.generated.ts +33 -0
  130. package/src/shaders/antialias.slang +112 -0
  131. package/src/shaders/capsule.generated.ts +4 -0
  132. package/src/shaders/capsule.slang +62 -0
  133. package/src/shaders/colorPack.slang +26 -0
  134. package/src/shaders/colorRampLut.generated.ts +4 -0
  135. package/src/shaders/colorRampLut.slang +51 -0
  136. package/src/shaders/coverageBand.generated.ts +6 -0
  137. package/src/shaders/coverageBand.slang +276 -0
  138. package/src/shaders/coverageBar.generated.ts +10 -0
  139. package/src/shaders/coverageBar.iface.generated.ts +172 -0
  140. package/src/shaders/coverageBar.slang +37 -0
  141. package/src/shaders/coverageIndicator.generated.ts +10 -0
  142. package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
  143. package/src/shaders/coverageIndicator.slang +61 -0
  144. package/src/shaders/coverageInterbase.generated.ts +10 -0
  145. package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
  146. package/src/shaders/coverageInterbase.slang +63 -0
  147. package/src/shaders/coverageMod.generated.ts +10 -0
  148. package/src/shaders/coverageMod.iface.generated.ts +211 -0
  149. package/src/shaders/coverageMod.slang +36 -0
  150. package/src/shaders/coverageSnp.generated.ts +10 -0
  151. package/src/shaders/coverageSnp.iface.generated.ts +211 -0
  152. package/src/shaders/coverageSnp.slang +52 -0
  153. package/src/shaders/diagonalGrid.slang +25 -0
  154. package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
  155. package/src/shaders/hpmath.js.generated.ts +47 -0
  156. package/src/shaders/hpmath.slang +226 -0
  157. package/src/shaders/pointGlyph.generated.ts +4 -0
  158. package/src/shaders/pointGlyph.js.generated.ts +9 -0
  159. package/src/shaders/pointGlyph.slang +93 -0
  160. package/src/shaders/rowRect.generated.ts +8 -0
  161. package/src/shaders/rowRect.js.generated.ts +17 -0
  162. package/src/shaders/rowRect.slang +160 -0
  163. package/src/shaders/scoreScale.js.generated.ts +51 -0
  164. package/src/shaders/scoreScale.slang +68 -0
@@ -0,0 +1,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
+ }