@jbrowse/render-core 5.0.0-beta.10 → 5.0.0-beta.11

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.
@@ -25,7 +25,7 @@ export interface RenderCanvasHandle {
25
25
  * render-prop body on the same value for the displays on the chrome, which it
26
26
  * does *not* get for free from the `renderError` unmount (that covers only a
27
27
  * reported loss — see the `canvasKey` note in useRenderingBackend.ts).
28
- * GPU_RENDERING.md said so and ended with "any new consumer rendering its own
28
+ * GPU_DISPLAY_LIFECYCLE.md said so and ended with "any new consumer rendering its own
29
29
  * banner must too" — a rule enforced by remembering to read the doc. Rendering
30
30
  * the element here makes it structural instead: there is no way to mount this
31
31
  * canvas without the key.
@@ -16,7 +16,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
16
16
  * render-prop body on the same value for the displays on the chrome, which it
17
17
  * does *not* get for free from the `renderError` unmount (that covers only a
18
18
  * reported loss — see the `canvasKey` note in useRenderingBackend.ts).
19
- * GPU_RENDERING.md said so and ended with "any new consumer rendering its own
19
+ * GPU_DISPLAY_LIFECYCLE.md said so and ended with "any new consumer rendering its own
20
20
  * banner must too" — a rule enforced by remembering to read the doc. Rendering
21
21
  * the element here makes it structural instead: there is no way to mount this
22
22
  * canvas without the key.
@@ -139,7 +139,7 @@ export function prepareCanvas(canvas, ctx, canvasWidth, canvasHeight) {
139
139
  // `getContext('2d')` + null-guard + `prepareCanvas` ritual that every
140
140
  // standalone 2D overlay otherwise hand-rolls in its draw effect — and the
141
141
  // `prepareCanvas` DPR step is easy to omit, which renders blurry on Retina
142
- // (see agent-docs/reference/GPU_RENDERING.md §"Canvas scaling & hi-DPI").
142
+ // (see agent-docs/reference/GPU_SHADERS.md §"Canvas scaling & hi-DPI").
143
143
  // Returning the
144
144
  // prepared ctx structurally prevents that omission.
145
145
  export function getPreparedCanvas2D(canvas, width, height) {
@@ -40,9 +40,9 @@ export declare function createRenderingBackend<TRenderingBackend>(canvas: HTMLCa
40
40
  * `DisplayChrome` machinery as a GPU display — the lifecycle is backend-
41
41
  * agnostic, so nothing downstream knows or cares there's no HAL.
42
42
  *
43
- * Reference: `plugins/sequence`'s `SequenceRenderer`. Promote to the dual-path
43
+ * No in-tree display uses it. Promote to the dual-path
44
44
  * `createRenderingBackend` only once a profile shows Canvas2D can't keep 60fps
45
45
  * at the display's real feature counts (≳100K features/frame — see
46
- * RFC-001 §3a).
46
+ * agent-docs/reference/PLUGIN_ABI_STABILITY.md §Canvas2D or GPU).
47
47
  */
48
48
  export declare function createCanvas2DBackend<TRenderingBackend>(canvas: HTMLCanvasElement, createCanvas2DRenderingBackend: (canvas: HTMLCanvasElement) => TRenderingBackend): Promise<TRenderingBackend>;
@@ -50,10 +50,10 @@ export async function createRenderingBackend(canvas, { passes, sampleCount = der
50
50
  * `DisplayChrome` machinery as a GPU display — the lifecycle is backend-
51
51
  * agnostic, so nothing downstream knows or cares there's no HAL.
52
52
  *
53
- * Reference: `plugins/sequence`'s `SequenceRenderer`. Promote to the dual-path
53
+ * No in-tree display uses it. Promote to the dual-path
54
54
  * `createRenderingBackend` only once a profile shows Canvas2D can't keep 60fps
55
55
  * at the display's real feature counts (≳100K features/frame — see
56
- * RFC-001 §3a).
56
+ * agent-docs/reference/PLUGIN_ABI_STABILITY.md §Canvas2D or GPU).
57
57
  */
58
58
  export function createCanvas2DBackend(canvas, createCanvas2DRenderingBackend) {
59
59
  return Promise.resolve(createCanvas2DRenderingBackend(canvas));
@@ -54,7 +54,7 @@ async function probeWebgpu() {
54
54
  * moment this returns and the browser reclaims the context on GC, the same
55
55
  * release the HAL relies on. A page that then reaches the 16-context ceiling
56
56
  * evicts this one first (it is the oldest, and nothing draws to it or
57
- * re-acquires it), so the eviction cascade in GPU_CONTEXT_BUDGET.md cannot
57
+ * re-acquires it), so the eviction cascade in GPU_PORTABILITY.md cannot
58
58
  * start here.
59
59
  *
60
60
  * The driver string comes off the same context, so it is free where it matters:
@@ -64,7 +64,7 @@ async function climbLadder(canvas, { passes, sampleCount, failures }) {
64
64
  // volvox tracks, and *no scroll churn at all* — headless Chrome (SwiftShader),
65
65
  // three runs each: WebGL2 blocks the main thread for 1.3-5.5 s in a single
66
66
  // task and 2.1-8.8 s in total, while Canvas2D never exceeds 339 ms and never
67
- // once produces a task over 500 ms. GPU_CONTEXT_BUDGET.md has the churn case
67
+ // once produces a task over 500 ms. GPU_PORTABILITY.md has the churn case
68
68
  // at ~25x; this is the floor, and it is the load-time pipeline build rather
69
69
  // than the per-pass rebuild the churn number measures.
70
70
  //
@@ -28,21 +28,18 @@ export function bindGroupLayoutEntries(bindings) {
28
28
  });
29
29
  }
30
30
  function gpuBlendState(bs) {
31
- // WebGPU rejects any factor but 'one' under max and min, which ignore them.
32
- // Alpha takes the same equation, as webgl2Hal's applyBlendState does.
31
+ // WebGPU rejects any factor but 'one' under max, which ignores them. Alpha
32
+ // accumulates as webgl2Hal's applyBlendState does.
33
33
  const max = { srcFactor: 'one', dstFactor: 'one', operation: 'max' };
34
- const min = { srcFactor: 'one', dstFactor: 'one', operation: 'min' };
35
34
  const behind = { srcFactor: 'one-minus-dst-alpha', dstFactor: 'one' };
36
35
  return bs.op === 'max'
37
36
  ? { color: max, alpha: max }
38
- : bs.op === 'min'
39
- ? { color: min, alpha: min }
40
- : bs.op === 'behind'
41
- ? { color: behind, alpha: behind }
42
- : {
43
- color: { srcFactor: bs.srcFactor, dstFactor: bs.dstFactor },
44
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
45
- };
37
+ : bs.op === 'behind'
38
+ ? { color: behind, alpha: behind }
39
+ : {
40
+ color: { srcFactor: bs.srcFactor, dstFactor: bs.dstFactor },
41
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
42
+ };
46
43
  }
47
44
  export function pipelineRecipe(desc, wgslSource, sampleCount) {
48
45
  return {
@@ -38,8 +38,6 @@ export type BlendState = {
38
38
  dstFactor: BlendFactor;
39
39
  } | {
40
40
  op: 'max';
41
- } | {
42
- op: 'min';
43
41
  } | {
44
42
  op: 'behind';
45
43
  };
@@ -43,7 +43,7 @@ export declare class WebGL2Hal extends GpuHalBase<RegionPassBuffer> implements G
43
43
  * owns (`antialias` above one sample), so this HAL holds no separate target —
44
44
  * releasing its samples means dropping the context, which costs a re-acquire
45
45
  * against a hard 16-context ceiling and a full shader recompile per display.
46
- * `agent-docs/reference/GPU_CONTEXT_BUDGET.md` measured that trade and
46
+ * `agent-docs/reference/GPU_PORTABILITY.md` measured that trade and
47
47
  * declined it; what makes the WebGPU side worth doing is that it pays
48
48
  * neither.
49
49
  */
@@ -308,7 +308,7 @@ export class WebGL2Hal extends GpuHalBase {
308
308
  * owns (`antialias` above one sample), so this HAL holds no separate target —
309
309
  * releasing its samples means dropping the context, which costs a re-acquire
310
310
  * against a hard 16-context ceiling and a full shader recompile per display.
311
- * `agent-docs/reference/GPU_CONTEXT_BUDGET.md` measured that trade and
311
+ * `agent-docs/reference/GPU_PORTABILITY.md` measured that trade and
312
312
  * declined it; what makes the WebGPU side worth doing is that it pays
313
313
  * neither.
314
314
  */
@@ -458,9 +458,6 @@ export class WebGL2Hal extends GpuHalBase {
458
458
  if (bs?.op === 'max') {
459
459
  gl.blendEquation(gl.MAX);
460
460
  }
461
- else if (bs?.op === 'min') {
462
- gl.blendEquation(gl.MIN);
463
- }
464
461
  else if (bs?.op === 'behind') {
465
462
  gl.blendEquation(gl.FUNC_ADD);
466
463
  gl.blendFuncSeparate(gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE);
@@ -60,9 +60,8 @@ async function buildPipeline(device, recipe, passId) {
60
60
  multisample: recipe.sampleCount === 1 ? undefined : { count: recipe.sampleCount },
61
61
  });
62
62
  }
63
- // Every declared pass up front, unlike WebGL2's first-draw link; see
64
- // ARCHITECTURAL_LIMITS.md §"Every WebGPU display resolves its whole pass list
65
- // before it can paint". Each pass compiles as soon as its own WGSL arrives.
63
+ // Every declared pass up front, unlike WebGL2's first-draw link. Each pass
64
+ // compiles as soon as its own WGSL arrives.
66
65
  async function resolvePipelines(device, descriptors, sampleCount) {
67
66
  const built = await Promise.all(descriptors.map(async (desc) => {
68
67
  const { WGSL_SOURCE } = await desc.source.wgsl();
@@ -14,7 +14,7 @@ export { appendGlyph, pointInsetPx } from './glyphPaint.ts';
14
14
  export { recordPath } from './pathRecorder.ts';
15
15
  export { abgrToCssRgba, makeAbgrFill } from './colorFill.ts';
16
16
  export { RAMP_NOT_A_NUMBER_COLOR, RAMP_NO_VALUE_BITS, RAMP_NO_VALUE_COLOR, } from '../shaders/markColor.generated.ts';
17
- export { instanceColor, isThreshold, keepRampValues, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.ts';
17
+ export { instanceColor, isThreshold, keepRampValues, paintColors, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.ts';
18
18
  export { HIDDEN_ROW, NO_ROW_COLOR, RowKeys, buildRowTable } from './rowTable.ts';
19
19
  export { keySlot, rowColor, rowSlot } from './rowLane.ts';
20
20
  export { rowSpanIndex, spansInRow } from './rowSpanIndex.ts';
@@ -14,7 +14,7 @@ export { appendGlyph, pointInsetPx } from './glyphPaint.js';
14
14
  export { recordPath } from './pathRecorder.js';
15
15
  export { abgrToCssRgba, makeAbgrFill } from './colorFill.js';
16
16
  export { RAMP_NOT_A_NUMBER_COLOR, RAMP_NO_VALUE_BITS, RAMP_NO_VALUE_COLOR, } from '../shaders/markColor.generated.js';
17
- export { instanceColor, isThreshold, keepRampValues, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.js';
17
+ export { instanceColor, isThreshold, keepRampValues, paintColors, rampUniforms, rampValueBits, thresholdBandOf, } from './markRamp.js';
18
18
  export { HIDDEN_ROW, NO_ROW_COLOR, RowKeys, buildRowTable } from './rowTable.js';
19
19
  export { keySlot, rowColor, rowSlot } from './rowLane.js';
20
20
  export { rowSpanIndex, spansInRow } from './rowSpanIndex.js';
@@ -56,8 +56,10 @@ export declare function rampValueBits(values: Float32Array): Uint32Array<ArrayBu
56
56
  */
57
57
  export declare function keepRampValues(values: Float32Array, keep: (value: number, index: number) => boolean): Float32Array<ArrayBuffer>;
58
58
  /**
59
- * The interval a value is in on a threshold: how many of the ascending cuts it
60
- * is at or past, `thresholdIndex`'s rule and the shader's `thresholdBand`.
59
+ * The interval a lane value is in on a threshold: how many of the ascending
60
+ * cuts it is at or past, `thresholdIndex`'s rule and the shader's
61
+ * `thresholdBand`. The value is a float32 and so is each cut in the shader's
62
+ * uniform, so a cut compares as one here: 0.7 off a lane is at a cut of 0.7.
61
63
  */
62
64
  export declare function thresholdBandOf(value: number, cuts: readonly number[]): number;
63
65
  /**
@@ -99,13 +99,15 @@ export function keepRampValues(values, keep) {
99
99
  return new Float32Array(kept.buffer, kept.byteOffset, kept.length);
100
100
  }
101
101
  /**
102
- * The interval a value is in on a threshold: how many of the ascending cuts it
103
- * is at or past, `thresholdIndex`'s rule and the shader's `thresholdBand`.
102
+ * The interval a lane value is in on a threshold: how many of the ascending
103
+ * cuts it is at or past, `thresholdIndex`'s rule and the shader's
104
+ * `thresholdBand`. The value is a float32 and so is each cut in the shader's
105
+ * uniform, so a cut compares as one here: 0.7 off a lane is at a cut of 0.7.
104
106
  */
105
107
  export function thresholdBandOf(value, cuts) {
106
108
  let band = 0;
107
109
  for (const cut of cuts) {
108
- if (value >= cut) {
110
+ if (value >= Math.fround(cut)) {
109
111
  band++;
110
112
  }
111
113
  }
@@ -150,9 +152,7 @@ export function paintColors(c, count, scale) {
150
152
  const colors = new Uint32Array(count);
151
153
  const bits = rampValueBits(colorValue);
152
154
  const colorOf = isThreshold(scale)
153
- ? (value) => Number.isFinite(value)
154
- ? scale.colors[thresholdBandOf(value, scale.cuts)]
155
- : scale.colors[value > 0 ? scale.cuts.length : 0]
155
+ ? thresholdColorOf(scale)
156
156
  : rampColorOf(scale);
157
157
  for (let i = 0; i < count; i++) {
158
158
  const value = colorValue[i];
@@ -175,6 +175,14 @@ function constantColors(c, color, count) {
175
175
  c.constantBake = colors;
176
176
  return colors;
177
177
  }
178
+ // The cuts the shader's uniform block holds, so Canvas2D paints a value past
179
+ // the last of them in the band the GPU does.
180
+ function thresholdColorOf(scale) {
181
+ const cuts = scale.cuts.slice(0, MAX_COLOR_CUTS);
182
+ return (value) => Number.isFinite(value)
183
+ ? scale.colors[thresholdBandOf(value, cuts)]
184
+ : scale.colors[value > 0 ? cuts.length : 0];
185
+ }
178
186
  function rampColorOf(ramp) {
179
187
  const [min, max] = ramp.domain;
180
188
  const log = ramp.scale === 'log';
@@ -16,8 +16,9 @@ export interface RowSpanIndex {
16
16
  }
17
17
  /**
18
18
  * The index over `count` spans from `x` to `x2`, each in the row `row` names
19
- * (row 0 where there is no lane). A counting sort by row, a sort of each row
20
- * the input left out of start order, and one pass for the reach.
19
+ * (row 0 where there is no lane), less those on `HIDDEN_ROW`, which no row
20
+ * holds. A counting sort by row, a sort of each row the input left out of
21
+ * start order, and one pass for the reach.
21
22
  */
22
23
  export declare function rowSpanIndex(x: Uint32Array, x2: Uint32Array, row: Uint32Array | undefined, count?: number): RowSpanIndex;
23
24
  /**
@@ -1,3 +1,4 @@
1
+ import { HIDDEN_ROW } from './rowTable.js';
1
2
  const lowEnd = (x, x2, i) => Math.min(x[i], x2[i]);
2
3
  const highEnd = (x, x2, i) => Math.max(x[i], x2[i]);
3
4
  // A row that arrived nearly in start order, as `cells` writes one (an
@@ -53,22 +54,27 @@ function sortRow(order, from, to, x, x2) {
53
54
  }
54
55
  /**
55
56
  * The index over `count` spans from `x` to `x2`, each in the row `row` names
56
- * (row 0 where there is no lane). A counting sort by row, a sort of each row
57
- * the input left out of start order, and one pass for the reach.
57
+ * (row 0 where there is no lane), less those on `HIDDEN_ROW`, which no row
58
+ * holds. A counting sort by row, a sort of each row the input left out of
59
+ * start order, and one pass for the reach.
58
60
  */
59
61
  export function rowSpanIndex(x, x2, row, count = x.length) {
60
62
  let rows = 1;
61
63
  if (row) {
62
64
  for (let i = 0; i < count; i++) {
63
- if (row[i] + 1 > rows) {
64
- rows = row[i] + 1;
65
+ const r = row[i];
66
+ if (r !== HIDDEN_ROW && r + 1 > rows) {
67
+ rows = r + 1;
65
68
  }
66
69
  }
67
70
  }
68
71
  const rowStart = new Uint32Array(rows + 1);
69
72
  if (row) {
70
73
  for (let i = 0; i < count; i++) {
71
- rowStart[row[i] + 1]++;
74
+ const r = row[i];
75
+ if (r !== HIDDEN_ROW) {
76
+ rowStart[r + 1]++;
77
+ }
72
78
  }
73
79
  for (let r = 0; r < rows; r++) {
74
80
  rowStart[r + 1] = rowStart[r + 1] + rowStart[r];
@@ -77,11 +83,15 @@ export function rowSpanIndex(x, x2, row, count = x.length) {
77
83
  else {
78
84
  rowStart[1] = count;
79
85
  }
80
- const order = new Uint32Array(count);
86
+ const placed = rowStart[rows];
87
+ const order = new Uint32Array(placed);
81
88
  if (row) {
82
89
  const next = rowStart.slice(0, rows);
83
90
  for (let i = 0; i < count; i++) {
84
- order[next[row[i]]++] = i;
91
+ const r = row[i];
92
+ if (r !== HIDDEN_ROW) {
93
+ order[next[r]++] = i;
94
+ }
85
95
  }
86
96
  }
87
97
  else {
@@ -89,7 +99,7 @@ export function rowSpanIndex(x, x2, row, count = x.length) {
89
99
  order[i] = i;
90
100
  }
91
101
  }
92
- const reach = new Uint32Array(count);
102
+ const reach = new Uint32Array(placed);
93
103
  for (let r = 0; r < rows; r++) {
94
104
  const from = rowStart[r];
95
105
  const to = rowStart[r + 1];
@@ -20,12 +20,11 @@ export interface ShaderModule {
20
20
  *
21
21
  * - `verticesPerInstance` — the canvas chevron pass, whose count is the
22
22
  * shader's `CHEVRON_VERTS` times a cap the *renderer* chooses.
23
- * - `blendState` / `topology` — the synteny fill is the one module drawn under
24
- * two blends: its strongest-ribbon passes take min or max where the stacking
25
- * pass inherits src-over, since one uniform switches the fragment between
26
- * the two outputs. Wiggle's step line and center line each have their own
27
- * module instead, and the center line states `//! blend: max` itself. A
28
- * shader whose passes disagree declares neither and says which at each pass.
23
+ * - `blendState` / `topology` — no pass in the tree overrides these. Wiggle's
24
+ * step line and center line did while they shared one module and blended
25
+ * differently (src-over against max); each record now has its own module,
26
+ * and the center line's states `//! blend: max` itself. A shader whose passes
27
+ * disagree declares neither and says which at each pass.
29
28
  * - `blend: false` — nothing disables blending today, which is why there is no
30
29
  * `//! blend: none` to inherit it from.
31
30
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/render-core",
3
- "version": "5.0.0-beta.10",
3
+ "version": "5.0.0-beta.11",
4
4
  "type": "module",
5
5
  "description": "GPU/Canvas2D rendering primitives for JBrowse displays: the HAL, the draw-lifecycle mixin, per-region/global backend bases, and the React backend hooks",
6
6
  "keywords": [
@@ -95,7 +95,7 @@
95
95
  "./useTabVisibilityRerender": "./esm/useTabVisibilityRerender.js"
96
96
  },
97
97
  "dependencies": {
98
- "@jbrowse/mobx-state-tree": "^6.6.1",
98
+ "@jbrowse/mobx-state-tree": "^6.7.0",
99
99
  "mobx": "^7.0.5"
100
100
  },
101
101
  "peerDependencies": {
@@ -2,7 +2,7 @@
2
2
  // `capsuleDist` and nowhere else: the edge ramp over that distance is
3
3
  // `antialias`'s one `edgeCoverage`, shared with every butt-capped stroke in the
4
4
  // tree. Consumers, and which half each takes:
5
- // agent-docs/reference/SHADER_SHAPE_LIBRARY.md.
5
+ // agent-docs/reference/SHADER_JS_CODEGEN.md.
6
6
  //
7
7
  // `capsuleQuadLocal` pads by exactly `aaHalfPx(dpr)` past the ink. Pad less and
8
8
  // the ramp is clipped at the quad edge — coverage falls to 0.5 at the boundary,
@@ -17,7 +17,7 @@
17
17
  // are sparse intervals with nothing beside them, so it floors; MAF's cells
18
18
  // never gap and it passes 0; wiggle density tiles at most zooms and gaps where
19
19
  // its source does, so it floors and pays nothing for it on the tiling. Both
20
- // halves measured — `agent-docs/reference/MAF_SUBPIXEL_CELLS.md`.
20
+ // halves measured — `agent-docs/reference/MAF_LARGE_BLOCKS.md`.
21
21
  //! export-consts: MIN_DRAWN_ROW_PX, MULTI_ROW_MIN_CELL_PX
22
22
  //! js-export: drawnRowHeightPx, rowBandOffsetPx
23
23
  module rowRect;
@@ -68,7 +68,7 @@ public struct RowRectVsOut {
68
68
  // There is no X-axis twin. A row is a container the reader is counting, so
69
69
  // losing one loses a species; a cell is a sample of a tiling, so losing one to
70
70
  // the blend is the honest answer and flooring it invents ink
71
- // (`agent-docs/reference/MAF_SUBPIXEL_CELLS.md`). The caller that does want an X
71
+ // (`agent-docs/reference/MAF_LARGE_BLOCKS.md`). The caller that does want an X
72
72
  // floor names its own width, below.
73
73
  static const float MIN_DRAWN_ROW_PX = 1.0;
74
74