@jbrowse/render-core 5.0.0-beta.2 → 5.0.0-beta.3

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 (110) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +20 -1
  2. package/esm/RenderLifecycleMixin.js +25 -2
  3. package/esm/canvas2dUtils.d.ts +25 -0
  4. package/esm/canvas2dUtils.js +27 -0
  5. package/esm/colorRampLut.d.ts +4 -4
  6. package/esm/colorRampLut.js +4 -4
  7. package/esm/computePipeline.d.ts +13 -0
  8. package/esm/computePipeline.js +55 -0
  9. package/esm/coverageBand.d.ts +0 -1
  10. package/esm/coverageBand.js +0 -1
  11. package/esm/coverageBandBuffers.d.ts +0 -6
  12. package/esm/coverageBandBuffers.js +1 -13
  13. package/esm/createRenderingBackend.d.ts +14 -18
  14. package/esm/createRenderingBackend.js +4 -2
  15. package/esm/encodeMemo.d.ts +14 -0
  16. package/esm/encodeMemo.js +48 -0
  17. package/esm/gpuDevice.d.ts +9 -0
  18. package/esm/gpuDevice.js +12 -3
  19. package/esm/graphicsCapabilities.d.ts +18 -15
  20. package/esm/graphicsCapabilities.js +23 -16
  21. package/esm/hal/createHal.d.ts +1 -2
  22. package/esm/hal/createHal.js +14 -4
  23. package/esm/hal/deviceGpuCache.js +2 -2
  24. package/esm/hal/gpuHalBase.d.ts +1 -1
  25. package/esm/hal/gpuHalBase.js +2 -3
  26. package/esm/hal/mockHal.d.ts +0 -1
  27. package/esm/hal/mockHal.js +0 -10
  28. package/esm/hal/regionRegistry.d.ts +0 -1
  29. package/esm/hal/regionRegistry.js +3 -15
  30. package/esm/hal/types.d.ts +5 -1
  31. package/esm/hal/webgl2Hal.d.ts +1 -1
  32. package/esm/hal/webgl2Hal.js +2 -2
  33. package/esm/hal/webgpuHal.d.ts +2 -2
  34. package/esm/hal/webgpuHal.js +13 -10
  35. package/esm/installUpload.d.ts +4 -1
  36. package/esm/installUpload.js +7 -39
  37. package/esm/instanceCache.d.ts +10 -7
  38. package/esm/instanceCache.js +14 -15
  39. package/esm/mapUploadSync.d.ts +0 -8
  40. package/esm/mapUploadSync.js +0 -1
  41. package/esm/marks/colorFill.d.ts +17 -0
  42. package/esm/marks/colorFill.js +26 -0
  43. package/esm/marks/drawAgainstHit.d.ts +47 -0
  44. package/esm/marks/drawAgainstHit.js +140 -0
  45. package/esm/marks/glyphPaint.d.ts +16 -0
  46. package/esm/marks/glyphPaint.js +41 -0
  47. package/esm/marks/index.d.ts +9 -0
  48. package/esm/marks/index.js +6 -0
  49. package/esm/marks/markBackend.d.ts +87 -0
  50. package/esm/marks/markBackend.js +133 -0
  51. package/esm/marks/markPaint.d.ts +13 -0
  52. package/esm/marks/markPaint.js +18 -0
  53. package/esm/marks/pointMark.d.ts +24 -0
  54. package/esm/marks/pointMark.js +98 -0
  55. package/esm/marks/spanMark.d.ts +45 -0
  56. package/esm/marks/spanMark.js +71 -0
  57. package/esm/marks/types.d.ts +206 -0
  58. package/esm/marks/types.js +125 -0
  59. package/esm/perRegionRenderingBackend.d.ts +15 -3
  60. package/esm/perRegionRenderingBackend.js +26 -3
  61. package/esm/regionDataMap.d.ts +2 -2
  62. package/esm/regionDataMap.js +2 -2
  63. package/esm/renderingBackendBase.d.ts +11 -14
  64. package/esm/renderingBackendBase.js +7 -7
  65. package/esm/shaders/capsule.js.generated.d.ts +1 -0
  66. package/esm/shaders/capsule.js.generated.js +11 -0
  67. package/esm/shaders/coverageBar.generated.d.ts +2 -2
  68. package/esm/shaders/coverageBar.generated.js +2 -2
  69. package/esm/shaders/coverageMod.generated.d.ts +2 -2
  70. package/esm/shaders/coverageMod.generated.js +2 -2
  71. package/esm/shaders/coverageSnp.generated.d.ts +2 -2
  72. package/esm/shaders/coverageSnp.generated.js +2 -2
  73. package/esm/shaders/pointMark.consts.generated.d.ts +4 -0
  74. package/esm/shaders/pointMark.consts.generated.js +6 -0
  75. package/esm/shaders/pointMark.generated.d.ts +5 -0
  76. package/esm/shaders/pointMark.generated.js +7 -0
  77. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  78. package/esm/shaders/pointMark.iface.generated.js +100 -0
  79. package/esm/shaders/pointMark.js.generated.d.ts +1 -0
  80. package/esm/shaders/pointMark.js.generated.js +16 -0
  81. package/esm/shaders/spanMark.generated.d.ts +4 -0
  82. package/esm/shaders/spanMark.generated.js +6 -0
  83. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  84. package/esm/shaders/spanMark.iface.generated.js +122 -0
  85. package/esm/sharedBackendKey.d.ts +18 -0
  86. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  87. package/esm/slangPass.d.ts +2 -0
  88. package/esm/slangPass.js +2 -0
  89. package/package.json +13 -4
  90. package/src/shaders/capsule.js.generated.ts +14 -0
  91. package/src/shaders/capsule.slang +12 -4
  92. package/src/shaders/colorPack.slang +5 -1
  93. package/src/shaders/coverageBand.slang +38 -15
  94. package/src/shaders/coverageBar.generated.ts +2 -2
  95. package/src/shaders/coverageBar.slang +9 -0
  96. package/src/shaders/coverageMod.generated.ts +2 -2
  97. package/src/shaders/coverageSnp.generated.ts +2 -2
  98. package/src/shaders/hpmath.slang +7 -1
  99. package/src/shaders/pointMark.consts.generated.ts +10 -0
  100. package/src/shaders/pointMark.generated.ts +12 -0
  101. package/src/shaders/pointMark.iface.generated.ts +146 -0
  102. package/src/shaders/pointMark.js.generated.ts +21 -0
  103. package/src/shaders/pointMark.slang +192 -0
  104. package/src/shaders/rowRect.slang +7 -8
  105. package/src/shaders/spanMark.generated.ts +10 -0
  106. package/src/shaders/spanMark.iface.generated.ts +168 -0
  107. package/src/shaders/spanMark.slang +22 -0
  108. package/esm/globalRenderingBackend.d.ts +0 -90
  109. package/esm/globalRenderingBackend.js +0 -53
  110. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -119,6 +119,23 @@ export declare function RenderLifecycleMixin(): import("@jbrowse/mobx-state-tree
119
119
  * and the two families compose the two mixins in opposite orders.
120
120
  */
121
121
  readonly paintInert: boolean;
122
+ /**
123
+ * #getter
124
+ * Overridable hook (default false): what is on the canvas was painted
125
+ * from data a later change has made wrong, so `painted` below should
126
+ * answer *pending* until the repaint lands even though `canvasDrawn` is
127
+ * still true. The per-region fetch foundation fills it with
128
+ * `staleSettingsDrawn` — held data drawn under settings that have since
129
+ * moved — which is the state `clearAllRpcData` used to express by
130
+ * resetting `canvasDrawn` on every settings change, blanking the display
131
+ * to do it. A capture waiting on `data-display-drawn` then waits for the
132
+ * refetch rather than snapshotting the previous setting's pixels.
133
+ *
134
+ * A hook and not a reset of the flag, because the flag is re-marked by
135
+ * any redraw — a pan between the settings change and the refetch would
136
+ * report the stale canvas drawn — and a derivation cannot be raced.
137
+ */
138
+ readonly paintSuperseded: boolean;
122
139
  } & {
123
140
  /**
124
141
  * #getter
@@ -140,7 +157,9 @@ export declare function RenderLifecycleMixin(): import("@jbrowse/mobx-state-tree
140
157
  *
141
158
  * `paintInert` is the third term and the same argument once more, for the
142
159
  * state where a display *would* paint a canvas and never gets to — a fetch
143
- * that failed before first paint. See that hook.
160
+ * that failed before first paint. `paintSuperseded` is the fourth, and
161
+ * the one that subtracts: a canvas painted from data a settings change
162
+ * has made wrong is drawn and not finished. See both hooks.
144
163
  */
145
164
  readonly painted: boolean;
146
165
  } & {
@@ -132,6 +132,25 @@ export function RenderLifecycleMixin() {
132
132
  get paintInert() {
133
133
  return false;
134
134
  },
135
+ /**
136
+ * #getter
137
+ * Overridable hook (default false): what is on the canvas was painted
138
+ * from data a later change has made wrong, so `painted` below should
139
+ * answer *pending* until the repaint lands even though `canvasDrawn` is
140
+ * still true. The per-region fetch foundation fills it with
141
+ * `staleSettingsDrawn` — held data drawn under settings that have since
142
+ * moved — which is the state `clearAllRpcData` used to express by
143
+ * resetting `canvasDrawn` on every settings change, blanking the display
144
+ * to do it. A capture waiting on `data-display-drawn` then waits for the
145
+ * refetch rather than snapshotting the previous setting's pixels.
146
+ *
147
+ * A hook and not a reset of the flag, because the flag is re-marked by
148
+ * any redraw — a pan between the settings change and the refetch would
149
+ * report the stale canvas drawn — and a derivation cannot be raced.
150
+ */
151
+ get paintSuperseded() {
152
+ return false;
153
+ },
135
154
  }))
