@jbrowse/render-core 5.0.0-beta.1 → 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 (120) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +25 -6
  2. package/esm/RenderLifecycleMixin.js +30 -7
  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/contractReports.d.ts +54 -0
  10. package/esm/contractReports.js +112 -0
  11. package/esm/coverageBand.d.ts +0 -1
  12. package/esm/coverageBand.js +0 -1
  13. package/esm/coverageBandBuffers.d.ts +0 -6
  14. package/esm/coverageBandBuffers.js +1 -13
  15. package/esm/createRenderingBackend.d.ts +14 -18
  16. package/esm/createRenderingBackend.js +4 -2
  17. package/esm/displayPhase.d.ts +10 -1
  18. package/esm/displayPhase.js +2 -1
  19. package/esm/encodeMemo.d.ts +14 -0
  20. package/esm/encodeMemo.js +48 -0
  21. package/esm/gpuDevice.d.ts +9 -0
  22. package/esm/gpuDevice.js +12 -3
  23. package/esm/graphicsCapabilities.d.ts +18 -15
  24. package/esm/graphicsCapabilities.js +23 -16
  25. package/esm/hal/createHal.d.ts +1 -2
  26. package/esm/hal/createHal.js +14 -4
  27. package/esm/hal/deviceGpuCache.js +2 -2
  28. package/esm/hal/gpuHalBase.d.ts +1 -1
  29. package/esm/hal/gpuHalBase.js +2 -3
  30. package/esm/hal/mockHal.d.ts +0 -1
  31. package/esm/hal/mockHal.js +0 -10
  32. package/esm/hal/regionRegistry.d.ts +0 -1
  33. package/esm/hal/regionRegistry.js +3 -15
  34. package/esm/hal/types.d.ts +5 -1
  35. package/esm/hal/webgl2Hal.d.ts +1 -1
  36. package/esm/hal/webgl2Hal.js +2 -2
  37. package/esm/hal/webgpuHal.d.ts +2 -2
  38. package/esm/hal/webgpuHal.js +13 -10
  39. package/esm/installUpload.d.ts +4 -1
  40. package/esm/installUpload.js +10 -41
  41. package/esm/instanceCache.d.ts +10 -7
  42. package/esm/instanceCache.js +14 -15
  43. package/esm/mapUploadSync.d.ts +0 -8
  44. package/esm/mapUploadSync.js +0 -1
  45. package/esm/marks/colorFill.d.ts +17 -0
  46. package/esm/marks/colorFill.js +26 -0
  47. package/esm/marks/drawAgainstHit.d.ts +47 -0
  48. package/esm/marks/drawAgainstHit.js +140 -0
  49. package/esm/marks/glyphPaint.d.ts +16 -0
  50. package/esm/marks/glyphPaint.js +41 -0
  51. package/esm/marks/index.d.ts +9 -0
  52. package/esm/marks/index.js +6 -0
  53. package/esm/marks/markBackend.d.ts +87 -0
  54. package/esm/marks/markBackend.js +133 -0
  55. package/esm/marks/markPaint.d.ts +13 -0
  56. package/esm/marks/markPaint.js +18 -0
  57. package/esm/marks/pointMark.d.ts +24 -0
  58. package/esm/marks/pointMark.js +98 -0
  59. package/esm/marks/spanMark.d.ts +45 -0
  60. package/esm/marks/spanMark.js +71 -0
  61. package/esm/marks/types.d.ts +206 -0
  62. package/esm/marks/types.js +125 -0
  63. package/esm/perRegionRenderingBackend.d.ts +15 -3
  64. package/esm/perRegionRenderingBackend.js +26 -3
  65. package/esm/regionDataMap.d.ts +2 -2
  66. package/esm/regionDataMap.js +25 -23
  67. package/esm/renderingBackendBase.d.ts +11 -14
  68. package/esm/renderingBackendBase.js +7 -7
  69. package/esm/shaders/capsule.js.generated.d.ts +1 -0
  70. package/esm/shaders/capsule.js.generated.js +11 -0
  71. package/esm/shaders/coverageBar.generated.d.ts +2 -2
  72. package/esm/shaders/coverageBar.generated.js +2 -2
  73. package/esm/shaders/coverageMod.generated.d.ts +2 -2
  74. package/esm/shaders/coverageMod.generated.js +2 -2
  75. package/esm/shaders/coverageSnp.generated.d.ts +2 -2
  76. package/esm/shaders/coverageSnp.generated.js +2 -2
  77. package/esm/shaders/hpmath.js.generated.d.ts +1 -0
  78. package/esm/shaders/hpmath.js.generated.js +4 -1
  79. package/esm/shaders/pointMark.consts.generated.d.ts +4 -0
  80. package/esm/shaders/pointMark.consts.generated.js +6 -0
  81. package/esm/shaders/pointMark.generated.d.ts +5 -0
  82. package/esm/shaders/pointMark.generated.js +7 -0
  83. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  84. package/esm/shaders/pointMark.iface.generated.js +100 -0
  85. package/esm/shaders/pointMark.js.generated.d.ts +1 -0
  86. package/esm/shaders/pointMark.js.generated.js +16 -0
  87. package/esm/shaders/rowRect.generated.d.ts +0 -1
  88. package/esm/shaders/rowRect.generated.js +0 -1
  89. package/esm/shaders/spanMark.generated.d.ts +4 -0
  90. package/esm/shaders/spanMark.generated.js +6 -0
  91. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  92. package/esm/shaders/spanMark.iface.generated.js +122 -0
  93. package/esm/sharedBackendKey.d.ts +18 -0
  94. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  95. package/esm/slangPass.d.ts +2 -0
  96. package/esm/slangPass.js +2 -0
  97. package/package.json +14 -4
  98. package/src/shaders/capsule.js.generated.ts +14 -0
  99. package/src/shaders/capsule.slang +12 -4
  100. package/src/shaders/colorPack.slang +5 -1
  101. package/src/shaders/coverageBand.slang +38 -15
  102. package/src/shaders/coverageBar.generated.ts +2 -2
  103. package/src/shaders/coverageBar.slang +9 -0
  104. package/src/shaders/coverageMod.generated.ts +2 -2
  105. package/src/shaders/coverageSnp.generated.ts +2 -2
  106. package/src/shaders/hpmath.js.generated.ts +5 -1
  107. package/src/shaders/hpmath.slang +29 -10
  108. package/src/shaders/pointMark.consts.generated.ts +10 -0
  109. package/src/shaders/pointMark.generated.ts +12 -0
  110. package/src/shaders/pointMark.iface.generated.ts +146 -0
  111. package/src/shaders/pointMark.js.generated.ts +21 -0
  112. package/src/shaders/pointMark.slang +192 -0
  113. package/src/shaders/rowRect.generated.ts +0 -2
  114. package/src/shaders/rowRect.slang +33 -44
  115. package/src/shaders/spanMark.generated.ts +10 -0
  116. package/src/shaders/spanMark.iface.generated.ts +168 -0
  117. package/src/shaders/spanMark.slang +22 -0
  118. package/esm/globalRenderingBackend.d.ts +0 -90
  119. package/esm/globalRenderingBackend.js +0 -53
  120. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -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>;
