@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,116 @@
1
+ import type { CanvasScale } from '../canvas2dUtils.ts';
2
+ export type { CanvasScale };
3
+ /**
4
+ * One field of a pass's **vertex input layout** — the map from bytes in the
5
+ * packed instance buffer to an input the shader declares. Both HALs build from
6
+ * it: WebGPU as a `GPUVertexBufferLayout` attribute, WebGL2 as a VAO pointer.
7
+ * Emitted from the shader by codegen as `VERTEX_ATTRIBUTES`; never hand-written.
8
+ */
9
+ export interface VertexAttributeLayout {
10
+ name: string;
11
+ components: number;
12
+ type: 'float' | 'uint' | 'int';
13
+ offsetBytes: number;
14
+ integer: boolean;
15
+ }
16
+ /**
17
+ * Samples per pixel a display's WebGPU render target is allocated at.
18
+ *
19
+ * **1 and 4 are the whole set the WebGPU spec permits** ("must be either 1 or
20
+ * 4" — standard sample patterns are defined for no other count), so this is the
21
+ * API's constraint rather than a policy of ours, and 2 is not an option to add.
22
+ *
23
+ * At 4 the HAL allocates a multisampled colour attachment the size of the
24
+ * canvas and resolves it into the canvas texture; at 1 it allocates **nothing**
25
+ * and draws straight into the canvas texture. The bytes are canvas area x dpr²
26
+ * x samples x 4, unrelated to how much data the display holds, which is why
27
+ * this is worth stating per display — see ARCHITECTURAL_LIMITS.md §"The MSAA
28
+ * target is the largest per-display allocation".
29
+ *
30
+ * WebGL2 has no counterpart: it draws to the default framebuffer with
31
+ * `antialias: true`, so its multisampling is the browser's choice inside the
32
+ * browser's budget.
33
+ */
34
+ export type SampleCount = 1 | 4;
35
+ export type BlendFactor = 'one' | 'src-alpha' | 'one-minus-src-alpha' | 'zero';
36
+ export type BlendState = {
37
+ op?: 'add';
38
+ srcFactor: BlendFactor;
39
+ dstFactor: BlendFactor;
40
+ } | {
41
+ op: 'max';
42
+ };
43
+ /**
44
+ * One entry of a shader's GPU binding table, as the Slang codegen reflects it.
45
+ *
46
+ * `kind` is spelled the way WebGPU spells it so a consumer can build a
47
+ * `createBindGroupLayout` entry from it directly. That is the point: the binding
48
+ * indices used to be asserted by hand in three places that never consulted the
49
+ * shader — this HAL's two hardcoded render layouts, and the LD compute driver's
50
+ * own. A shader's `BINDINGS` export is the reflected truth, and `pnpm
51
+ * gen:shaders` now refuses a render shader whose table is not one the HALs bind.
52
+ */
53
+ export interface ShaderBinding {
54
+ index: number;
55
+ kind: 'uniform' | 'texture' | 'sampler' | 'storage' | 'read-only-storage';
56
+ name: string;
57
+ }
58
+ export interface TextureBinding {
59
+ textureBinding: number;
60
+ samplerBinding: number;
61
+ glTextureUnit: number;
62
+ glUniformName: string;
63
+ filter: 'linear' | 'nearest';
64
+ }
65
+ /**
66
+ * A **pipeline state object (PSO)**: shaders, vertex input layout, blend state,
67
+ * primitive topology and texture bindings, baked into one immutable
68
+ * description. One of these compiles to exactly one `GPURenderPipeline`
69
+ * (`webgpuHal`) or one linked program + VAO (`webgl2Hal`), up front at HAL
70
+ * construction — never lazily mid-frame.
71
+ *
72
+ * `id` is the join key everything else calls a *pass*: `drawPass(id, …)` binds
73
+ * this pipeline and issues one instanced draw, and the HAL's buffer registry is
74
+ * keyed `(regionKey, id)`. That word does **not** mean WebGPU's render pass,
75
+ * which is the `beginFrame`/`endFrame` bracket and happens once per frame.
76
+ */
77
+ export interface PipelineDescriptor {
78
+ id: string;
79
+ wgslSource: string;
80
+ glslVertex: string;
81
+ glslFragment: string;
82
+ instanceStride: number;
83
+ verticesPerInstance: number;
84
+ blend: boolean;
85
+ blendState?: BlendState;
86
+ vertexAttributes: readonly VertexAttributeLayout[];
87
+ topology?: 'triangle-list' | 'triangle-strip' | 'line-list';
88
+ textures?: readonly [TextureBinding, ...TextureBinding[]];
89
+ bindings?: readonly ShaderBinding[];
90
+ }
91
+ export interface GpuHal {
92
+ /**
93
+ * Size the canvas, and report the scale its backing store actually got.
94
+ * Callers derive every device-px rect from THAT rather than from `getDpr()`:
95
+ * past `MAX_CANVAS_DIM_PX` the two differ, and a rect built from the true dpr
96
+ * exceeds the target it draws into (WebGPU rejects the whole pass and the
97
+ * track goes blank; WebGL2 clamps and paints at the wrong scale).
98
+ */
99
+ resize(width: number, height: number): CanvasScale;
100
+ uploadBuffer(regionKey: number, passId: string, data: ArrayBuffer | ArrayBufferView, count: number): void;
101
+ getBufferCount(regionKey: number, passId: string): number;
102
+ deleteBuffer(regionKey: number, passId: string): void;
103
+ deleteRegion(regionKey: number): void;
104
+ pruneRegions(active: Iterable<number>): void;
105
+ uploadTexture(passId: string, data: Uint8Array, width: number, height: number): void;
106
+ writeUniforms(data: ArrayBuffer): void;
107
+ beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
108
+ drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
109
+ endFrame(): void;
110
+ setScissor(x: number, y: number, w: number, h: number): void;
111
+ clearScissor(): void;
112
+ setViewport(x: number, y: number, w: number, h: number): void;
113
+ clearViewport(): void;
114
+ setErrorHandler(handler: (error: Error) => void): void;
115
+ dispose(): void;
116
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { GpuHalBase } from './gpuHalBase.ts';
2
+ import type { GpuHal, PipelineDescriptor, TextureBinding } from './types.ts';
3
+ interface RegionPassBuffer {
4
+ vbo: WebGLBuffer;
5
+ count: number;
6
+ }
7
+ export declare class WebGL2Hal extends GpuHalBase<RegionPassBuffer> implements GpuHal {
8
+ private gl;
9
+ private canvas;
10
+ private passes;
11
+ private ubo;
12
+ private maxTextureDim;
13
+ private debug;
14
+ private instanceId;
15
+ private firstDrawSeen;
16
+ private contextWasLost;
17
+ private contextLostListener;
18
+ private contextRestoredListener;
19
+ private checkGlError;
20
+ constructor(canvas: HTMLCanvasElement, descriptors: PipelineDescriptor[], uniformByteSize: number);
21
+ protected limits(): {
22
+ maxBufferBytes: number;
23
+ maxTextureDimensionPx: number;
24
+ };
25
+ protected createBuffer(data: ArrayBuffer | ArrayBufferView, count: number): {
26
+ vbo: WebGLBuffer;
27
+ count: number;
28
+ };
29
+ protected destroyBuffer(buf: RegionPassBuffer): void;
30
+ private compilePass;
31
+ private getPass;
32
+ resize(width: number, height: number): import("./types.ts").CanvasScale;
33
+ protected createTexture(passId: string, binding: TextureBinding, data: Uint8Array, width: number, height: number): void;
34
+ writeUniforms(data: ArrayBuffer): void;
35
+ beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
36
+ setScissor(x: number, y: number, w: number, h: number): void;
37
+ clearScissor(): void;
38
+ setViewport(x: number, y: number, w: number, h: number): void;
39
+ clearViewport(): void;
40
+ drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
41
+ endFrame(): void;
42
+ protected releaseResources(): void;
43
+ private applyBlendState;
44
+ private bindTextures;
45
+ private bindAttributes;
46
+ }
47
+ export {};
@@ -0,0 +1,490 @@
1
+ import { syncCanvasSize } from '../canvas2dUtils.js';
2
+ import { canvasContextError, noteCanvasContext } from '../canvasContext.js';
3
+ import { GpuHalBase } from './gpuHalBase.js';
4
+ function createShader(gl, type, source) {
5
+ const shader = gl.createShader(type);
6
+ gl.shaderSource(shader, source);
7
+ gl.compileShader(shader);
8
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
9
+ const info = gl.getShaderInfoLog(shader);
10
+ gl.deleteShader(shader);
11
+ throw new Error(`Shader compile error: ${info}`);
12
+ }
13
+ return shader;
14
+ }
15
+ function createProgram(gl, vsSource, fsSource) {
16
+ const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
17
+ let fs;
18
+ try {
19
+ fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
20
+ }
21
+ catch (e) {
22
+ gl.deleteShader(vs);
23
+ throw e;
24
+ }
25
+ const program = gl.createProgram();
26
+ gl.attachShader(program, vs);
27
+ gl.attachShader(program, fs);
28
+ gl.linkProgram(program);
29
+ gl.detachShader(program, vs);
30
+ gl.detachShader(program, fs);
31
+ gl.deleteShader(vs);
32
+ gl.deleteShader(fs);
33
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
34
+ const info = gl.getProgramInfoLog(program);
35
+ gl.deleteProgram(program);
36
+ throw new Error(`Program link error: ${info}`);
37
+ }
38
+ return program;
39
+ }
40
+ function bindUniformBlock(gl, program, blockName, bindingPoint) {
41
+ const idx = gl.getUniformBlockIndex(program, blockName);
42
+ if (idx !== gl.INVALID_INDEX) {
43
+ gl.uniformBlockBinding(program, idx, bindingPoint);
44
+ }
45
+ }
46
+ // Set `DEBUG.webgl2 = true` in devtools (or `?webgl2-debug=1` in URL) to
47
+ // enable verbose logging. Kept guarded so production builds stay quiet.
48
+ function debugEnabled() {
49
+ if (typeof window === 'undefined') {
50
+ return false;
51
+ }
52
+ const w = window;
53
+ if (w.DEBUG?.webgl2) {
54
+ return true;
55
+ }
56
+ return /(?:\?|&)webgl2-debug=1\b/.test(window.location.search);
57
+ }
58
+ function glErrorName(gl, code) {
59
+ if (code === gl.NO_ERROR) {
60
+ return 'NO_ERROR';
61
+ }
62
+ if (code === gl.INVALID_ENUM) {
63
+ return 'INVALID_ENUM';
64
+ }
65
+ if (code === gl.INVALID_VALUE) {
66
+ return 'INVALID_VALUE';
67
+ }
68
+ if (code === gl.INVALID_OPERATION) {
69
+ return 'INVALID_OPERATION';
70
+ }
71
+ if (code === gl.INVALID_FRAMEBUFFER_OPERATION) {
72
+ return 'INVALID_FRAMEBUFFER_OPERATION';
73
+ }
74
+ if (code === gl.OUT_OF_MEMORY) {
75
+ return 'OUT_OF_MEMORY';
76
+ }
77
+ if (code === gl.CONTEXT_LOST_WEBGL) {
78
+ return 'CONTEXT_LOST_WEBGL';
79
+ }
80
+ return `0x${code.toString(16)}`;
81
+ }
82
+ function glBlendFactor(gl, factor) {
83
+ switch (factor) {
84
+ case 'one':
85
+ return gl.ONE;
86
+ case 'zero':
87
+ return gl.ZERO;
88
+ case 'src-alpha':
89
+ return gl.SRC_ALPHA;
90
+ case 'one-minus-src-alpha':
91
+ return gl.ONE_MINUS_SRC_ALPHA;
92
+ }
93
+ }
94
+ // Module-scope lifecycle tracking — Firefox caps active WebGL contexts
95
+ // around 16 and Chrome around 8. Context leaks force the oldest contexts to
96
+ // lose. These counters surface the leak when it happens.
97
+ let totalCreated = 0;
98
+ let totalDisposed = 0;
99
+ // The vertex-buffer ceiling this HAL refuses past, in bytes.
100
+ //
101
+ // WebGL2 exposes no max-buffer-size parameter, so unlike WebGPU there is
102
+ // nothing to ask — but "ask the driver" was never what the guard was for. Its
103
+ // job is to turn one pathological upload into the "zoom in" banner, and without
104
+ // it the same upload that banners on WebGPU takes Chrome's context down here
105
+ // instead. That is strictly worse than a blank track on a page at the context
106
+ // ceiling: the loss evicts a sibling, whose recovery evicts another, and
107
+ // GPU_CONTEXT_BUDGET.md is that cascade.
108
+ //
109
+ // 256 MiB is WebGPU's *spec default* `maxBufferSize`. It is not parity with
110
+ // what the WebGPU backend actually refuses at: `gpuDevice.acquire` raises the
111
+ // limit to `adapter.limits.maxBufferSize`, which is 2147483644 bytes (~2 GiB)
112
+ // on the Firefox Nightly / Intel UHD 630 this was checked on, so WebGL2 is the
113
+ // stricter of the two and a region can banner here while rendering there. That
114
+ // asymmetry is accepted — a quarter-gigabyte single vertex buffer is not
115
+ // something to hand an API that answers a failed allocation by dropping the
116
+ // context.
117
+ //
118
+ // Deliberately a ceiling and not a budget: it catches the single upload
119
+ // nothing else bounds, and says nothing about the total, which is
120
+ // ARCHITECTURAL_LIMITS.md §"No session-level GPU memory budget".
121
+ const MAX_VERTEX_BUFFER_BYTES = 256 * 1024 * 1024;
122
+ // Behavioral parity with WebGPUHal is enforced by tests, not by this file:
123
+ // products/jbrowse-web/browser-tests/compare-backends.ts pixel-diffs webgl vs
124
+ // webgpu vs canvas2d output, and shared buffer bookkeeping is covered by
125
+ // hal/regionRegistry.test.ts. Neither HAL is where attribute layout is checked —
126
+ // `assertVertexInputsMatch` does that at `pnpm gen:shaders` time, per shader and
127
+ // per target. Mirror any behavior change in webgpuHal.ts — and note that
128
+ // `GpuHalBase` already holds the parts that were only ever mirrored: the
129
+ // descriptor map, the buffer registry, both upload shells and their over-limit
130
+ // wording, and the dispose guard.
131
+ export class WebGL2Hal extends GpuHalBase {
132
+ gl;
133
+ canvas;
134
+ // Compiled passes, filled on demand by `getPass`. A pass whose program failed
135
+ // to build caches `null` so the failure is reported once, not every frame.
136
+ passes;
137
+ ubo;
138
+ // Asked once: `getParameter` is a synchronous driver query and `limits()` is
139
+ // now read on every upload, not only on a texture one.
140
+ maxTextureDim;
141
+ debug = false;
142
+ instanceId = 0;
143
+ firstDrawSeen = new Set();
144
+ // Latched on context loss, never cleared. GL objects from before the loss are
145
+ // invalid even after restore — isContextLost()===false by then, so a live
146
+ // check alone can't guard dispose().
147
+ contextWasLost = false;
148
+ contextLostListener = null;
149
+ contextRestoredListener = null;
150
+ checkGlError(label) {
151
+ if (!this.debug) {
152
+ return;
153
+ }
154
+ const err = this.gl.getError();
155
+ if (err !== this.gl.NO_ERROR) {
156
+ console.error(`[WebGL2Hal] GL error at "${label}": ${glErrorName(this.gl, err)}`);
157
+ }
158
+ }
159
+ constructor(canvas, descriptors, uniformByteSize) {
160
+ super(descriptors, 'WebGL2Hal');
161
+ this.canvas = canvas;
162
+ this.debug = debugEnabled();
163
+ totalCreated += 1;
164
+ this.instanceId = totalCreated;
165
+ if (this.debug) {
166
+ console.warn(`[WebGL2Hal #${this.instanceId}] init (live=${totalCreated - totalDisposed}/${totalCreated}, passes declared=${descriptors.length}, compiled on first draw)`);
167
+ }
168
+ const onContextLost = (e) => {
169
+ const ev = e;
170
+ console.error(`[WebGL2Hal #${this.instanceId}] context LOST (statusMessage="${ev.statusMessage}", live=${totalCreated - totalDisposed})`);
171
+ this.contextWasLost = true;
172
+ e.preventDefault();
173
+ };
174
+ const onContextRestored = () => {
175
+ console.warn(`[WebGL2Hal #${this.instanceId}] context restored`);
176
+ };
177
+ canvas.addEventListener('webglcontextlost', onContextLost, false);
178
+ canvas.addEventListener('webglcontextrestored', onContextRestored, false);
179
+ this.contextLostListener = onContextLost;
180
+ this.contextRestoredListener = onContextRestored;
181
+ // premultipliedAlpha:true is required for correct AA edge blending.
182
+ // The canvas is cleared to (0,0,0,0) and drawn with SRC_ALPHA,ONE_MINUS_SRC_ALPHA
183
+ // blend, which produces premultiplied-alpha values in the framebuffer
184
+ // (edge pixel: rgb = color*alpha, a = alpha). With premultipliedAlpha:true
185
+ // the browser compositor reads those as premultiplied and composites correctly:
186
+ // output = fb.rgb + bg*(1-fb.a)
187
+ // With premultipliedAlpha:false the compositor treats them as straight alpha and
188
+ // multiplies rgb by alpha a second time, making AA edges appear too dark.
189
+ // The WebGPU HAL uses alphaMode:'premultiplied' for the same reason.
190
+ const gl = canvas.getContext('webgl2', {
191
+ antialias: true,
192
+ premultipliedAlpha: true,
193
+ });
194
+ if (!gl) {
195
+ // Was a bare 'WebGL2 not supported', which is the wrong diagnosis for the
196
+ // case that actually reaches here in production: a re-init on a canvas the
197
+ // WebGPU rung already committed. See canvasContext.ts.
198
+ throw canvasContextError(canvas, 'webgl2');
199
+ }
200
+ noteCanvasContext(canvas, 'webgl2');
201
+ this.gl = gl;
202
+ this.maxTextureDim = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE));
203
+ this.ubo = gl.createBuffer();
204
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo);
205
+ gl.bufferData(gl.UNIFORM_BUFFER, uniformByteSize, gl.DYNAMIC_DRAW);
206
+ this.passes = new Map();
207
+ // Programs are built on first use (see `getPass`), not here: a renderer
208
+ // declares every pass it could ever draw — alignments alone declares 21,
209
+ // most of them behind a colorBy/arc/per-base setting — and linking one
210
+ // costs tens of ms of driver time on the main thread. A profile of a
211
+ // three-track LGV linked 29 programs and drew with 14.
212
+ //
213
+ // The first descriptor still links eagerly as a canary, so a GL stack that
214
+ // can't compile our shaders at all throws from the constructor and
215
+ // `createGpuHal` falls back to Canvas2D — the ladder only runs at
216
+ // construction. Per-pass compile failures after that are caught in
217
+ // `getPass` and surfaced through the error handler.
218
+ const canary = descriptors[0];
219
+ if (canary) {
220
+ this.compilePass(canary);
221
+ }
222
+ gl.enable(gl.BLEND);
223
+ }
224
+ // A fixed buffer ceiling rather than a queried one — see
225
+ // MAX_VERTEX_BUFFER_BYTES for why the guard exists at all and why the number
226
+ // is WebGPU's. Past it, an unguarded bufferData loses the context in Chrome
227
+ // and throws RangeError in Firefox; neither is a getError() case, so the
228
+ // base's report is the only place the display can be told.
229
+ limits() {
230
+ return {
231
+ maxBufferBytes: MAX_VERTEX_BUFFER_BYTES,
232
+ maxTextureDimensionPx: this.maxTextureDim,
233
+ };
234
+ }
235
+ createBuffer(data, count) {
236
+ const gl = this.gl;
237
+ const vbo = gl.createBuffer();
238
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
239
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
240
+ return { vbo, count };
241
+ }
242
+ // No mid-frame deferral, unlike `WebGPUHal`'s hook: GL is immediate-mode, so
243
+ // a draw already issued has consumed the buffer and deleting it between
244
+ // beginFrame and endFrame is defined. The parity that matters is the
245
+ // caller's — the same upload sequence is legal on both.
246
+ destroyBuffer(buf) {
247
+ const gl = this.gl;
248
+ if (!this.contextWasLost && !gl.isContextLost()) {
249
+ gl.deleteBuffer(buf.vbo);
250
+ }
251
+ }
252
+ compilePass(desc) {
253
+ const gl = this.gl;
254
+ const program = createProgram(gl, desc.glslVertex, desc.glslFragment);
255
+ bindUniformBlock(gl, program, 'Uniforms', 0);
256
+ this.checkGlError(`link pass "${desc.id}"`);
257
+ const attrLocs = desc.vertexAttributes.map(attr => gl.getAttribLocation(program, attr.name));
258
+ if (this.debug) {
259
+ const pairs = desc.vertexAttributes.map((a, i) => `${a.name}@${attrLocs[i]}`);
260
+ console.warn(`[WebGL2Hal] pass "${desc.id}" stride=${desc.instanceStride} attrs: ${pairs.join(', ')}`);
261
+ const missing = desc.vertexAttributes.filter((_, i) => attrLocs[i] < 0);
262
+ if (missing.length > 0) {
263
+ console.warn(`[WebGL2Hal] pass "${desc.id}" missing attribute locations: ${missing.map(a => a.name).join(', ')}`);
264
+ }
265
+ }
266
+ const vao = gl.createVertexArray();
267
+ gl.bindVertexArray(vao);
268
+ for (const loc of attrLocs) {
269
+ if (loc >= 0) {
270
+ gl.enableVertexAttribArray(loc);
271
+ gl.vertexAttribDivisor(loc, 1);
272
+ }
273
+ }
274
+ gl.bindVertexArray(null);
275
+ let textureState = null;
276
+ const tb = desc.textures?.[0];
277
+ if (tb) {
278
+ // Bind the sampler uniform to the texture unit once — it never changes.
279
+ gl.useProgram(program);
280
+ gl.uniform1i(gl.getUniformLocation(program, tb.glUniformName), tb.glTextureUnit);
281
+ textureState = { texture: null, unit: tb.glTextureUnit };
282
+ }
283
+ const state = {
284
+ program,
285
+ vao,
286
+ descriptor: desc,
287
+ textureState,
288
+ attrLocs,
289
+ };
290
+ this.passes.set(desc.id, state);
291
+ return state;
292
+ }
293
+ // Compiled pass for `passId`, building it the first time it's asked for.
294
+ // Returns undefined for an unknown id or a pass whose program failed to link.
295
+ getPass(passId) {
296
+ const existing = this.passes.get(passId);
297
+ if (existing !== undefined) {
298
+ return existing ?? undefined;
299
+ }
300
+ const desc = this.descriptors.get(passId);
301
+ if (!desc) {
302
+ return undefined;
303
+ }
304
+ try {
305
+ return this.compilePass(desc);
306
+ }
307
+ catch (e) {
308
+ this.passes.set(passId, null);
309
+ this.oom.report(`#${this.instanceId} could not build the "${passId}" shader on this GPU: ${e instanceof Error ? e.message : String(e)}`);
310
+ return undefined;
311
+ }
312
+ }
313
+ resize(width, height) {
314
+ return syncCanvasSize(this.canvas, width, height).scale;
315
+ }
316
+ createTexture(passId, binding, data, width, height) {
317
+ const gl = this.gl;
318
+ const ts = this.getPass(passId)?.textureState;
319
+ if (!ts) {
320
+ return;
321
+ }
322
+ if (ts.texture) {
323
+ gl.deleteTexture(ts.texture);
324
+ }
325
+ const tex = gl.createTexture();
326
+ gl.bindTexture(gl.TEXTURE_2D, tex);
327
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
328
+ const filter = binding.filter === 'linear' ? gl.LINEAR : gl.NEAREST;
329
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
330
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
331
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
332
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
333
+ ts.texture = tex;
334
+ }
335
+ writeUniforms(data) {
336
+ const gl = this.gl;
337
+ gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo);
338
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
339
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, this.ubo);
340
+ }
341
+ beginFrame(clearR, clearG, clearB, clearA = 1) {
342
+ const gl = this.gl;
343
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
344
+ gl.disable(gl.SCISSOR_TEST);
345
+ gl.viewport(0, 0, this.canvas.width, this.canvas.height);
346
+ gl.clearColor(clearR, clearG, clearB, clearA);
347
+ gl.clear(gl.COLOR_BUFFER_BIT);
348
+ }
349
+ setScissor(x, y, w, h) {
350
+ const gl = this.gl;
351
+ gl.enable(gl.SCISSOR_TEST);
352
+ gl.scissor(x, this.canvas.height - y - h, w, h);
353
+ }
354
+ clearScissor() {
355
+ this.gl.disable(this.gl.SCISSOR_TEST);
356
+ }
357
+ setViewport(x, y, w, h) {
358
+ this.gl.viewport(x, this.canvas.height - y - h, w, h);
359
+ }
360
+ clearViewport() {
361
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
362
+ }
363
+ drawPass(passId, regionKey, bufferPassId) {
364
+ const gl = this.gl;
365
+ // Buffer first, program second: a pass with nothing to draw must not be
366
+ // the reason its shader gets compiled.
367
+ const regionBuf = this.regions.get(regionKey, bufferPassId ?? passId);
368
+ if (!regionBuf || regionBuf.count === 0) {
369
+ return;
370
+ }
371
+ const pass = this.getPass(passId);
372
+ if (!pass) {
373
+ return;
374
+ }
375
+ this.applyBlendState(pass.descriptor);
376
+ gl.useProgram(pass.program);
377
+ gl.bindVertexArray(pass.vao);
378
+ this.bindAttributes(pass, regionBuf.vbo);
379
+ this.bindTextures(pass);
380
+ const topo = pass.descriptor.topology ?? 'triangle-list';
381
+ const glMode = topo === 'triangle-strip'
382
+ ? gl.TRIANGLE_STRIP
383
+ : topo === 'line-list'
384
+ ? gl.LINES
385
+ : gl.TRIANGLES;
386
+ gl.drawArraysInstanced(glMode, 0, pass.descriptor.verticesPerInstance, regionBuf.count);
387
+ if (this.debug && !this.firstDrawSeen.has(passId)) {
388
+ this.firstDrawSeen.add(passId);
389
+ const err = gl.getError();
390
+ console.warn(`[WebGL2Hal #${this.instanceId}] first draw pass="${passId}" verts=${pass.descriptor.verticesPerInstance} instances=${regionBuf.count} err=${glErrorName(gl, err)}`);
391
+ }
392
+ gl.bindVertexArray(null);
393
+ }
394
+ endFrame() {
395
+ const gl = this.gl;
396
+ gl.disable(gl.SCISSOR_TEST);
397
+ gl.viewport(0, 0, this.canvas.width, this.canvas.height);
398
+ }
399
+ releaseResources() {
400
+ const gl = this.gl;
401
+ totalDisposed += 1;
402
+ if (this.debug) {
403
+ console.warn(`[WebGL2Hal #${this.instanceId}] DISPOSING context (live=${totalCreated - totalDisposed}/${totalCreated})`);
404
+ }
405
+ // Remove canvas event listeners to prevent closure references from keeping
406
+ // the context alive after disposal. This is critical for test suites where
407
+ // multiple contexts are created in sequence (e.g. Puppeteer page navigation).
408
+ // Double-dispose is short-circuited above, so listeners are guaranteed set.
409
+ if (this.contextLostListener) {
410
+ this.canvas.removeEventListener('webglcontextlost', this.contextLostListener);
411
+ }
412
+ if (this.contextRestoredListener) {
413
+ this.canvas.removeEventListener('webglcontextrestored', this.contextRestoredListener);
414
+ }
415
+ this.regions.deleteAll();
416
+ if (!this.contextWasLost && !gl.isContextLost()) {
417
+ for (const pass of this.passes.values()) {
418
+ if (pass) {
419
+ gl.deleteVertexArray(pass.vao);
420
+ gl.deleteProgram(pass.program);
421
+ if (pass.textureState?.texture) {
422
+ gl.deleteTexture(pass.textureState.texture);
423
+ }
424
+ }
425
+ }
426
+ gl.deleteBuffer(this.ubo);
427
+ }
428
+ this.passes.clear();
429
+ // Firefox appears to treat WEBGL_lose_context.loseContext() as a
430
+ // driver-wide reset: calling it on one disposed HAL synchronously
431
+ // knocks out sibling live contexts too, so tracks go blank en masse.
432
+ // Chrome only needs this as a test-suite optimisation; for production we
433
+ // let the browser reclaim the context when the canvas is GC'd. If we
434
+ // need explicit release again, gate it on navigator.userAgent.
435
+ }
436
+ applyBlendState(desc) {
437
+ const gl = this.gl;
438
+ if (!desc.blend) {
439
+ gl.disable(gl.BLEND);
440
+ return;
441
+ }
442
+ gl.enable(gl.BLEND);
443
+ const bs = desc.blendState;
444
+ if (bs?.op === 'max') {
445
+ // MIN/MAX ignore blend factors: the framebuffer keeps the per-channel
446
+ // max(src, dst). Used by same-color AA lines so overlapping segments union
447
+ // instead of darkening. Reset explicitly below for every other pass.
448
+ gl.blendEquation(gl.MAX);
449
+ }
450
+ else {
451
+ gl.blendEquation(gl.FUNC_ADD);
452
+ // RGB and alpha get different blend factors (blendFuncSeparate):
453
+ // RGB: out = src_rgb * srcFactor + dst_rgb * dstFactor (default: src-alpha / 1-src-alpha)
454
+ // Alpha: out = src_alpha * 1 + dst_alpha * (1 - src_alpha)
455
+ // The alpha channel uses ONE/ONE_MINUS_SRC_ALPHA regardless of the custom blend state;
456
+ // using the RGB srcFactor for alpha too would give out_alpha = src_alpha² + ..., which is wrong.
457
+ const src = bs ? glBlendFactor(gl, bs.srcFactor) : gl.SRC_ALPHA;
458
+ const dst = bs ? glBlendFactor(gl, bs.dstFactor) : gl.ONE_MINUS_SRC_ALPHA;
459
+ gl.blendFuncSeparate(src, dst, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
460
+ }
461
+ }
462
+ bindTextures(pass) {
463
+ const gl = this.gl;
464
+ const ts = pass.textureState;
465
+ if (ts?.texture) {
466
+ gl.activeTexture(gl.TEXTURE0 + ts.unit);
467
+ gl.bindTexture(gl.TEXTURE_2D, ts.texture);
468
+ // sampler uniform set once at construction — no uniform1i here
469
+ }
470
+ }
471
+ bindAttributes(pass, vbo) {
472
+ const gl = this.gl;
473
+ const desc = pass.descriptor;
474
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
475
+ for (let i = 0; i < desc.vertexAttributes.length; i++) {
476
+ const loc = pass.attrLocs[i];
477
+ if (loc < 0) {
478
+ continue;
479
+ }
480
+ const attr = desc.vertexAttributes[i];
481
+ if (attr.integer) {
482
+ const glType = attr.type === 'uint' ? gl.UNSIGNED_INT : gl.INT;
483
+ gl.vertexAttribIPointer(loc, attr.components, glType, desc.instanceStride, attr.offsetBytes);
484
+ }
485
+ else {
486
+ gl.vertexAttribPointer(loc, attr.components, gl.FLOAT, false, desc.instanceStride, attr.offsetBytes);
487
+ }
488
+ }
489
+ }
490
+ }