136
155
  .views(self => ({
137
156
  /**
@@ -154,10 +173,14 @@ export function RenderLifecycleMixin() {
154
173
  *
155
174
  * `paintInert` is the third term and the same argument once more, for the
156
175
  * state where a display *would* paint a canvas and never gets to — a fetch
157
- * that failed before first paint. See that hook.
176
+ * that failed before first paint. `paintSuperseded` is the fourth, and
177
+ * the one that subtracts: a canvas painted from data a settings change
178
+ * has made wrong is drawn and not finished. See both hooks.
158
179
  */
159
180
  get painted() {
160
- return self.canvasDrawn || !self.rendersCanvas || self.paintInert;
181
+ return ((self.canvasDrawn && !self.paintSuperseded) ||
182
+ !self.rendersCanvas ||
183
+ self.paintInert);
161
184
  },
162
185
  }))
163
186
  .actions(self => ({
@@ -203,6 +203,31 @@ export declare function strokeRectInside(ctx: {
203
203
  * Scalar in, scalar out: runs per feature per frame, so it must not allocate.
204
204
  */
205
205
  export declare function spanLeft(x1: number, x2: number, width: number): number;
206
+ /**
207
+ * The `fillRect` x and width a genomic span paints as, given a bare
208
+ * `makeBpMapper`: `spanLeft`'s pivot and the min-width floor that goes with it,
209
+ * as one value a painter and its hit box can both read.
210
+ *
211
+ * `minWidth` defaults to 0, which makes this plain screen ordering — the
212
+ * leftmost projected edge and the unsigned distance. Pass a floor and the span
213
+ * grows away from `startBp`, which is the reversed-block rule `spanLeft` owns:
214
+ * anchoring the leftmost edge instead slides a sub-pixel mark by up to the floor
215
+ * on a flipped region and never on a forward one.
216
+ *
217
+ * Reach for this over `spanLeft` wherever the *numbers* are wanted rather than a
218
+ * fill — a hover box, an SVG rect, a cull test, an overlay div — which is where
219
+ * the pivot and the floor were being restated separately. The multi-row painter
220
+ * and the hover box it has to line up with each spelled out both.
221
+ * `fillBpSpan` is the same span when a `fillRect` is the only consumer.
222
+ *
223
+ * Not for a per-instance paint loop: this allocates a result per call, so the
224
+ * span mark's painter and wiggle's two bar loops take `spanLeft` and the two
225
+ * numbers inline, and stay the last hand-written copies of this arithmetic.
226
+ */
227
+ export declare function spanRect(toX: (bp: number) => number, startBp: number, endBp: number, minWidth?: number): {
228
+ left: number;
229
+ width: number;
230
+ };
206
231
  export declare function makeRampFillStyleLut(ramp: Uint8Array): (t: number) => string;
207
232
  export declare function bpToScreenPx(absBp: number, regionStart: number, regionEnd: number, screenStartPx: number, screenEndPx: number, reversed?: boolean): number;
208
233
  export declare function makeBpMapper(bounds: BpRegionBounds): (bp: number) => number;
@@ -334,6 +334,33 @@ export function strokeRectInside(ctx, x, y, w, h, lineWidth = 1) {
334
334
  export function spanLeft(x1, x2, width) {
335
335
  return x2 < x1 ? x1 - width : x1;
336
336
  }
337
+ /**
338
+ * The `fillRect` x and width a genomic span paints as, given a bare
339
+ * `makeBpMapper`: `spanLeft`'s pivot and the min-width floor that goes with it,
340
+ * as one value a painter and its hit box can both read.
341
+ *
342
+ * `minWidth` defaults to 0, which makes this plain screen ordering — the
343
+ * leftmost projected edge and the unsigned distance. Pass a floor and the span
344
+ * grows away from `startBp`, which is the reversed-block rule `spanLeft` owns:
345
+ * anchoring the leftmost edge instead slides a sub-pixel mark by up to the floor
346
+ * on a flipped region and never on a forward one.
347
+ *
348
+ * Reach for this over `spanLeft` wherever the *numbers* are wanted rather than a
349
+ * fill — a hover box, an SVG rect, a cull test, an overlay div — which is where
350
+ * the pivot and the floor were being restated separately. The multi-row painter
351
+ * and the hover box it has to line up with each spelled out both.
352
+ * `fillBpSpan` is the same span when a `fillRect` is the only consumer.
353
+ *
354
+ * Not for a per-instance paint loop: this allocates a result per call, so the
355
+ * span mark's painter and wiggle's two bar loops take `spanLeft` and the two
356
+ * numbers inline, and stay the last hand-written copies of this arithmetic.
357
+ */
358
+ export function spanRect(toX, startBp, endBp, minWidth = 0) {
359
+ const x1 = toX(startBp);
360
+ const x2 = toX(endBp);
361
+ const width = Math.max(minWidth, Math.abs(x2 - x1));
362
+ return { left: spanLeft(x1, x2, width), width };
363
+ }
337
364
  // Quantize a 0..1 ramp position to a 256-entry index. Shared so the direct
338
365
  // lookup and the fillStyle LUT below can't quantize differently — a half-step
339
366
  // of drift between them shows up as a one-shade mismatch between a cell's fill
@@ -9,9 +9,9 @@ export declare const COLOR_RAMP_LUT_ENTRIES = 256;
9
9
  /**
10
10
  * Upload a 256-entry RGBA colour-ramp LUT as each named pass's 256×1 texture —
11
11
  * the runtime half of the shared ramp mechanism whose shader half is
12
- * `shaders/colorRampLut.slang`. HiC, LD (whose two shader variants are the
13
- * reason this takes a pass list) and wiggle density all put their ramps behind
14
- * their samplers through this one call; each keeps its own scale and its own
15
- * LUT contents.
12
+ * `shaders/colorRampLut.slang`. Called by `createMarkBackend` alone now, for
13
+ * every mark declaring a `texture`: hic, LD's two shader variants and wiggle
14
+ * density all put their ramps behind their samplers through it, each keeping
15
+ * its own scale and its own LUT contents.
16
16
  */
17
17
  export declare function uploadColorRampLut(hal: GpuHal, ramp: Uint8Array, passIds: readonly string[]): void;
@@ -9,10 +9,10 @@ export const COLOR_RAMP_LUT_ENTRIES = RAMP_LUT_ENTRIES;
9
9
  /**
10
10
  * Upload a 256-entry RGBA colour-ramp LUT as each named pass's 256×1 texture —
11
11
  * the runtime half of the shared ramp mechanism whose shader half is
12
- * `shaders/colorRampLut.slang`. HiC, LD (whose two shader variants are the
13
- * reason this takes a pass list) and wiggle density all put their ramps behind
14
- * their samplers through this one call; each keeps its own scale and its own
15
- * LUT contents.
12
+ * `shaders/colorRampLut.slang`. Called by `createMarkBackend` alone now, for
13
+ * every mark declaring a `texture`: hic, LD's two shader variants and wiggle
14
+ * density all put their ramps behind their samplers through it, each keeping
15
+ * its own scale and its own LUT contents.
16
16
  */
17
17
  export function uploadColorRampLut(hal, ramp, passIds) {
18
18
  if (ramp.length !== COLOR_RAMP_LUT_ENTRIES * 4) {
@@ -0,0 +1,13 @@
1
+ import type { ShaderBinding } from './hal/index.ts';
2
+ export interface ComputePipelineState {
3
+ device: GPUDevice;
4
+ pipeline: GPUComputePipeline;
5
+ bindGroupLayout: GPUBindGroupLayout;
6
+ bindings: readonly ShaderBinding[];
7
+ }
8
+ /**
9
+ * One compute pipeline per kernel, built on first use against the current
10
+ * device and rebuilt after a device loss. Concurrent first callers share the
11
+ * one async build.
12
+ */
13
+ export declare function makeComputePipelineCache(code: string, entryPoint: string, bindings: readonly ShaderBinding[]): (device: GPUDevice) => Promise<ComputePipelineState>;
@@ -0,0 +1,55 @@
1
+ /// <reference types="@webgpu/types" />
2
+ import { onDeviceLost } from './gpuDevice.js';
3
+ // Built from the kernel's own reflected binding table rather than restated by
4
+ // the caller, so a mismatch shows up as a validation failure at pipeline
5
+ // creation rather than as a wrong buffer under a right-looking index.
6
+ function makeBindGroupLayout(device, bindings) {
7
+ return device.createBindGroupLayout({
8
+ entries: bindings.map(b => {
9
+ if (b.kind === 'texture' || b.kind === 'sampler') {
10
+ throw new Error(`compute kernel: binding ${b.index} ('${b.name}') is a ${b.kind}, ` +
11
+ `which a buffer-only dispatch does not bind`);
12
+ }
13
+ return {
14
+ binding: b.index,
15
+ visibility: GPUShaderStage.COMPUTE,
16
+ buffer: { type: b.kind },
17
+ };
18
+ }),
19
+ });
20
+ }
21
+ /**
22
+ * One compute pipeline per kernel, built on first use against the current
23
+ * device and rebuilt after a device loss. Concurrent first callers share the
24
+ * one async build.
25
+ */
26
+ export function makeComputePipelineCache(code, entryPoint, bindings) {
27
+ let state = null;
28
+ let statePromise = null;
29
+ onDeviceLost(() => {
30
+ state = null;
31
+ statePromise = null;
32
+ });
33
+ return async function ensurePipeline(device) {
34
+ if (state?.device === device) {
35
+ return state;
36
+ }
37
+ if (statePromise) {
38
+ return statePromise;
39
+ }
40
+ statePromise = (async () => {
41
+ const module = device.createShaderModule({ code });
42
+ const bindGroupLayout = makeBindGroupLayout(device, bindings);
43
+ const pipeline = await device.createComputePipelineAsync({
44
+ layout: device.createPipelineLayout({
45
+ bindGroupLayouts: [bindGroupLayout],
46
+ }),
47
+ compute: { module, entryPoint },
48
+ });
49
+ state = { device, pipeline, bindGroupLayout, bindings };
50
+ statePromise = null;
51
+ return state;
52
+ })();
53
+ return statePromise;
54
+ };
55
+ }
@@ -1,6 +1,5 @@
1
1
  import type { CoverageBandBuffers } from './coverageBandBuffers.ts';
2
2
  import type { InstancePass } from './instancePass.ts';
3
- export { coverageBandBuffers } from './coverageBandBuffers.ts';
4
3
  export type { CoverageBandBuffers } from './coverageBandBuffers.ts';
5
4
  export declare const COVERAGE_BAND_UNIFORMS_SIZE_BYTES = 112;
6
5
  /**
@@ -4,7 +4,6 @@ import * as coverageInterbaseShader from './shaders/coverageInterbase.generated.
4
4
  import * as coverageModShader from './shaders/coverageMod.generated.js';
5
5
  import * as coverageSnpShader from './shaders/coverageSnp.generated.js';
6
6
  import { slangPass } from './slangPass.js';
7
- export { coverageBandBuffers } from './coverageBandBuffers.js';
8
7
  export const COVERAGE_BAND_UNIFORMS_SIZE_BYTES = coverageBarShader.UNIFORMS_SIZE_BYTES;
9
8
  /**
10
9
  * The band's z-order, back to front: the depth bars, the SNP slices stacked
@@ -22,9 +22,3 @@ export interface CoverageBandBuffers {
22
22
  interbasePackedBuffer: ArrayBuffer;
23
23
  indicatorPackedBuffer: ArrayBuffer;
24
24
  }
25
- /**
26
- * Just the band's four buffers out of a wider per-region payload — the field set
27
- * stated once, so a display carrying them into its own upload payload cannot
28
- * spell three of them.
29
- */
30
- export declare function coverageBandBuffers(src: CoverageBandBuffers): CoverageBandBuffers;
@@ -1,13 +1 @@
1
- /**
2
- * Just the band's four buffers out of a wider per-region payload — the field set
3
- * stated once, so a display carrying them into its own upload payload cannot
4
- * spell three of them.
5
- */
6
- export function coverageBandBuffers(src) {
7
- return {
8
- coveragePackedBuffer: src.coveragePackedBuffer,
9
- snpPackedBuffer: src.snpPackedBuffer,
10
- interbasePackedBuffer: src.interbasePackedBuffer,
11
- indicatorPackedBuffer: src.indicatorPackedBuffer,
12
- };
13
- }
1
+ export {};
@@ -7,31 +7,27 @@ import type { GpuHal, PipelineDescriptor, SampleCount } from './hal/types.ts';
7
7
  */
8
8
  export interface RenderingBackendOptions<TRenderingBackend> {
9
9
  passes: PipelineDescriptor[];
10
- uniformByteSize: number;
11
10
  /**
12
- * Samples per pixel this display's WebGPU target is allocated at — 4 unless
13
- * stated, which is what every display asked for while this was a build-time
14
- * constant.
11
+ * Samples per pixel this display's WebGPU target is allocated at. Omitted, it
12
+ * is derived from `passes`: 1 when every registered pass declares
13
+ * `//! coverage: analytic`, else 4. Registered, not drawn — an inactive pass
14
+ * that leans on MSAA holds its display at 4. Passing it overrides the
15
+ * derivation in either direction.
15
16
  *
16
- * **It is a property of the display, not of the build**, because what it buys
17
- * differs per display and what it costs does not. The cost is one colour
18
- * attachment the size of the canvas, so an empty 600px track pays what a full
19
- * one does and eight ordinary tracks on a retina panel hold 109.7 MiB nothing
20
- * counts (ARCHITECTURAL_LIMITS.md §"The MSAA target is the largest
21
- * per-display allocation"). What it buys is nothing at all for a display
22
- * whose fragments compute their own coverage, and the one fix there is for
23
- * conflation at the shared edges of tiled cells, which is Hi-C and LD.
24
- *
25
- * Setting it to 1 allocates **no** target rather than a smaller one. Which
26
- * displays should is a look-at-the-pixels decision taken one display at a
27
- * time; `ideas/arc-antialiasing-without-msaa.md` is the survey and the
28
- * captures.
17
+ * The cost is one colour attachment the size of the canvas — an empty 600px
18
+ * track pays what a full one does, and eight ordinary tracks on a retina
19
+ * panel hold 109.7 MiB nothing counts (ARCHITECTURAL_LIMITS.md §"The MSAA
20
+ * target is the largest per-display allocation"); 1 allocates none. Whether a
21
+ * display's pixels survive the flip is a look-at-the-pixels decision taken
22
+ * one display at a time; `ideas/arc-antialiasing-without-msaa.md` is the
23
+ * survey and the captures.
29
24
  */
30
25
  sampleCount?: SampleCount;
31
26
  createGpuBackend: (hal: GpuHal) => TRenderingBackend;
32
27
  createCanvas2DBackend: (canvas: HTMLCanvasElement) => TRenderingBackend;
33
28
  }
34
- export declare function createRenderingBackend<TRenderingBackend>(canvas: HTMLCanvasElement, { passes, uniformByteSize, sampleCount, createGpuBackend, createCanvas2DBackend, }: RenderingBackendOptions<TRenderingBackend>): Promise<TRenderingBackend>;
29
+ export declare function deriveSampleCount(passes: readonly PipelineDescriptor[]): SampleCount;
30
+ export declare function createRenderingBackend<TRenderingBackend>(canvas: HTMLCanvasElement, { passes, sampleCount, createGpuBackend, createCanvas2DBackend, }: RenderingBackendOptions<TRenderingBackend>): Promise<TRenderingBackend>;
35
31
  /**
36
32
  * Factory for a **Canvas2D-only** display — one that ships no GPU shader path.
37
33
  *
@@ -1,5 +1,8 @@
1
1
  import { assertUniquePassIds, createGpuHal } from './hal/index.js';
2
- export async function createRenderingBackend(canvas, { passes, uniformByteSize, sampleCount = 4, createGpuBackend, createCanvas2DBackend, }) {
2
+ export function deriveSampleCount(passes) {
3
+ return passes.every(p => p.coverage === 'analytic') ? 1 : 4;
4
+ }
5
+ export async function createRenderingBackend(canvas, { passes, sampleCount = deriveSampleCount(passes), createGpuBackend, createCanvas2DBackend, }) {
3
6
  // Before the ladder, so a duplicate id is reported on every machine rather
4
7
  // than only the ones that reach a GPU rung — the bug it prevents is silent
5
8
  // and GPU-only, which is precisely why a developer on the Canvas2D fallback
@@ -15,7 +18,6 @@ export async function createRenderingBackend(canvas, { passes, uniformByteSize,
15
18
  const ladderFailures = [];
16
19
  const hal = await createGpuHal(canvas, {
17
20
  passes,
18
- uniformByteSize,
19
21
  sampleCount,
20
22
  failures: ladderFailures,
21
23
  });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A per-key encode memo over a display's cell map: `encode` runs for a key
3
+ * when its data reference moves, every key re-encodes when `inputs` yields a
4
+ * new identity, and a departed key is dropped. `inputs` runs through a
5
+ * `computed`, so a getter that allocates a fresh object per read re-encodes
6
+ * only when what it read changed.
7
+ *
8
+ * The returned map is a new instance whenever an entry moved and the same one
9
+ * otherwise, so an MST view holding it invalidates its observers exactly when
10
+ * a cell did. `installUpload` runs its `encode` path on this; a display that
11
+ * needs the encoded map itself — for a hit test, an SVG export — holds one in
12
+ * a `.views` closure and hands the installer identity cells.
13
+ */
14
+ export declare function createEncodeMemo<K, Data, Props, Encoded>(cells: () => ReadonlyMap<K, Data>, inputs: (() => Props) | undefined, encode: (data: Data, props: Props, key: K) => Encoded): () => ReadonlyMap<K, Encoded>;
@@ -0,0 +1,48 @@
1
+ import { computed } from 'mobx';
2
+ /**
3
+ * A per-key encode memo over a display's cell map: `encode` runs for a key
4
+ * when its data reference moves, every key re-encodes when `inputs` yields a
5
+ * new identity, and a departed key is dropped. `inputs` runs through a
6
+ * `computed`, so a getter that allocates a fresh object per read re-encodes
7
+ * only when what it read changed.
8
+ *
9
+ * The returned map is a new instance whenever an entry moved and the same one
10
+ * otherwise, so an MST view holding it invalidates its observers exactly when
11
+ * a cell did. `installUpload` runs its `encode` path on this; a display that
12
+ * needs the encoded map itself — for a hit test, an SVG export — holds one in
13
+ * a `.views` closure and hands the installer identity cells.
14
+ */
15
+ export function createEncodeMemo(cells, inputs, encode) {
16
+ const encoded = new Map();
17
+ const encodedFrom = new Map();
18
+ const props = inputs && computed(inputs);
19
+ let lastProps;
20
+ let snapshot = encoded;
21
+ return () => {
22
+ const current = cells();
23
+ const p = props ? props.get() : undefined;
24
+ if (p !== lastProps) {
25
+ lastProps = p;
26
+ encodedFrom.clear();
27
+ }
28
+ let changed = false;
29
+ for (const [key, data] of current) {
30
+ if (encodedFrom.get(key) !== data) {
31
+ encoded.set(key, encode(data, p, key));
32
+ encodedFrom.set(key, data);
33
+ changed = true;
34
+ }
35
+ }
36
+ for (const key of encoded.keys()) {
37
+ if (!current.has(key)) {
38
+ encoded.delete(key);
39
+ encodedFrom.delete(key);
40
+ changed = true;
41
+ }
42
+ }
43
+ if (changed) {
44
+ snapshot = new Map(encoded);
45
+ }
46
+ return snapshot;
47
+ };
48
+ }
@@ -1,3 +1,4 @@
1
+ import type { RendererName } from './graphicsCapabilities.ts';
1
2
  interface GpuDeviceCell {
2
3
  device: GPUDevice | null;
3
4
  /** Serializes concurrent calls during async init and after recovery. */
@@ -30,6 +31,12 @@ interface GpuDeviceCell {
30
31
  * @see setGpuOverride
31
32
  */
32
33
  gpuOverride: string | null;
34
+ /**
35
+ * The rung the last `createGpuHal` returned, so `effectiveRenderer` reports
36
+ * what drew rather than what the probe predicted. Optional because a cell an
37
+ * older copy built has none, and "nothing built yet" is a real state there.
38
+ */
39
+ builtRenderer?: RendererName;
33
40
  }
34
41
  declare global {
35
42
  var __jbrowseRenderCoreGpuDeviceV1: GpuDeviceCell | undefined;
@@ -61,6 +68,8 @@ export type GpuOverride = (typeof GPU_OVERRIDES)[number];
61
68
  */
62
69
  export declare function setGpuOverride(value: string | null): void;
63
70
  export declare function getGpuOverride(): string | null;
71
+ export declare function setBuiltRenderer(name: RendererName): void;
72
+ export declare function getBuiltRenderer(): RendererName | undefined;
64
73
  /**
65
74
  * Whether the GPU is off for the whole page — either pinned by `?renderer=` at
66
75
  * startup or switched off from a display's GPU-error banner after an
package/esm/gpuDevice.js CHANGED
@@ -24,9 +24,11 @@
24
24
  * field added later would read `undefined` on a cell built by an older copy.
25
25
  * Changing this interface therefore means bumping the `V1` in the global's
26
26
  * name, which costs the sharing between the two versions — the point of the
27
- * suffix is that losing it is a decision rather than a silent misread. Reset in
28
- * place (see `resetGpuDeviceForTests`); never reassign the cell itself, or
29
- * copies holding the old object diverge from the new one.
27
+ * suffix is that losing it is a decision rather than a silent misread. The one
28
+ * exception is an optional field whose `undefined` is an honest state, as
29
+ * `builtRenderer`'s is. Reset in place (see `resetGpuDeviceForTests`); never
30
+ * reassign the cell itself, or copies holding the old object diverge from the
31
+ * new one.
30
32
  */
31
33
  const cell = (globalThis.__jbrowseRenderCoreGpuDeviceV1 ??= {
32
34
  device: null,
@@ -216,6 +218,12 @@ export function setGpuOverride(value) {
216
218
  export function getGpuOverride() {
217
219
  return cell.gpuOverride;
218
220
  }
221
+ export function setBuiltRenderer(name) {
222
+ cell.builtRenderer = name;
223
+ }
224
+ export function getBuiltRenderer() {
225
+ return cell.builtRenderer;
226
+ }
219
227
  /**
220
228
  * Whether the GPU is off for the whole page — either pinned by `?renderer=` at
221
229
  * startup or switched off from a display's GPU-error banner after an
@@ -248,6 +256,7 @@ export function resetGpuDeviceForTests() {
248
256
  cell.devicePromise = null;
249
257
  cell.hadDevice = false;
250
258
  cell.deviceLostListeners.clear();
259
+ cell.builtRenderer = undefined;
251
260
  }
252
261
  export function getGpuDevice() {
253
262
  const override = getGpuOverride();
@@ -65,22 +65,25 @@ export declare function isSoftwareRenderer(glRenderer: string): boolean;
65
65
  */
66
66
  export declare function getGraphicsCapabilities(): Promise<GraphicsCapabilities>;
67
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.
68
+ * Which rung draws. Once `createGpuHal` has built one on this page, the rung it
69
+ * returned is the answer — the adapter probe says WebGPU where
70
+ * `WebGPUHal.create` then throws and the ladder lands on WebGL2. Before any
71
+ * backend exists it is the ladder read as an answer rather than run, from what
72
+ * the machine can do *and* the page-wide pin from `?renderer=` or the
73
+ * GPU-error banner's "disable GPU" button. This is the question every consumer
74
+ * asks: the About widget's "Graphics:" line, the stack-trace dialog's
75
+ * environment block, the analytics field.
74
76
  *
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.
77
+ * The Canvas2D pin is checked ahead of the record because the banner's
78
+ * "disable GPU" sets it before the displays rebuild on the new rung. The other
79
+ * pins win outright in the prediction, because **a pin never falls through to
80
+ * the next rung** — `createGpuHal` throws instead, on the reasoning that a
81
+ * pinned renderer which silently substitutes another makes any comparison
82
+ * against it wrong. So the pinned rung *is* the answer, and a pin that cannot
83
+ * be honored surfaces as a `renderError` rather than as a different string
84
+ * here. The one case the prediction cannot resolve from capabilities alone
85
+ * (`?renderer=webgl` on a WebGPU machine, where the WebGL2 probe was skipped)
86
+ * is also the case where the pin decides on its own.
84
87
  *
85
88
  * **`softwareWebgl` skips the WebGL2 rung here because it skips it there**, and
86
89
  * only where the ladder skips it — unpinned. Leaving it out is not a rounding
@@ -1,4 +1,4 @@
1
- import { getGpuOverride, isGpuRenderingDisabled } from './gpuDevice.js';
1
+ import { getBuiltRenderer, getGpuOverride, isGpuRenderingDisabled, } from './gpuDevice.js';
2
2
  // Substrings of UNMASKED_RENDERER_WEBGL that mean "no GPU is involved". Kept
3
3
  // conservative and specific — a wrong `true` here would describe a real GPU as
4
4
  // software, and the reports built from it are what a later ladder decision would
@@ -111,22 +111,25 @@ export function getGraphicsCapabilities() {
111
111
  return capabilities;
112
112
  }
113
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.
114
+ * Which rung draws. Once `createGpuHal` has built one on this page, the rung it
115
+ * returned is the answer — the adapter probe says WebGPU where
116
+ * `WebGPUHal.create` then throws and the ladder lands on WebGL2. Before any
117
+ * backend exists it is the ladder read as an answer rather than run, from what
118
+ * the machine can do *and* the page-wide pin from `?renderer=` or the
119
+ * GPU-error banner's "disable GPU" button. This is the question every consumer
120
+ * asks: the About widget's "Graphics:" line, the stack-trace dialog's
121
+ * environment block, the analytics field.
120
122
  *
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.
123
+ * The Canvas2D pin is checked ahead of the record because the banner's
124
+ * "disable GPU" sets it before the displays rebuild on the new rung. The other
125
+ * pins win outright in the prediction, because **a pin never falls through to
126
+ * the next rung** — `createGpuHal` throws instead, on the reasoning that a
127
+ * pinned renderer which silently substitutes another makes any comparison
128
+ * against it wrong. So the pinned rung *is* the answer, and a pin that cannot
129
+ * be honored surfaces as a `renderError` rather than as a different string
130
+ * here. The one case the prediction cannot resolve from capabilities alone
131
+ * (`?renderer=webgl` on a WebGPU machine, where the WebGL2 probe was skipped)
132
+ * is also the case where the pin decides on its own.
130
133
  *
131
134
  * **`softwareWebgl` skips the WebGL2 rung here because it skips it there**, and
132
135
  * only where the ladder skips it — unpinned. Leaving it out is not a rounding
@@ -145,6 +148,10 @@ export function effectiveRenderer(c) {
145
148
  if (isGpuRenderingDisabled()) {
146
149
  return 'Canvas2D';
147
150
  }
151
+ const built = getBuiltRenderer();
152
+ if (built) {
153
+ return built;
154
+ }
148
155
  const override = getGpuOverride();
149
156
  if (override === 'webgl') {
150
157
  return 'WebGL2';
@@ -1,7 +1,6 @@
1
1
  import type { GpuHal, PipelineDescriptor, SampleCount } from './types.ts';
2
2
  export interface GpuHalOptions {
3
3
  passes: PipelineDescriptor[];
4
- uniformByteSize: number;
5
4
  /**
6
5
  * Reaches the WebGPU rung only. The WebGL2 rung draws to the default
7
6
  * framebuffer with `antialias: true`, so its multisampling is the browser's
@@ -19,4 +18,4 @@ export interface GpuHalOptions {
19
18
  */
20
19
  failures?: unknown[];
21
20
  }
22
- export declare function createGpuHal(canvas: HTMLCanvasElement, { passes, uniformByteSize, sampleCount, failures }: GpuHalOptions): Promise<GpuHal | null>;
21
+ export declare function createGpuHal(canvas: HTMLCanvasElement, options: GpuHalOptions): Promise<GpuHal | null>;