@@ -1,4 +1,4 @@
1
- import { getGpuOverride, isGpuRenderingDisabled } from '../gpuDevice.js';
1
+ import { getGpuOverride, isGpuRenderingDisabled, setBuiltRenderer, } from '../gpuDevice.js';
2
2
  import { getGraphicsCapabilities } from '../graphicsCapabilities.js';
3
3
  import { WebGL2Hal } from './webgl2Hal.js';
4
4
  import { WebGPUHal } from './webgpuHal.js';
@@ -17,14 +17,24 @@ function warnSoftwareRasterizerOnce(glRenderer) {
17
17
  // Ladder: WebGPU → WebGL2 → Canvas2D (null). The `?renderer=` URL param pins it
18
18
  // to a single rung for debugging — `webgpu`, `webgl`, or `canvas2d`/`canvas` —
19
19
  // and a pin never falls through to the next rung. See GPU_OVERRIDES.
20
- export async function createGpuHal(canvas, { passes, uniformByteSize, sampleCount, failures }) {
20
+ //
21
+ // Records the rung it returned on the page-wide cell, which is how
22
+ // `effectiveRenderer` gets to report what drew rather than what the adapter
23
+ // probe predicted — the two part when `WebGPUHal.create` throws and the ladder
24
+ // lands on WebGL2.
25
+ export async function createGpuHal(canvas, options) {
26
+ const hal = await climbLadder(canvas, options);
27
+ setBuiltRenderer(hal === null ? 'Canvas2D' : hal instanceof WebGPUHal ? 'WebGPU' : 'WebGL2');
28
+ return hal;
29
+ }
30
+ async function climbLadder(canvas, { passes, sampleCount, failures }) {
21
31
  if (isGpuRenderingDisabled()) {
22
32
  return null;
23
33
  }
24
34
  const override = getGpuOverride();
25
35
  if (override !== 'webgl') {
26
36
  try {
27
- const webgpu = await WebGPUHal.create(canvas, passes, uniformByteSize, sampleCount);
37
+ const webgpu = await WebGPUHal.create(canvas, passes, sampleCount);
28
38
  if (webgpu) {
29
39
  return webgpu;
30
40
  }
@@ -81,7 +91,7 @@ export async function createGpuHal(canvas, { passes, uniformByteSize, sampleCoun
81
91
  }
82
92
  }
83
93
  try {
84
- return new WebGL2Hal(canvas, passes, uniformByteSize);
94
+ return new WebGL2Hal(canvas, passes);
85
95
  }
86
96
  catch (e) {
87
97
  console.warn('[GPU] WebGL2 unavailable, falling back to Canvas2D:', e);
@@ -21,8 +21,8 @@
21
21
  * is the one pipeline input the descriptor cannot express, so it is the inner
22
22
  * key; see the `pipelines` field.
23
23
  *
24
- * The one field of a pipeline that is not on the descriptor is the uniform
25
- * size, and it cannot make two HALs disagree here:
24
+ * The descriptor's `uniformByteSize` never reaches the pipeline, so it cannot
25
+ * make two HALs disagree here:
26
26
  * `createUniformOnlyBindGroupLayout` declares no `minBindingSize`, so the
27
27
  * layout — and thus the pipeline built against it — is the same whatever a
28
28
  * display's `uniformByteSize` is. The size reaches the GPU through the bind
@@ -31,6 +31,7 @@ export declare abstract class GpuHalBase<Buf extends {
31
31
  protected descriptors: Map<string, PipelineDescriptor>;
32
32
  protected regions: RegionRegistry<Buf>;
33
33
  protected oom: OomReporter;
34
+ readonly uniformByteSize: number;
34
35
  protected disposed: boolean;
35
36
  constructor(descriptors: PipelineDescriptor[], halName: string);
36
37
  protected abstract limits(): HalLimits;
@@ -49,7 +50,6 @@ export declare abstract class GpuHalBase<Buf extends {
49
50
  getBufferCount(regionKey: number, passId: string): number;
50
51
  deleteBuffer(regionKey: number, passId: string): void;
51
52
  deleteRegion(regionKey: number): void;
52
- pruneRegions(active: Iterable<number>): void;
53
53
  uploadTexture(passId: string, data: Uint8Array, width: number, height: number): void;
54
54
  dispose(): void;
55
55
  }
@@ -19,11 +19,13 @@ export class GpuHalBase {
19
19
  descriptors;
20
20
  regions;
21
21
  oom;
22
+ uniformByteSize;
22
23
  // Guards dispose() against double invocation (pagehide + React cleanup can
23
24
  // both fire).
24
25
  disposed = false;
25
26
  constructor(descriptors, halName) {
26
27
  this.descriptors = new Map(descriptors.map(d => [d.id, d]));
28
+ this.uniformByteSize = Math.max(0, ...descriptors.map(d => d.uniformByteSize));
27
29
  this.oom = new OomReporter(halName);
28
30
  this.regions = new RegionRegistry(buf => {
29
31
  this.destroyBuffer(buf);
@@ -55,9 +57,6 @@ export class GpuHalBase {
55
57
  deleteRegion(regionKey) {
56
58
  this.regions.deleteRegion(regionKey);
57
59
  }
58
- pruneRegions(active) {
59
- this.regions.prune(active);
60
- }
61
60
  uploadTexture(passId, data, width, height) {
62
61
  // Read the binding off the descriptor, so a pass with no texture is
63
62
  // answered without touching the backend's pipeline state at all.
@@ -102,7 +102,6 @@ export declare class MockHal extends GpuHalBase<MockBuffer> implements GpuHal {
102
102
  uploadBuffer(regionKey: number, passId: string, data: ArrayBuffer | ArrayBufferView, count: number): void;
103
103
  deleteBuffer(regionKey: number, passId: string): void;
104
104
  deleteRegion(regionKey: number): void;
105
- pruneRegions(active: Iterable<number>): void;
106
105
  private noteReplaced;
107
106
  private noteRegionReplaced;
108
107
  writeUniforms(data: ArrayBuffer): void;
@@ -119,16 +119,6 @@ export class MockHal extends GpuHalBase {
119
119
  this.noteRegionReplaced(regionKey);
120
120
  super.deleteRegion(regionKey);
121
121
  }
122
- pruneRegions(active) {
123
- const activeSet = new Set(active);
124
- this.record('pruneRegions', [...activeSet]);
125
- for (const regionKey of this.drawnThisFrame.keys()) {
126
- if (!activeSet.has(regionKey)) {
127
- this.noteRegionReplaced(regionKey);
128
- }
129
- }
130
- super.pruneRegions(activeSet);
131
- }
132
122
  noteReplaced(regionKey, passId) {
133
123
  if (this.frameOpen && this.drawnThisFrame.get(regionKey)?.has(passId)) {
134
124
  this.replacedWhileDrawnLog.push(`${regionKey}:${passId}`);
@@ -7,5 +7,4 @@ export declare class RegionRegistry<Buf> {
7
7
  deleteBuffer(regionKey: number, passId: string): void;
8
8
  deleteRegion(regionKey: number): void;
9
9
  deleteAll(): void;
10
- prune(active: Iterable<number>): void;
11
10
  }
@@ -1,9 +1,6 @@
1
- // Per-(region, pass) buffer registry shared by WebGL2Hal and WebGPUHal.
2
- // Owns the nested map and the destroy semantics so the two HALs only have to
3
- // supply a leaf-buffer type and its destroy hook. Both HALs used to ship
4
- // near-identical deleteBuffer/deleteRegion/deleteAll/prune/getOrCreate
5
- // implementations; centralizing avoids drift when one side's lifecycle is
6
- // tweaked and the other isn't. Contract pinned by regionRegistry.test.ts.
1
+ // Per-(region, pass) buffer registry shared by WebGL2Hal and WebGPUHal: the
2
+ // nested map plus destroy semantics, so each HAL supplies only its leaf-buffer
3
+ // type and destroy hook.
7
4
  export class RegionRegistry {
8
5
  destroy;
9
6
  regions = new Map();
@@ -54,13 +51,4 @@ export class RegionRegistry {
54
51
  }
55
52
  this.regions.clear();
56
53
  }
57
- // Delete every region whose key is NOT in `active`.
58
- prune(active) {
59
- const activeSet = new Set(active);
60
- for (const regionKey of this.regions.keys()) {
61
- if (!activeSet.has(regionKey)) {
62
- this.deleteRegion(regionKey);
63
- }
64
- }
65
- }
66
54
  }
@@ -80,15 +80,20 @@ export interface PipelineDescriptor {
80
80
  glslVertex: string;
81
81
  glslFragment: string;
82
82
  instanceStride: number;
83
+ /** Bytes of the uniform block the shader binds; the HAL's slot is the largest over its passes. */
84
+ uniformByteSize: number;
83
85
  verticesPerInstance: number;
84
86
  blend: boolean;
85
87
  blendState?: BlendState;
86
88
  vertexAttributes: readonly VertexAttributeLayout[];
87
89
  topology?: 'triangle-list' | 'triangle-strip' | 'line-list';
90
+ coverage?: 'analytic';
88
91
  textures?: readonly [TextureBinding, ...TextureBinding[]];
89
92
  bindings?: readonly ShaderBinding[];
90
93
  }
91
94
  export interface GpuHal {
95
+ /** The uniform slot size, the largest block any registered pass binds. */
96
+ readonly uniformByteSize: number;
92
97
  /**
93
98
  * Size the canvas, and report the scale its backing store actually got.
94
99
  * Callers derive every device-px rect from THAT rather than from `getDpr()`:
@@ -101,7 +106,6 @@ export interface GpuHal {
101
106
  getBufferCount(regionKey: number, passId: string): number;
102
107
  deleteBuffer(regionKey: number, passId: string): void;
103
108
  deleteRegion(regionKey: number): void;
104
- pruneRegions(active: Iterable<number>): void;
105
109
  uploadTexture(passId: string, data: Uint8Array, width: number, height: number): void;
106
110
  writeUniforms(data: ArrayBuffer): void;
107
111
  beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
@@ -17,7 +17,7 @@ export declare class WebGL2Hal extends GpuHalBase<RegionPassBuffer> implements G
17
17
  private contextLostListener;
18
18
  private contextRestoredListener;
19
19
  private checkGlError;
20
- constructor(canvas: HTMLCanvasElement, descriptors: PipelineDescriptor[], uniformByteSize: number);
20
+ constructor(canvas: HTMLCanvasElement, descriptors: PipelineDescriptor[]);
21
21
  protected limits(): {
22
22
  maxBufferBytes: number;
23
23
  maxTextureDimensionPx: number;
@@ -156,7 +156,7 @@ export class WebGL2Hal extends GpuHalBase {
156
156
  console.error(`[WebGL2Hal] GL error at "${label}": ${glErrorName(this.gl, err)}`);
157
157
  }
158
158
  }
159
- constructor(canvas, descriptors, uniformByteSize) {
159
+ constructor(canvas, descriptors) {
160
160
  super(descriptors, 'WebGL2Hal');
161
161
  this.canvas = canvas;
162
162
  this.debug = debugEnabled();
@@ -202,7 +202,7 @@ export class WebGL2Hal extends GpuHalBase {
202
202
  this.maxTextureDim = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE));
203
203
  this.ubo = gl.createBuffer();
204
204
  gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo);
205
- gl.bufferData(gl.UNIFORM_BUFFER, uniformByteSize, gl.DYNAMIC_DRAW);
205
+ gl.bufferData(gl.UNIFORM_BUFFER, this.uniformByteSize, gl.DYNAMIC_DRAW);
206
206
  this.passes = new Map();
207
207
  // Programs are built on first use (see `getPass`), not here: a renderer
208
208
  // declares every pass it could ever draw — alignments alone declares 21,
@@ -66,7 +66,7 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
66
66
  * configured context behind with nothing to release it.
67
67
  */
68
68
  private configureContext;
69
- static create(canvas: HTMLCanvasElement, descriptors: PipelineDescriptor[], uniformByteSize: number, sampleCount: SampleCount): Promise<WebGPUHal | null>;
69
+ static create(canvas: HTMLCanvasElement, descriptors: PipelineDescriptor[], sampleCount: SampleCount): Promise<WebGPUHal | null>;
70
70
  resize(width: number, height: number): import("./types.ts").CanvasScale;
71
71
  private recreateMsaaTexture;
72
72
  /**
@@ -92,7 +92,7 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
92
92
  * {@link pendingDestroy} for why the frame decides when.
93
93
  *
94
94
  * Every buffer release routes here through the `RegionRegistry` destroy hook,
95
- * so `uploadBuffer`, `deleteBuffer`, `deleteRegion`, `pruneRegions` and
95
+ * so `uploadBuffer`, `deleteBuffer`, `deleteRegion` and
96
96
  * `dispose` are all covered without any of them knowing a frame is open.
97
97
  */
98
98
  private destroyWhenIdle;
@@ -15,11 +15,13 @@ class ShaderCompileError extends Error {
15
15
  // aligned slot in the uniform ring buffer.
16
16
  //
17
17
  // The ring is allocated eagerly at this many slots, as a GPU buffer AND a CPU
18
- // staging array, whether a renderer writes 4 slots or 1900 — so the cost is
19
- // per display and is the aligned uniform size that decides it, not the slot
20
- // count. A display whose uniform fits the 256-byte minimum alignment pays
21
- // 512 KB each side; alignments' 864-byte uniform aligns to 1024, so 2 MiB each
22
- // side. Trivial once, per-track at ten open alignments tracks.
18
+ // staging array, whether a renderer writes 4 slots or 1900 — so the cost is per
19
+ // display, and both terms are in it. A display whose uniform fits the 256-byte
20
+ // minimum alignment pays 512 KB each side; alignments' 640-byte uniform aligns
21
+ // to 768, so 1.5 MiB each side, per track. A frame is measured using 2-4 of
22
+ // those slots, which is what makes the count above the oversized term rather
23
+ // than any struct:
24
+ // agent-docs/ideas/size-the-uniform-ring-to-its-measured-occupancy.md.
23
25
  //
24
26
  // Exhausting it does not throw: the write is dropped and its draws render
25
27
  // against the previous batch's uniforms, which is wrong data rather than stale
@@ -196,7 +198,7 @@ export class WebGPUHal extends GpuHalBase {
196
198
  // than once per frame, and a reconfigure that failed is not attempted again.
197
199
  warnedSwapChainLoss = false;
198
200
  swapChainUnrecoverable = false;
199
- constructor(device, canvas, context, descriptors, uniformByteSize, sampleCount, pipelines, layouts) {
201
+ constructor(device, canvas, context, descriptors, sampleCount, pipelines, layouts) {
200
202
  super(descriptors, 'WebGPUHal');
201
203
  this.device = device;
202
204
  this.canvas = canvas;
@@ -206,7 +208,8 @@ export class WebGPUHal extends GpuHalBase {
206
208
  this.sampleCount = sampleCount;
207
209
  // Align uniform slots to device requirements for dynamic offsets
208
210
  const alignment = device.limits.minUniformBufferOffsetAlignment;
209
- this.alignedUniformSize = Math.ceil(uniformByteSize / alignment) * alignment;
211
+ this.alignedUniformSize =
212
+ Math.ceil(this.uniformByteSize / alignment) * alignment;
210
213
  const ringSize = MAX_UNIFORM_SLOTS * this.alignedUniformSize;
211
214
  this.uniformRingBuffer = device.createBuffer({
212
215
  size: ringSize,
@@ -245,7 +248,7 @@ export class WebGPUHal extends GpuHalBase {
245
248
  });
246
249
  noteCanvasConfigured(this.canvas, this);
247
250
  }
248
- static async create(canvas, descriptors, uniformByteSize, sampleCount) {
251
+ static async create(canvas, descriptors, sampleCount) {
249
252
  const device = await getGpuDevice();
250
253
  if (!device) {
251
254
  return null;
@@ -266,7 +269,7 @@ export class WebGPUHal extends GpuHalBase {
266
269
  throw canvasContextError(canvas, 'webgpu');
267
270
  }
268
271
  noteCanvasContext(canvas, 'webgpu');
269
- return new WebGPUHal(device, canvas, context, descriptors, uniformByteSize, sampleCount, pipelines, layouts);
272
+ return new WebGPUHal(device, canvas, context, descriptors, sampleCount, pipelines, layouts);
270
273
  }
271
274
  resize(width, height) {
272
275
  const { changed, scale } = syncCanvasSize(this.canvas, width, height);
@@ -352,7 +355,7 @@ export class WebGPUHal extends GpuHalBase {
352
355
  * {@link pendingDestroy} for why the frame decides when.
353
356
  *
354
357
  * Every buffer release routes here through the `RegionRegistry` destroy hook,
355
- * so `uploadBuffer`, `deleteBuffer`, `deleteRegion`, `pruneRegions` and
358
+ * so `uploadBuffer`, `deleteBuffer`, `deleteRegion` and
356
359
  * `dispose` are all covered without any of them knowing a frame is open.
357
360
  */
358
361
  destroyWhenIdle(resource) {
@@ -74,7 +74,10 @@ export declare function oneCell<K, T>(key: K, value: T | undefined | null): Read
74
74
  *
75
75
  * **Calling again only swaps the backend.** Displays wire this from
76
76
  * `startRenderingBackend`, which fires again on every context-loss recovery;
77
- * the diff and the encode cache live in the setup thunk and survive it.
77
+ * the diff and the encode memo live in the setup thunk and survive it.
78
+ *
79
+ * `encode` runs through `createEncodeMemo`. A display that needs the encoded
80
+ * map itself holds that memo in a `.views` closure and hands identity cells.
78
81
  *
79
82
  * @see installUpload.test.ts for the upload, encode and release counts.
80
83
  * @see ADR-078 for why this is one autorun and a diff.
@@ -1,5 +1,6 @@
1
1
  import { getType } from '@jbrowse/mobx-state-tree';
2
- import { computed } from 'mobx';
2
+ import { contractReportsOn, reportContractViolation, } from './contractReports.js';
3
+ import { createEncodeMemo } from './encodeMemo.js';
3
4
  import { createMapUploadSync } from './mapUploadSync.js';
4
5
  import { isCheckedAtTheStore } from './regionDataMap.js';
5
6
  /**
@@ -29,7 +30,7 @@ function checkPayloads(self, cells) {
29
30
  }
30
31
  if (!named.has(key)) {
31
32
  named.add(key);
32
- console.error(`[jbrowse display contract] ${getType(self).name}: cell ${String(key)} ` +
33
+ reportContractViolation('display', `${getType(self).name}: cell ${String(key)} ` +
33
34
  `handed to installUpload is \`${String(value)}\`. A fetch answers ` +
34
35
  'a payload or nothing; leave the key out until it has one.');
35
36
  }
@@ -46,49 +47,17 @@ export function installUpload(self, backend, { cells, inputs, encode, render })
46
47
  b.release(key);
47
48
  },
48
49
  });
49
- if (!encode) {
50
- const own = cells;
51
- return {
52
- upload: b => {
53
- const current = own();
54
- if (process.env.NODE_ENV !== 'production') {
55
- checkPayloads(self, current);
56
- }
57
- return sync(b, current);
58
- },
59
- render: b => render(b, own()),
60
- };
61
- }
62
- const encoded = new Map();
63
- const encodedFrom = new Map();
64
- const props = inputs && computed(inputs);
65
- let lastProps;
50
+ const own = encode
51
+ ? createEncodeMemo(cells, inputs, encode)
52
+ : cells;
66
53
  return {
67
54
  upload: b => {
68
- const current = cells();
69
- if (process.env.NODE_ENV !== 'production') {
70
- checkPayloads(self, current);
71
- }
72
- const p = props ? props.get() : undefined;
73
- if (p !== lastProps) {
74
- lastProps = p;
75
- encodedFrom.clear();
76
- }
77
- for (const [key, data] of current) {
78
- if (encodedFrom.get(key) !== data) {
79
- encoded.set(key, encode(data, p, key));
80
- encodedFrom.set(key, data);
81
- }
82
- }
83
- for (const key of encoded.keys()) {
84
- if (!current.has(key)) {
85
- encoded.delete(key);
86
- encodedFrom.delete(key);
87
- }
55
+ if (contractReportsOn()) {
56
+ checkPayloads(self, cells());
88
57
  }
89
- return sync(b, encoded);
58
+ return sync(b, own());
90
59
  },
91
- render: b => render(b, encoded),
60
+ render: b => render(b, own()),
92
61
  };
93
62
  });
94
63
  }