@jbrowse/render-core 5.0.0-beta.8 → 5.0.0-beta.9

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 (174) hide show
  1. package/esm/RenderCanvas.d.ts +2 -2
  2. package/esm/RenderLifecycleMixin.d.ts +7 -6
  3. package/esm/RenderLifecycleMixin.js +7 -6
  4. package/esm/canvas2dUtils.d.ts +58 -17
  5. package/esm/canvas2dUtils.js +82 -30
  6. package/esm/computePipeline.d.ts +4 -3
  7. package/esm/computePipeline.js +21 -14
  8. package/esm/coverageBand.d.ts +3 -3
  9. package/esm/coverageBand.js +2 -2
  10. package/esm/coverageBandBuffers.d.ts +2 -3
  11. package/esm/createRenderingBackend.d.ts +1 -1
  12. package/esm/displayPhase.d.ts +79 -109
  13. package/esm/displayPhase.js +42 -83
  14. package/esm/gpuDevice.d.ts +11 -10
  15. package/esm/gpuDevice.js +10 -11
  16. package/esm/graphicsCapabilities.d.ts +6 -7
  17. package/esm/graphicsCapabilities.js +3 -4
  18. package/esm/hal/createHal.js +1 -1
  19. package/esm/hal/deviceGpuCache.d.ts +25 -30
  20. package/esm/hal/deviceGpuCache.js +85 -97
  21. package/esm/hal/index.d.ts +2 -2
  22. package/esm/hal/index.js +1 -1
  23. package/esm/hal/mockHal.d.ts +8 -6
  24. package/esm/hal/mockHal.js +7 -2
  25. package/esm/hal/passIds.d.ts +8 -7
  26. package/esm/hal/passIds.js +11 -10
  27. package/esm/hal/types.d.ts +28 -12
  28. package/esm/hal/webgl2Hal.d.ts +11 -3
  29. package/esm/hal/webgl2Hal.js +116 -145
  30. package/esm/hal/webgpuHal.d.ts +8 -8
  31. package/esm/hal/webgpuHal.js +97 -124
  32. package/esm/instancePass.d.ts +2 -1
  33. package/esm/mapUploadSync.d.ts +2 -2
  34. package/esm/mapUploadSync.js +2 -2
  35. package/esm/marks/barMark.d.ts +6 -3
  36. package/esm/marks/barMark.js +19 -13
  37. package/esm/marks/drawAgainstHit.d.ts +58 -39
  38. package/esm/marks/drawAgainstHit.js +136 -96
  39. package/esm/marks/glyphPaint.d.ts +14 -2
  40. package/esm/marks/glyphPaint.js +16 -2
  41. package/esm/marks/index.d.ts +4 -2
  42. package/esm/marks/index.js +3 -2
  43. package/esm/marks/markBackend.d.ts +2 -2
  44. package/esm/marks/markBackend.js +2 -2
  45. package/esm/marks/markHit.d.ts +2 -2
  46. package/esm/marks/markHit.js +2 -2
  47. package/esm/marks/markPlan.d.ts +2 -2
  48. package/esm/marks/markPlan.js +2 -2
  49. package/esm/marks/markRamp.d.ts +1 -3
  50. package/esm/marks/markRamp.js +1 -5
  51. package/esm/marks/nearestMarkHit.d.ts +49 -0
  52. package/esm/marks/nearestMarkHit.js +126 -0
  53. package/esm/marks/pointMark.d.ts +9 -2
  54. package/esm/marks/pointMark.js +36 -31
  55. package/esm/marks/rowLane.d.ts +17 -0
  56. package/esm/marks/rowLane.js +11 -0
  57. package/esm/marks/spanMark.d.ts +6 -26
  58. package/esm/marks/spanMark.js +34 -31
  59. package/esm/marks/types.d.ts +51 -181
  60. package/esm/marks/types.js +91 -131
  61. package/esm/namedReactions.d.ts +3 -4
  62. package/esm/namedReactions.js +3 -4
  63. package/esm/scoreScale.d.ts +20 -0
  64. package/esm/scoreScale.js +77 -0
  65. package/esm/shaders/barMark.generated.d.ts +2 -3
  66. package/esm/shaders/barMark.generated.js +4 -3
  67. package/esm/shaders/barMark.glsl.generated.d.ts +2 -0
  68. package/esm/shaders/barMark.glsl.generated.js +4 -0
  69. package/esm/shaders/barMark.iface.generated.d.ts +12 -6
  70. package/esm/shaders/barMark.iface.generated.js +27 -15
  71. package/esm/shaders/barMark.wgsl.generated.d.ts +1 -0
  72. package/esm/shaders/barMark.wgsl.generated.js +3 -0
  73. package/esm/shaders/coverageBar.generated.d.ts +2 -3
  74. package/esm/shaders/coverageBar.generated.js +4 -3
  75. package/esm/shaders/coverageBar.glsl.generated.d.ts +2 -0
  76. package/esm/shaders/coverageBar.glsl.generated.js +4 -0
  77. package/esm/shaders/coverageBar.iface.generated.js +1 -1
  78. package/esm/shaders/coverageBar.wgsl.generated.d.ts +1 -0
  79. package/esm/shaders/coverageBar.wgsl.generated.js +3 -0
  80. package/esm/shaders/coverageIndicator.generated.d.ts +2 -3
  81. package/esm/shaders/coverageIndicator.generated.js +4 -3
  82. package/esm/shaders/coverageIndicator.glsl.generated.d.ts +2 -0
  83. package/esm/shaders/coverageIndicator.glsl.generated.js +4 -0
  84. package/esm/shaders/coverageIndicator.iface.generated.js +1 -1
  85. package/esm/shaders/coverageIndicator.wgsl.generated.d.ts +1 -0
  86. package/esm/shaders/coverageIndicator.wgsl.generated.js +3 -0
  87. package/esm/shaders/coverageInterbase.generated.d.ts +2 -3
  88. package/esm/shaders/coverageInterbase.generated.js +4 -3
  89. package/esm/shaders/coverageInterbase.glsl.generated.d.ts +2 -0
  90. package/esm/shaders/coverageInterbase.glsl.generated.js +4 -0
  91. package/esm/shaders/coverageInterbase.iface.generated.js +1 -1
  92. package/esm/shaders/coverageInterbase.wgsl.generated.d.ts +1 -0
  93. package/esm/shaders/coverageInterbase.wgsl.generated.js +3 -0
  94. package/esm/shaders/coverageMod.generated.d.ts +2 -3
  95. package/esm/shaders/coverageMod.generated.js +4 -3
  96. package/esm/shaders/coverageMod.glsl.generated.d.ts +2 -0
  97. package/esm/shaders/coverageMod.glsl.generated.js +4 -0
  98. package/esm/shaders/coverageMod.iface.generated.js +1 -1
  99. package/esm/shaders/coverageMod.wgsl.generated.d.ts +1 -0
  100. package/esm/shaders/coverageMod.wgsl.generated.js +3 -0
  101. package/esm/shaders/coverageSnp.generated.d.ts +2 -3
  102. package/esm/shaders/coverageSnp.generated.js +4 -3
  103. package/esm/shaders/coverageSnp.glsl.generated.d.ts +2 -0
  104. package/esm/shaders/coverageSnp.glsl.generated.js +4 -0
  105. package/esm/shaders/coverageSnp.iface.generated.js +1 -1
  106. package/esm/shaders/coverageSnp.wgsl.generated.d.ts +1 -0
  107. package/esm/shaders/coverageSnp.wgsl.generated.js +3 -0
  108. package/esm/shaders/pointMark.generated.d.ts +2 -3
  109. package/esm/shaders/pointMark.generated.js +4 -3
  110. package/esm/shaders/pointMark.glsl.generated.d.ts +2 -0
  111. package/esm/shaders/pointMark.glsl.generated.js +4 -0
  112. package/esm/shaders/pointMark.iface.generated.d.ts +11 -3
  113. package/esm/shaders/pointMark.iface.generated.js +23 -9
  114. package/esm/shaders/pointMark.js.generated.d.ts +1 -1
  115. package/esm/shaders/pointMark.js.generated.js +51 -29
  116. package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -0
  117. package/esm/shaders/pointMark.wgsl.generated.js +3 -0
  118. package/esm/shaders/scoreScale.js.generated.js +46 -22
  119. package/esm/shaders/spanMark.generated.d.ts +2 -3
  120. package/esm/shaders/spanMark.generated.js +4 -3
  121. package/esm/shaders/spanMark.glsl.generated.d.ts +2 -0
  122. package/esm/shaders/spanMark.glsl.generated.js +4 -0
  123. package/esm/shaders/spanMark.iface.generated.js +1 -1
  124. package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -0
  125. package/esm/shaders/spanMark.wgsl.generated.js +3 -0
  126. package/esm/slangPass.d.ts +9 -10
  127. package/esm/slangPass.js +9 -3
  128. package/esm/useGraphicsCapabilities.d.ts +1 -1
  129. package/esm/useGraphicsCapabilities.js +1 -1
  130. package/esm/webgpuUtils.d.ts +3 -11
  131. package/esm/webgpuUtils.js +12 -33
  132. package/package.json +3 -2
  133. package/src/shaders/barMark.generated.ts +6 -5
  134. package/src/shaders/barMark.glsl.generated.ts +6 -0
  135. package/src/shaders/barMark.iface.generated.ts +31 -15
  136. package/src/shaders/barMark.slang +11 -3
  137. package/src/shaders/barMark.wgsl.generated.ts +4 -0
  138. package/src/shaders/colorPack.slang +3 -2
  139. package/src/shaders/coverageBand.slang +5 -11
  140. package/src/shaders/coverageBar.generated.ts +6 -5
  141. package/src/shaders/coverageBar.glsl.generated.ts +6 -0
  142. package/src/shaders/coverageBar.iface.generated.ts +1 -1
  143. package/src/shaders/coverageBar.slang +1 -1
  144. package/src/shaders/coverageBar.wgsl.generated.ts +4 -0
  145. package/src/shaders/coverageIndicator.generated.ts +6 -5
  146. package/src/shaders/coverageIndicator.glsl.generated.ts +6 -0
  147. package/src/shaders/coverageIndicator.iface.generated.ts +1 -1
  148. package/src/shaders/coverageIndicator.wgsl.generated.ts +4 -0
  149. package/src/shaders/coverageInterbase.generated.ts +6 -5
  150. package/src/shaders/coverageInterbase.glsl.generated.ts +6 -0
  151. package/src/shaders/coverageInterbase.iface.generated.ts +1 -1
  152. package/src/shaders/coverageInterbase.wgsl.generated.ts +4 -0
  153. package/src/shaders/coverageMod.generated.ts +6 -5
  154. package/src/shaders/coverageMod.glsl.generated.ts +6 -0
  155. package/src/shaders/coverageMod.iface.generated.ts +1 -1
  156. package/src/shaders/coverageMod.wgsl.generated.ts +4 -0
  157. package/src/shaders/coverageSnp.generated.ts +6 -5
  158. package/src/shaders/coverageSnp.glsl.generated.ts +6 -0
  159. package/src/shaders/coverageSnp.iface.generated.ts +1 -1
  160. package/src/shaders/coverageSnp.wgsl.generated.ts +4 -0
  161. package/src/shaders/hpmath.slang +18 -6
  162. package/src/shaders/pointMark.generated.ts +6 -6
  163. package/src/shaders/pointMark.glsl.generated.ts +6 -0
  164. package/src/shaders/pointMark.iface.generated.ts +28 -9
  165. package/src/shaders/pointMark.js.generated.ts +48 -29
  166. package/src/shaders/pointMark.slang +18 -5
  167. package/src/shaders/pointMark.wgsl.generated.ts +4 -0
  168. package/src/shaders/scoreScale.js.generated.ts +43 -22
  169. package/src/shaders/scoreScale.slang +37 -53
  170. package/src/shaders/spanMark.generated.ts +6 -5
  171. package/src/shaders/spanMark.glsl.generated.ts +6 -0
  172. package/src/shaders/spanMark.iface.generated.ts +1 -1
  173. package/src/shaders/spanMark.wgsl.generated.ts +4 -0
  174. package/src/shaders/valueScale.slang +8 -45
