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

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 (114) 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 +19 -16
  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 +137 -0
  51. package/esm/marks/markHit.d.ts +25 -0
  52. package/esm/marks/markHit.js +46 -0
  53. package/esm/marks/markPaint.d.ts +13 -0
  54. package/esm/marks/markPaint.js +18 -0
  55. package/esm/marks/pointMark.d.ts +24 -0
  56. package/esm/marks/pointMark.js +91 -0
  57. package/esm/marks/spanMark.d.ts +45 -0
  58. package/esm/marks/spanMark.js +59 -0
  59. package/esm/marks/types.d.ts +216 -0
  60. package/esm/marks/types.js +146 -0
  61. package/esm/perRegionRenderingBackend.d.ts +15 -3
  62. package/esm/perRegionRenderingBackend.js +26 -3
  63. package/esm/regionDataMap.d.ts +2 -2
  64. package/esm/regionDataMap.js +2 -2
  65. package/esm/renderBlock.d.ts +14 -0
  66. package/esm/renderBlock.js +25 -0
  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/pointMark.consts.generated.d.ts +4 -0
  78. package/esm/shaders/pointMark.consts.generated.js +6 -0
  79. package/esm/shaders/pointMark.generated.d.ts +5 -0
  80. package/esm/shaders/pointMark.generated.js +7 -0
  81. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  82. package/esm/shaders/pointMark.iface.generated.js +100 -0
  83. package/esm/shaders/pointMark.js.generated.d.ts +2 -0
  84. package/esm/shaders/pointMark.js.generated.js +19 -0
  85. package/esm/shaders/spanMark.generated.d.ts +4 -0
  86. package/esm/shaders/spanMark.generated.js +6 -0
  87. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  88. package/esm/shaders/spanMark.iface.generated.js +122 -0
  89. package/esm/sharedBackendKey.d.ts +18 -0
  90. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  91. package/esm/slangPass.d.ts +2 -0
  92. package/esm/slangPass.js +2 -0
  93. package/package.json +14 -4
  94. package/src/shaders/capsule.js.generated.ts +14 -0
  95. package/src/shaders/capsule.slang +12 -4
  96. package/src/shaders/colorPack.slang +5 -1
  97. package/src/shaders/coverageBand.slang +38 -15
  98. package/src/shaders/coverageBar.generated.ts +2 -2
  99. package/src/shaders/coverageBar.slang +9 -0
  100. package/src/shaders/coverageMod.generated.ts +2 -2
  101. package/src/shaders/coverageSnp.generated.ts +2 -2
  102. package/src/shaders/hpmath.slang +7 -1
  103. package/src/shaders/pointMark.consts.generated.ts +10 -0
  104. package/src/shaders/pointMark.generated.ts +12 -0
  105. package/src/shaders/pointMark.iface.generated.ts +146 -0
  106. package/src/shaders/pointMark.js.generated.ts +25 -0
  107. package/src/shaders/pointMark.slang +205 -0
  108. package/src/shaders/rowRect.slang +7 -8
  109. package/src/shaders/spanMark.generated.ts +10 -0
  110. package/src/shaders/spanMark.iface.generated.ts +168 -0
  111. package/src/shaders/spanMark.slang +22 -0
  112. package/esm/globalRenderingBackend.d.ts +0 -90
  113. package/esm/globalRenderingBackend.js +0 -53
  114. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -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) {
@@ -372,12 +375,12 @@ export class WebGPUHal extends GpuHalBase {
372
375
  createTexture(passId, binding, data, width, height) {
373
376
  const existing = this.passTextures.get(passId);
374
377
  if (existing) {
375
- // Same hazard as a buffer, one step further from a live caller: every
376
- // `uploadTexture` in tree is a colour ramp written from an upload autorun
377
- // (hic's `LinearHicDisplay/model.ts`, variants' `LDDisplay/shared.ts`),
378
- // which never runs inside a frame. Routing it through the same deferral
379
- // costs nothing and keeps "replaced mid-frame" from being a hazard again
380
- // the first time a renderer builds a texture from render state.
378
+ // Same hazard as a buffer, and no longer a hypothetical one: every
379
+ // `uploadTexture` in tree is a colour ramp, and since the ramp became a
380
+ // `defineMark` `texture` they are all written by `GpuMarkBackend`'s
381
+ // `bindRamp` from inside `drawRegion` — mid-frame, between an encoded
382
+ // draw and its submit. The deferral is what makes replacing one there
383
+ // safe.
381
384
  this.destroyWhenIdle(existing.texture);
382
385
  }
383
386
  const texture = this.device.createTexture({
@@ -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,6 +1,6 @@
1
1
  import { getType } from '@jbrowse/mobx-state-tree';
2
- import { computed } from 'mobx';
3
2
  import { contractReportsOn, reportContractViolation, } from './contractReports.js';
3
+ import { createEncodeMemo } from './encodeMemo.js';
4
4
  import { createMapUploadSync } from './mapUploadSync.js';
5
5
  import { isCheckedAtTheStore } from './regionDataMap.js';
6
6
  /**
@@ -47,49 +47,17 @@ export function installUpload(self, backend, { cells, inputs, encode, render })
47
47
  b.release(key);
48
48
  },
49
49
  });
50
- if (!encode) {
51
- const own = cells;
52
- return {
53
- upload: b => {
54
- const current = own();
55
- if (contractReportsOn()) {
56
- checkPayloads(self, current);
57
- }
58
- return sync(b, current);
59
- },
60
- render: b => render(b, own()),
61
- };
62
- }
63
- const encoded = new Map();
64
- const encodedFrom = new Map();
65
- const props = inputs && computed(inputs);
66
- let lastProps;
50
+ const own = encode
51
+ ? createEncodeMemo(cells, inputs, encode)
52
+ : cells;
67
53
  return {
68
54
  upload: b => {
69
- const current = cells();
70
55
  if (contractReportsOn()) {
71
- checkPayloads(self, current);
72
- }
73
- const p = props ? props.get() : undefined;
74
- if (p !== lastProps) {
75
- lastProps = p;
76
- encodedFrom.clear();
77
- }
78
- for (const [key, data] of current) {
79
- if (encodedFrom.get(key) !== data) {
80
- encoded.set(key, encode(data, p, key));
81
- encodedFrom.set(key, data);
82
- }
83
- }
84
- for (const key of encoded.keys()) {
85
- if (!current.has(key)) {
86
- encoded.delete(key);
87
- encodedFrom.delete(key);
88
- }
56
+ checkPayloads(self, cells());
89
57
  }
90
- return sync(b, encoded);
58
+ return sync(b, own());
91
59
  },
92
- render: b => render(b, encoded),
60
+ render: b => render(b, own()),
93
61
  };
94
62
  });
95
63
  }
@@ -14,9 +14,8 @@ export interface InstanceCacheOpts<TData> {
14
14
  colorOffsetWords: number;
15
15
  }
16
16
  /**
17
- * A per-key cache of packed instance bytes with a geometry/color split: a
18
- * recolor patches the one color lane in place instead of re-packing every
19
- * other one.
17
+ * A cache of packed instance bytes with a geometry/color split: a recolor
18
+ * patches the one color lane in place instead of re-packing every other one.
20
19
  *
21
20
  * The split is worth having because the two inputs move on completely
22
21
  * different schedules — geometry on an RPC refetch, colors on a `colorBy`
@@ -32,12 +31,16 @@ export interface InstanceCacheOpts<TData> {
32
31
  * problem. `installUpload` exists for the same reason one
33
32
  * layer up — an upload memo is not a hand-rolled `let`.
34
33
  *
34
+ * **The geometry token is the key**, so nothing has to evict: a payload whose
35
+ * coordinates were replaced takes its bytes with it, and a cache shared by
36
+ * several displays cannot hand one display another's buffer — two displays
37
+ * never hold one coordinate array. That is also what lets a mark's `pack`
38
+ * hold one of these, a mark list being module-level and shared.
39
+ *
35
40
  * The GPU re-upload still happens on a recolor (no HAL does partial buffer
36
41
  * updates); what is skipped is the dominant CPU interleave.
37
42
  */
38
43
  export declare function createInstanceCache<TData>(opts: InstanceCacheOpts<TData>): {
39
- /** Packed bytes for `key`, re-packing or re-coloring only as needed. */
40
- get(key: number, data: TData): ArrayBuffer;
41
- delete(key: number): void;
42
- clear(): void;
44
+ /** Packed bytes for `data`, re-packing or re-coloring only as needed. */
45
+ get(data: TData): ArrayBuffer;
43
46
  };
@@ -1,7 +1,6 @@
1
1
  /**
2
- * A per-key cache of packed instance bytes with a geometry/color split: a
3
- * recolor patches the one color lane in place instead of re-packing every
4
- * other one.
2
+ * A cache of packed instance bytes with a geometry/color split: a recolor
3
+ * patches the one color lane in place instead of re-packing every other one.
5
4
  *
6
5
  * The split is worth having because the two inputs move on completely
7
6
  * different schedules — geometry on an RPC refetch, colors on a `colorBy`
@@ -17,19 +16,25 @@
17
16
  * problem. `installUpload` exists for the same reason one
18
17
  * layer up — an upload memo is not a hand-rolled `let`.
19
18
  *
19
+ * **The geometry token is the key**, so nothing has to evict: a payload whose
20
+ * coordinates were replaced takes its bytes with it, and a cache shared by
21
+ * several displays cannot hand one display another's buffer — two displays
22
+ * never hold one coordinate array. That is also what lets a mark's `pack`
23
+ * hold one of these, a mark list being module-level and shared.
24
+ *
20
25
  * The GPU re-upload still happens on a recolor (no HAL does partial buffer
21
26
  * updates); what is skipped is the dominant CPU interleave.
22
27
  */
23
28
  export function createInstanceCache(opts) {
24
29
  const { geomToken, colors, interleave, strideWords, colorOffsetWords } = opts;
25
- const cache = new Map();
30
+ const cache = new WeakMap();
26
31
  return {
27
- /** Packed bytes for `key`, re-packing or re-coloring only as needed. */
28
- get(key, data) {
32
+ /** Packed bytes for `data`, re-packing or re-coloring only as needed. */
33
+ get(data) {
29
34
  const geom = geomToken(data);
30
35
  const cols = colors(data);
31
- const hit = cache.get(key);
32
- if (hit?.geom === geom) {
36
+ const hit = cache.get(geom);
37
+ if (hit) {
33
38
  if (hit.colors !== cols) {
34
39
  const u32 = new Uint32Array(hit.buf);
35
40
  for (let i = 0, n = cols.length; i < n; i++) {
@@ -40,14 +45,8 @@ export function createInstanceCache(opts) {
40
45
  return hit.buf;
41
46
  }
42
47
  const buf = interleave(data);
43
- cache.set(key, { geom, colors: cols, buf });
48
+ cache.set(geom, { colors: cols, buf });
44
49
  return buf;
45
50
  },
46
- delete(key) {
47
- cache.delete(key);
48
- },
49
- clear() {
50
- cache.clear();
51
- },
52
51
  };
53
52
  }
@@ -1,14 +1,6 @@
1
1
  export interface MapUploadTarget<K, T, B> {
2
2
  upload: (backend: B, key: K, data: T) => void;
3
- /**
4
- * How a key that left the map is released. `remove` deletes it by itself;
5
- * `prune` is handed the keys still present and releases everything else.
6
- * `installUpload` takes the first, which is the only correct shape on a
7
- * canvas several displays share and does the same job on a display's own
8
- * map.
9
- */
10
3
  remove?: (backend: B, key: K) => void;
11
- prune?: (backend: B, active: ReadonlySet<K>) => void;
12
4
  }
13
5
  /**
14
6
  * The one reference diff every keyed GPU upload in the tree runs on. Uploads
@@ -39,7 +39,6 @@ export function createMapUploadSync(target) {
39
39
  changed = true;
40
40
  }
41
41
  }
42
- target.prune?.(backend, new Set(entries.keys()));
43
42
  return changed;
44
43
  };
45
44
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Packed ABGR → `rgba(...)`, and the run-tracking fill a painter wraps it in.
3
+ *
4
+ * `@jbrowse/core/util/colorBits` re-exports `abgrToCssRgba` from here: core
5
+ * depends on render-core, never the reverse. The direction that genuinely
6
+ * needs core is resolving a CSS colour to a packed number, which is why a
7
+ * shape takes packed colours and the display resolves them.
8
+ */
9
+ export declare function abgrToCssRgba(c: number): string;
10
+ /**
11
+ * A fill-style setter that only touches `ctx.fillStyle` when the colour
12
+ * changes. Most of a painting is runs of one colour, and both the string
13
+ * allocation and the context write cost more than the comparison.
14
+ */
15
+ export declare function makeAbgrFill(ctx: {
16
+ fillStyle: string | CanvasGradient | CanvasPattern;
17
+ }): (abgr: number) => void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Packed ABGR → `rgba(...)`, and the run-tracking fill a painter wraps it in.
3
+ *
4
+ * `@jbrowse/core/util/colorBits` re-exports `abgrToCssRgba` from here: core
5
+ * depends on render-core, never the reverse. The direction that genuinely
6
+ * needs core is resolving a CSS colour to a packed number, which is why a
7
+ * shape takes packed colours and the display resolves them.
8
+ */
9
+ export function abgrToCssRgba(c) {
10
+ const a = ((c >>> 24) & 255) / 255;
11
+ return `rgba(${c & 255},${(c >>> 8) & 255},${(c >>> 16) & 255},${a})`;
12
+ }
13
+ /**
14
+ * A fill-style setter that only touches `ctx.fillStyle` when the colour
15
+ * changes. Most of a painting is runs of one colour, and both the string
16
+ * allocation and the context write cost more than the comparison.
17
+ */
18
+ export function makeAbgrFill(ctx) {
19
+ let last;
20
+ return (abgr) => {
21
+ if (abgr !== last) {
22
+ last = abgr;
23
+ ctx.fillStyle = abgrToCssRgba(abgr);
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,47 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
2
+ import type { MarkContext2D, MarkFrame, MarkShape } from './types.ts';
3
+ export interface RecordedRect {
4
+ x: number;
5
+ y: number;
6
+ w: number;
7
+ h: number;
8
+ fillStyle: string;
9
+ }
10
+ /**
11
+ * A `MarkContext2D` that records every rect a painter fills, in paint order —
12
+ * `fillRect` and path `rect` alike, so a shape that batches by colour records
13
+ * the same list as one that fills per instance. A path traced with
14
+ * `moveTo`/`lineTo` and filled records its bounding box, which is what a
15
+ * shape whose hit test answers a polygon's box (the variant inversion
16
+ * triangle) is held to.
17
+ *
18
+ * A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
19
+ * edge's bounding box widened by half the current `lineWidth` — the extent a
20
+ * round cap reaches on all four sides. Per edge rather than per `stroke()`,
21
+ * because a painter that batches a colour run into one path (dotplot's) would
22
+ * otherwise record the run's hull as a single instance. `fillStyle` on such a
23
+ * record is the `strokeStyle` it went down in; the field is the colour of the
24
+ * ink either way.
25
+ */
26
+ export declare function recordingContext(): {
27
+ ctx: MarkContext2D;
28
+ calls: RecordedRect[];
29
+ };
30
+ /**
31
+ * The draw-against-hit gate for a shape whose instances paint as rects: paint
32
+ * the block, then walk it in `step` px and hold `hitNearest` to what the
33
+ * painter recorded. A hit at distance 0 has to sit on the rect of the index it
34
+ * names, and a point on a rect has to answer that rect — the last-painted one
35
+ * where rects overlap, since the sweep hands candidates in back to front.
36
+ *
37
+ * Returns the violations rather than asserting, so a test reads as
38
+ * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
39
+ * shape whose hit target is wider than its ink (`point`) needs a bound that
40
+ * admits the whole rect.
41
+ */
42
+ export declare function sweepDrawAgainstHit<C extends {
43
+ count: number;
44
+ }, P>(shape: MarkShape<C, P>, channels: C, block: RenderBlock, frame: MarkFrame, params: P, { maxDistSq, step }: {
45
+ maxDistSq: number;
46
+ step?: number;
47
+ }): string[];