@@ -43,8 +43,7 @@ function bindUniformBlock(gl, program, blockName, bindingPoint) {
43
43
  gl.uniformBlockBinding(program, idx, bindingPoint);
44
44
  }
45
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.
46
+ // `DEBUG.webgl2 = true` in devtools, or `?webgl2-debug=1`, logs verbosely.
48
47
  function debugEnabled() {
49
48
  if (typeof window === 'undefined') {
50
49
  return false;
@@ -91,59 +90,33 @@ function glBlendFactor(gl, factor) {
91
90
  return gl.ONE_MINUS_SRC_ALPHA;
92
91
  }
93
92
  }
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.
93
+ // Live-context counts for the debug log: a leaked context evicts the oldest
94
+ // once the browser's cap is reached.
97
95
  let totalCreated = 0;
98
96
  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".
97
+ // WebGPU's spec-default `maxBufferSize`, since WebGL2 has none to query: an
98
+ // upload past it banners rather than dropping the context
99
+ // (ARCHITECTURAL_LIMITS.md).
121
100
  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
101
  export class WebGL2Hal extends GpuHalBase {
132
102
  gl;
133
103
  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;
104
+ // `null` is a pass whose link failed, so it is reported once, not per frame.
105
+ passes = new Map();
106
+ glsl;
107
+ // By vertex source, then fragment source, attribute names and sampler as
108
+ // JSON: every pass id over one program shares it, and a failed link keeps
109
+ // its error.
110
+ programs = new Map();
111
+ passTextures = new Map();
137
112
  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.
113
+ // `getParameter` is a synchronous driver query; `limits()` runs per upload.
140
114
  maxTextureDim;
141
115
  debug = false;
142
116
  instanceId = 0;
143
117
  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().
118
+ // Never cleared: GL objects from before a loss stay invalid after restore,
119
+ // when isContextLost() already answers false.
147
120
  contextWasLost = false;
148
121
  contextLostListener = null;
149
122
  contextRestoredListener = null;
@@ -156,9 +129,18 @@ export class WebGL2Hal extends GpuHalBase {
156
129
  console.error(`[WebGL2Hal] GL error at "${label}": ${glErrorName(this.gl, err)}`);
157
130
  }
158
131
  }
159
- constructor(canvas, descriptors) {
132
+ /**
133
+ * Loads every pass's GLSL before the context is acquired, so a load that
134
+ * fails leaves the canvas free for Canvas2D.
135
+ */
136
+ static async create(canvas, descriptors) {
137
+ const glsl = await Promise.all(descriptors.map(async (d) => [d.id, await d.source.glsl()]));
138
+ return new WebGL2Hal(canvas, descriptors, new Map(glsl));
139
+ }
140
+ constructor(canvas, descriptors, glsl) {
160
141
  super(descriptors, 'WebGL2Hal');
161
142
  this.canvas = canvas;
143
+ this.glsl = glsl;
162
144
  this.debug = debugEnabled();
163
145
  totalCreated += 1;
164
146
  this.instanceId = totalCreated;
@@ -178,23 +160,14 @@ export class WebGL2Hal extends GpuHalBase {
178
160
  canvas.addEventListener('webglcontextrestored', onContextRestored, false);
179
161
  this.contextLostListener = onContextLost;
180
162
  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.
163
+ // The blend leaves premultiplied values in the framebuffer, which a
164
+ // straight-alpha compositor would darken at AA edges; WebGPU's alphaMode
165
+ // matches.
190
166
  const gl = canvas.getContext('webgl2', {
191
167
  antialias: true,
192
168
  premultipliedAlpha: true,
193
169
  });
194
170
  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
171
  throw canvasContextError(canvas, 'webgl2');
199
172
  }
200
173
  noteCanvasContext(canvas, 'webgl2');
@@ -203,29 +176,20 @@ export class WebGL2Hal extends GpuHalBase {
203
176
  this.ubo = gl.createBuffer();
204
177
  gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo);
205
178
  gl.bufferData(gl.UNIFORM_BUFFER, this.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.
179
+ // Programs link on first draw (`getPass`). The first links here as a
180
+ // canary, so a GL stack that cannot compile our shaders throws while
181
+ // `createGpuHal` can still fall back to Canvas2D.
218
182
  const canary = descriptors[0];
219
183
  if (canary) {
220
- this.compilePass(canary);
184
+ this.passes.set(canary.id, {
185
+ descriptor: canary,
186
+ linked: this.link(canary),
187
+ });
221
188
  }
222
189
  gl.enable(gl.BLEND);
223
190
  }
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.
191
+ // Past the ceiling an unguarded bufferData loses the context in Chrome and
192
+ // throws in Firefox, neither of them a getError() case.
229
193
  limits() {
230
194
  return {
231
195
  maxBufferBytes: MAX_VERTEX_BUFFER_BYTES,
@@ -239,28 +203,55 @@ export class WebGL2Hal extends GpuHalBase {
239
203
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
240
204
  return { vbo, count };
241
205
  }
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.
206
+ // No mid-frame deferral, unlike `WebGPUHal`: GL is immediate-mode, so a draw
207
+ // already issued has consumed the buffer.
246
208
  destroyBuffer(buf) {
247
209
  const gl = this.gl;
248
210
  if (!this.contextWasLost && !gl.isContextLost()) {
249
211
  gl.deleteBuffer(buf.vbo);
250
212
  }
251
213
  }
252
- compilePass(desc) {
214
+ // Throws on a failed link, and again on every later ask for that content.
215
+ link(desc) {
253
216
  const gl = this.gl;
254
- const program = createProgram(gl, desc.glslVertex, desc.glslFragment);
217
+ const stages = this.glsl.get(desc.id);
218
+ if (!stages) {
219
+ throw new Error(`no GLSL was loaded for pass "${desc.id}"`);
220
+ }
221
+ const { GLSL_VERTEX, GLSL_FRAGMENT } = stages;
222
+ const tb = desc.textures?.[0];
223
+ const names = desc.vertexAttributes.map(attr => attr.name);
224
+ const sampler = tb && [tb.glUniformName, tb.glTextureUnit];
225
+ const key = JSON.stringify([GLSL_FRAGMENT, names, sampler]);
226
+ let byVertex = this.programs.get(GLSL_VERTEX);
227
+ if (!byVertex) {
228
+ byVertex = new Map();
229
+ this.programs.set(GLSL_VERTEX, byVertex);
230
+ }
231
+ const known = byVertex.get(key);
232
+ if (known instanceof Error) {
233
+ throw known;
234
+ }
235
+ if (known) {
236
+ return known;
237
+ }
238
+ let program;
239
+ try {
240
+ program = createProgram(gl, GLSL_VERTEX, GLSL_FRAGMENT);
241
+ }
242
+ catch (e) {
243
+ byVertex.set(key, e instanceof Error ? e : new Error(String(e)));
244
+ throw e;
245
+ }
255
246
  bindUniformBlock(gl, program, 'Uniforms', 0);
256
247
  this.checkGlError(`link pass "${desc.id}"`);
257
- const attrLocs = desc.vertexAttributes.map(attr => gl.getAttribLocation(program, attr.name));
248
+ const attrLocs = names.map(name => gl.getAttribLocation(program, name));
258
249
  if (this.debug) {
259
- const pairs = desc.vertexAttributes.map((a, i) => `${a.name}@${attrLocs[i]}`);
250
+ const pairs = names.map((name, i) => `${name}@${attrLocs[i]}`);
260
251
  console.warn(`[WebGL2Hal] pass "${desc.id}" stride=${desc.instanceStride} attrs: ${pairs.join(', ')}`);
261
- const missing = desc.vertexAttributes.filter((_, i) => attrLocs[i] < 0);
252
+ const missing = names.filter((_, i) => attrLocs[i] < 0);
262
253
  if (missing.length > 0) {
263
- console.warn(`[WebGL2Hal] pass "${desc.id}" missing attribute locations: ${missing.map(a => a.name).join(', ')}`);
254
+ console.warn(`[WebGL2Hal] pass "${desc.id}" missing attribute locations: ${missing.join(', ')}`);
264
255
  }
265
256
  }
266
257
  const vao = gl.createVertexArray();
@@ -272,26 +263,14 @@ export class WebGL2Hal extends GpuHalBase {
272
263
  }
273
264
  }
274
265
  gl.bindVertexArray(null);
275
- let textureState = null;
276
- const tb = desc.textures?.[0];
277
266
  if (tb) {
278
- // Bind the sampler uniform to the texture unit once — it never changes.
279
267
  gl.useProgram(program);
280
268
  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;
269
+ }
270
+ const linked = { program, vao, attrLocs };
271
+ byVertex.set(key, linked);
272
+ return linked;
292
273
  }
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
274
  getPass(passId) {
296
275
  const existing = this.passes.get(passId);
297
276
  if (existing !== undefined) {
@@ -302,7 +281,9 @@ export class WebGL2Hal extends GpuHalBase {
302
281
  return undefined;
303
282
  }
304
283
  try {
305
- return this.compilePass(desc);
284
+ const pass = { descriptor: desc, linked: this.link(desc) };
285
+ this.passes.set(passId, pass);
286
+ return pass;
306
287
  }
307
288
  catch (e) {
308
289
  this.passes.set(passId, null);
@@ -315,12 +296,9 @@ export class WebGL2Hal extends GpuHalBase {
315
296
  }
316
297
  createTexture(passId, binding, data, width, height) {
317
298
  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);
299
+ const existing = this.passTextures.get(passId);
300
+ if (existing) {
301
+ gl.deleteTexture(existing);
324
302
  }
325
303
  const tex = gl.createTexture();
326
304
  gl.bindTexture(gl.TEXTURE_2D, tex);
@@ -337,7 +315,7 @@ export class WebGL2Hal extends GpuHalBase {
337
315
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
338
316
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
339
317
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
340
- ts.texture = tex;
318
+ this.passTextures.set(passId, tex);
341
319
  }
342
320
  writeUniforms(data) {
343
321
  const gl = this.gl;
@@ -369,8 +347,7 @@ export class WebGL2Hal extends GpuHalBase {
369
347
  }
370
348
  drawPass(passId, regionKey, bufferPassId) {
371
349
  const gl = this.gl;
372
- // Buffer first, program second: a pass with nothing to draw must not be
373
- // the reason its shader gets compiled.
350
+ // Buffer first, so a pass with nothing to draw links no program.
374
351
  const regionBuf = this.regions.get(regionKey, bufferPassId ?? passId);
375
352
  if (!regionBuf || regionBuf.count === 0) {
376
353
  return;
@@ -380,10 +357,10 @@ export class WebGL2Hal extends GpuHalBase {
380
357
  return;
381
358
  }
382
359
  this.applyBlendState(pass.descriptor);
383
- gl.useProgram(pass.program);
384
- gl.bindVertexArray(pass.vao);
360
+ gl.useProgram(pass.linked.program);
361
+ gl.bindVertexArray(pass.linked.vao);
385
362
  this.bindAttributes(pass, regionBuf.vbo);
386
- this.bindTextures(pass);
363
+ this.bindTexture(passId, pass.descriptor);
387
364
  const topo = pass.descriptor.topology ?? 'triangle-list';
388
365
  const glMode = topo === 'triangle-strip'
389
366
  ? gl.TRIANGLE_STRIP
@@ -409,10 +386,6 @@ export class WebGL2Hal extends GpuHalBase {
409
386
  if (this.debug) {
410
387
  console.warn(`[WebGL2Hal #${this.instanceId}] DISPOSING context (live=${totalCreated - totalDisposed}/${totalCreated})`);
411
388
  }
412
- // Remove canvas event listeners to prevent closure references from keeping
413
- // the context alive after disposal. This is critical for test suites where
414
- // multiple contexts are created in sequence (e.g. Puppeteer page navigation).
415
- // Double-dispose is short-circuited above, so listeners are guaranteed set.
416
389
  if (this.contextLostListener) {
417
390
  this.canvas.removeEventListener('webglcontextlost', this.contextLostListener);
418
391
  }
@@ -421,24 +394,24 @@ export class WebGL2Hal extends GpuHalBase {
421
394
  }
422
395
  this.regions.deleteAll();
423
396
  if (!this.contextWasLost && !gl.isContextLost()) {
424
- for (const pass of this.passes.values()) {
425
- if (pass) {
426
- gl.deleteVertexArray(pass.vao);
427
- gl.deleteProgram(pass.program);
428
- if (pass.textureState?.texture) {
429
- gl.deleteTexture(pass.textureState.texture);
397
+ for (const byVertex of this.programs.values()) {
398
+ for (const linked of byVertex.values()) {
399
+ if (!(linked instanceof Error)) {
400
+ gl.deleteVertexArray(linked.vao);
401
+ gl.deleteProgram(linked.program);
430
402
  }
431
403
  }
432
404
  }
405
+ for (const texture of this.passTextures.values()) {
406
+ gl.deleteTexture(texture);
407
+ }
433
408
  gl.deleteBuffer(this.ubo);
434
409
  }
435
410
  this.passes.clear();
436
- // Firefox appears to treat WEBGL_lose_context.loseContext() as a
437
- // driver-wide reset: calling it on one disposed HAL synchronously
438
- // knocks out sibling live contexts too, so tracks go blank en masse.
439
- // Chrome only needs this as a test-suite optimisation; for production we
440
- // let the browser reclaim the context when the canvas is GC'd. If we
441
- // need explicit release again, gate it on navigator.userAgent.
411
+ this.programs.clear();
412
+ this.passTextures.clear();
413
+ // No WEBGL_lose_context.loseContext(): Firefox treats it as a driver-wide
414
+ // reset that blanks sibling live contexts, so the canvas's GC reclaims it.
442
415
  }
443
416
  applyBlendState(desc) {
444
417
  const gl = this.gl;
@@ -449,30 +422,28 @@ export class WebGL2Hal extends GpuHalBase {
449
422
  gl.enable(gl.BLEND);
450
423
  const bs = desc.blendState;
451
424
  if (bs?.op === 'max') {
452
- // MIN/MAX ignore blend factors: the framebuffer keeps the per-channel
453
- // max(src, dst). Used by same-color AA lines so overlapping segments union
454
- // instead of darkening. Reset explicitly below for every other pass.
455
425
  gl.blendEquation(gl.MAX);
456
426
  }
427
+ else if (bs?.op === 'behind') {
428
+ gl.blendEquation(gl.FUNC_ADD);
429
+ gl.blendFuncSeparate(gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE);
430
+ }
457
431
  else {
458
432
  gl.blendEquation(gl.FUNC_ADD);
459
- // RGB and alpha get different blend factors (blendFuncSeparate):
460
- // RGB: out = src_rgb * srcFactor + dst_rgb * dstFactor (default: src-alpha / 1-src-alpha)
461
- // Alpha: out = src_alpha * 1 + dst_alpha * (1 - src_alpha)
462
- // The alpha channel uses ONE/ONE_MINUS_SRC_ALPHA regardless of the custom blend state;
463
- // using the RGB srcFactor for alpha too would give out_alpha = src_alpha² + ..., which is wrong.
433
+ // Alpha keeps ONE / ONE_MINUS_SRC_ALPHA whatever the RGB factors: the
434
+ // RGB srcFactor on alpha would square it.
464
435
  const src = bs ? glBlendFactor(gl, bs.srcFactor) : gl.SRC_ALPHA;
465
436
  const dst = bs ? glBlendFactor(gl, bs.dstFactor) : gl.ONE_MINUS_SRC_ALPHA;
466
437
  gl.blendFuncSeparate(src, dst, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
467
438
  }
468
439
  }
469
- bindTextures(pass) {
440
+ bindTexture(passId, desc) {
470
441
  const gl = this.gl;
471
- const ts = pass.textureState;
472
- if (ts?.texture) {
473
- gl.activeTexture(gl.TEXTURE0 + ts.unit);
474
- gl.bindTexture(gl.TEXTURE_2D, ts.texture);
475
- // sampler uniform set once at construction — no uniform1i here
442
+ const tb = desc.textures?.[0];
443
+ const texture = tb && this.passTextures.get(passId);
444
+ if (tb && texture) {
445
+ gl.activeTexture(gl.TEXTURE0 + tb.glTextureUnit);
446
+ gl.bindTexture(gl.TEXTURE_2D, texture);
476
447
  }
477
448
  }
478
449
  bindAttributes(pass, vbo) {
@@ -480,7 +451,7 @@ export class WebGL2Hal extends GpuHalBase {
480
451
  const desc = pass.descriptor;
481
452
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
482
453
  for (let i = 0; i < desc.vertexAttributes.length; i++) {
483
- const loc = pass.attrLocs[i];
454
+ const loc = pass.linked.attrLocs[i];
484
455
  if (loc < 0) {
485
456
  continue;
486
457
  }
@@ -11,13 +11,11 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
11
11
  private pipelines;
12
12
  private passTextures;
13
13
  private passBindGroups;
14
- private layouts;
15
14
  private alignedUniformSize;
16
15
  private uniformRingBuffer;
17
16
  private uniformStaging;
18
17
  private uniformSlot;
19
18
  private warnedUniformSlots;
20
- private uniformOnlyBindGroup;
21
19
  private sampleCount;
22
20
  private msaaTexture;
23
21
  private msaaView;
@@ -36,7 +34,7 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
36
34
  * selection spanning two sections re-uploads `OVERLAY_REGION` once per
37
35
  * section, inside the block loop.
38
36
  *
39
- * Deferring is what makes mid-frame replacement legal instead of merely
37
+ * Deferring the destroy makes mid-frame replacement legal instead of merely
40
38
  * warned about; synteny's `ensureUploaded` has always deleted a pass mid-frame
41
39
  * that the open pass never referenced, and that case was never the bug.
42
40
  */
@@ -49,6 +47,9 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
49
47
  private swapChainUnrecoverable;
50
48
  private reportedSurfaceFailure;
51
49
  private constructor();
50
+ private bindGroupLayoutOf;
51
+ private createBindGroup;
52
+ private bindingResource;
52
53
  protected limits(): {
53
54
  maxBufferBytes: number;
54
55
  maxTextureDimensionPx: number;
@@ -81,13 +82,12 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
81
82
  * (and a dropped draw) the moment one side samples a texture.
82
83
  *
83
84
  * Nothing in a bind group varies per region either: it references the
84
- * HAL-wide uniform ring buffer plus the pass's own texture/sampler. So
85
- * uniform-only passes all share `uniformOnlyBindGroup`, and a textured pass
86
- * gets exactly one, cached until `uploadTexture` replaces its texture.
87
- * Returns undefined when a pass needs a texture that hasn't arrived yet;
88
- * drawPass skips those.
85
+ * HAL-wide uniform ring buffer plus the pass's own texture/sampler. Returns
86
+ * undefined when a pass needs a texture that hasn't arrived yet; drawPass
87
+ * skips those.
89
88
  */
90
89
  private getBindGroup;
90
+ private bindTexture;
91
91
  /**
92
92
  * The one place a GPU resource this HAL owns is released — see
93
93
  * {@link pendingDestroy} for why the frame decides when.