@jbrowse/render-core 4.3.0

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 (164) hide show
  1. package/LICENSE +201 -0
  2. package/esm/OverlayCanvas.d.ts +29 -0
  3. package/esm/OverlayCanvas.js +43 -0
  4. package/esm/RenderCanvas.d.ts +76 -0
  5. package/esm/RenderCanvas.js +38 -0
  6. package/esm/RenderLifecycleMixin.d.ts +180 -0
  7. package/esm/RenderLifecycleMixin.js +279 -0
  8. package/esm/ScrollLockedOverlay.d.ts +28 -0
  9. package/esm/ScrollLockedOverlay.js +42 -0
  10. package/esm/blockClipUtils.d.ts +27 -0
  11. package/esm/blockClipUtils.js +101 -0
  12. package/esm/canvas2dUtils.d.ts +372 -0
  13. package/esm/canvas2dUtils.js +564 -0
  14. package/esm/canvasContext.d.ts +68 -0
  15. package/esm/canvasContext.js +74 -0
  16. package/esm/colorRampLut.d.ts +17 -0
  17. package/esm/colorRampLut.js +25 -0
  18. package/esm/coverageBand.d.ts +107 -0
  19. package/esm/coverageBand.js +109 -0
  20. package/esm/coverageBandBuffers.d.ts +30 -0
  21. package/esm/coverageBandBuffers.js +13 -0
  22. package/esm/createRenderingBackend.d.ts +52 -0
  23. package/esm/createRenderingBackend.js +58 -0
  24. package/esm/displayPhase.d.ts +155 -0
  25. package/esm/displayPhase.js +97 -0
  26. package/esm/globalRenderingBackend.d.ts +90 -0
  27. package/esm/globalRenderingBackend.js +53 -0
  28. package/esm/gpuDevice.d.ts +91 -0
  29. package/esm/gpuDevice.js +277 -0
  30. package/esm/graphicsCapabilities.d.ts +98 -0
  31. package/esm/graphicsCapabilities.js +165 -0
  32. package/esm/hal/createHal.d.ts +22 -0
  33. package/esm/hal/createHal.js +91 -0
  34. package/esm/hal/deviceGpuCache.d.ts +36 -0
  35. package/esm/hal/deviceGpuCache.js +120 -0
  36. package/esm/hal/gpuHalBase.d.ts +55 -0
  37. package/esm/hal/gpuHalBase.js +81 -0
  38. package/esm/hal/index.d.ts +7 -0
  39. package/esm/hal/index.js +5 -0
  40. package/esm/hal/mockHal.d.ts +153 -0
  41. package/esm/hal/mockHal.js +297 -0
  42. package/esm/hal/oomReporter.d.ts +15 -0
  43. package/esm/hal/oomReporter.js +22 -0
  44. package/esm/hal/passIds.d.ts +22 -0
  45. package/esm/hal/passIds.js +36 -0
  46. package/esm/hal/regionRegistry.d.ts +11 -0
  47. package/esm/hal/regionRegistry.js +66 -0
  48. package/esm/hal/types.d.ts +116 -0
  49. package/esm/hal/types.js +1 -0
  50. package/esm/hal/webgl2Hal.d.ts +47 -0
  51. package/esm/hal/webgl2Hal.js +490 -0
  52. package/esm/hal/webgpuHal.d.ts +169 -0
  53. package/esm/hal/webgpuHal.js +722 -0
  54. package/esm/installUpload.d.ts +91 -0
  55. package/esm/installUpload.js +94 -0
  56. package/esm/instanceCache.d.ts +43 -0
  57. package/esm/instanceCache.js +53 -0
  58. package/esm/instancePass.d.ts +50 -0
  59. package/esm/instancePass.js +34 -0
  60. package/esm/keyedRenderingBackend.d.ts +41 -0
  61. package/esm/keyedRenderingBackend.js +19 -0
  62. package/esm/mapUploadSync.d.ts +31 -0
  63. package/esm/mapUploadSync.js +45 -0
  64. package/esm/namedReactions.d.ts +29 -0
  65. package/esm/namedReactions.js +62 -0
  66. package/esm/perRegionRenderingBackend.d.ts +114 -0
  67. package/esm/perRegionRenderingBackend.js +93 -0
  68. package/esm/recoveryBudget.d.ts +40 -0
  69. package/esm/recoveryBudget.js +56 -0
  70. package/esm/regionDataMap.d.ts +60 -0
  71. package/esm/regionDataMap.js +98 -0
  72. package/esm/renderBlock.d.ts +25 -0
  73. package/esm/renderBlock.js +10 -0
  74. package/esm/renderingBackendBase.d.ts +71 -0
  75. package/esm/renderingBackendBase.js +50 -0
  76. package/esm/shaders/antialias.js.generated.d.ts +4 -0
  77. package/esm/shaders/antialias.js.generated.js +25 -0
  78. package/esm/shaders/capsule.generated.d.ts +1 -0
  79. package/esm/shaders/capsule.generated.js +3 -0
  80. package/esm/shaders/colorRampLut.generated.d.ts +1 -0
  81. package/esm/shaders/colorRampLut.generated.js +3 -0
  82. package/esm/shaders/coverageBand.generated.d.ts +2 -0
  83. package/esm/shaders/coverageBand.generated.js +4 -0
  84. package/esm/shaders/coverageBar.generated.d.ts +4 -0
  85. package/esm/shaders/coverageBar.generated.js +6 -0
  86. package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
  87. package/esm/shaders/coverageBar.iface.generated.js +114 -0
  88. package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
  89. package/esm/shaders/coverageIndicator.generated.js +6 -0
  90. package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
  91. package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
  92. package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
  93. package/esm/shaders/coverageInterbase.generated.js +6 -0
  94. package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
  95. package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
  96. package/esm/shaders/coverageMod.generated.d.ts +4 -0
  97. package/esm/shaders/coverageMod.generated.js +6 -0
  98. package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
  99. package/esm/shaders/coverageMod.iface.generated.js +144 -0
  100. package/esm/shaders/coverageSnp.generated.d.ts +4 -0
  101. package/esm/shaders/coverageSnp.generated.js +6 -0
  102. package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
  103. package/esm/shaders/coverageSnp.iface.generated.js +144 -0
  104. package/esm/shaders/hpmath.js.generated.d.ts +3 -0
  105. package/esm/shaders/hpmath.js.generated.js +48 -0
  106. package/esm/shaders/pointGlyph.generated.d.ts +1 -0
  107. package/esm/shaders/pointGlyph.generated.js +3 -0
  108. package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
  109. package/esm/shaders/pointGlyph.js.generated.js +7 -0
  110. package/esm/shaders/rowRect.generated.d.ts +3 -0
  111. package/esm/shaders/rowRect.generated.js +5 -0
  112. package/esm/shaders/rowRect.js.generated.d.ts +2 -0
  113. package/esm/shaders/rowRect.js.generated.js +13 -0
  114. package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
  115. package/esm/shaders/scoreScale.js.generated.js +46 -0
  116. package/esm/slangPass.d.ts +49 -0
  117. package/esm/slangPass.js +22 -0
  118. package/esm/useEventCallback.d.ts +1 -0
  119. package/esm/useEventCallback.js +19 -0
  120. package/esm/useGraphicsCapabilities.d.ts +17 -0
  121. package/esm/useGraphicsCapabilities.js +38 -0
  122. package/esm/useRenderingBackend.d.ts +83 -0
  123. package/esm/useRenderingBackend.js +422 -0
  124. package/esm/useTabVisibilityRerender.d.ts +13 -0
  125. package/esm/useTabVisibilityRerender.js +39 -0
  126. package/esm/webgpuUtils.d.ts +23 -0
  127. package/esm/webgpuUtils.js +76 -0
  128. package/package.json +91 -0
  129. package/src/shaders/antialias.js.generated.ts +33 -0
  130. package/src/shaders/antialias.slang +112 -0
  131. package/src/shaders/capsule.generated.ts +4 -0
  132. package/src/shaders/capsule.slang +62 -0
  133. package/src/shaders/colorPack.slang +26 -0
  134. package/src/shaders/colorRampLut.generated.ts +4 -0
  135. package/src/shaders/colorRampLut.slang +51 -0
  136. package/src/shaders/coverageBand.generated.ts +6 -0
  137. package/src/shaders/coverageBand.slang +276 -0
  138. package/src/shaders/coverageBar.generated.ts +10 -0
  139. package/src/shaders/coverageBar.iface.generated.ts +172 -0
  140. package/src/shaders/coverageBar.slang +37 -0
  141. package/src/shaders/coverageIndicator.generated.ts +10 -0
  142. package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
  143. package/src/shaders/coverageIndicator.slang +61 -0
  144. package/src/shaders/coverageInterbase.generated.ts +10 -0
  145. package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
  146. package/src/shaders/coverageInterbase.slang +63 -0
  147. package/src/shaders/coverageMod.generated.ts +10 -0
  148. package/src/shaders/coverageMod.iface.generated.ts +211 -0
  149. package/src/shaders/coverageMod.slang +36 -0
  150. package/src/shaders/coverageSnp.generated.ts +10 -0
  151. package/src/shaders/coverageSnp.iface.generated.ts +211 -0
  152. package/src/shaders/coverageSnp.slang +52 -0
  153. package/src/shaders/diagonalGrid.slang +25 -0
  154. package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
  155. package/src/shaders/hpmath.js.generated.ts +47 -0
  156. package/src/shaders/hpmath.slang +226 -0
  157. package/src/shaders/pointGlyph.generated.ts +4 -0
  158. package/src/shaders/pointGlyph.js.generated.ts +9 -0
  159. package/src/shaders/pointGlyph.slang +93 -0
  160. package/src/shaders/rowRect.generated.ts +8 -0
  161. package/src/shaders/rowRect.js.generated.ts +17 -0
  162. package/src/shaders/rowRect.slang +160 -0
  163. package/src/shaders/scoreScale.js.generated.ts +51 -0
  164. package/src/shaders/scoreScale.slang +68 -0
@@ -0,0 +1,91 @@
1
+ import type { LifecycleHost } from './RenderLifecycleMixin.ts';
2
+ /**
3
+ * What every backend that takes keyed uploads implements. One cell is one
4
+ * immutable payload under one key, and the key is whatever the display's own
5
+ * map is keyed by: a `displayedRegionIndex`, a sibling display's
6
+ * `sharedBackendKey`, or a slot name on a display that holds one payload.
7
+ *
8
+ * `release` is per key, never an active-set prune. The diff below knows
9
+ * exactly which keys departed, so a per-key release does the same job on a
10
+ * display's own map and is the only correct shape on a canvas several displays
11
+ * share, where a prune computed from one display's map would wipe its
12
+ * siblings' buffers.
13
+ */
14
+ export interface CellTarget<K, Encoded> {
15
+ upload(key: K, data: Encoded): void;
16
+ release(key: K): void;
17
+ }
18
+ /**
19
+ * What the backend takes, read off the backend, which is the direction the
20
+ * overloads have to check in: the payload reaching `upload` must be assignable
21
+ * to the parameter it declares. Inferring the other way, from `encode`'s
22
+ * return, loses to the contravariant `backend` position and no encoding
23
+ * display resolves an overload.
24
+ */
25
+ type EncodedOf<B> = B extends {
26
+ upload(key: never, data: infer E): void;
27
+ } ? E : never;
28
+ export interface UploadSpec<K, Data, Props, Encoded, B> {
29
+ /**
30
+ * The display's payloads, one per key. Read inside the upload autorun, so a
31
+ * commit re-runs the diff and only the entries whose reference moved reach
32
+ * the backend. Absence is absence: a display with nothing to upload yet
33
+ * leaves the key out rather than storing `undefined` under it, and the
34
+ * departed key is released.
35
+ */
36
+ cells: () => ReadonlyMap<K, Data>;
37
+ /**
38
+ * Everything `encode` needs beyond the cell's own data, as one value. The
39
+ * helper memoizes it and re-encodes every cell on its identity, so return
40
+ * the narrow thing a re-encode is actually about, never the whole render
41
+ * state.
42
+ */
43
+ inputs?: () => Props;
44
+ /**
45
+ * Build one cell's upload payload. Pure in `(data, props, key)`: an
46
+ * observable read here is tracked but invalidates nothing, only `cells` and
47
+ * `inputs` do. The key is for a display whose cells are not all the same
48
+ * kind of thing, a contact matrix beside the palette it is drawn through.
49
+ */
50
+ encode?: (data: Data, props: Props, key: K) => Encoded;
51
+ /**
52
+ * Draw one frame from the latest encoded map and say whether real content
53
+ * reached the canvas.
54
+ */
55
+ render: (backend: B, encoded: ReadonlyMap<K, Encoded>) => boolean;
56
+ }
57
+ /**
58
+ * The cells of a display that holds one payload for the whole view: the key
59
+ * while the payload is there, nothing while it is not. Absence is how such a
60
+ * display says "nothing to upload yet", so the diff releases the key rather
61
+ * than being handed `undefined` under it.
62
+ */
63
+ export declare function oneCell<K, T>(key: K, value: T | undefined | null): ReadonlyMap<K, T>;
64
+ /**
65
+ * The one upload lifecycle. A display hands over a map of immutable payloads
66
+ * and a render; the helper diffs the map by reference on every commit,
67
+ * encodes and uploads what moved, releases what left, and re-uploads
68
+ * everything into a fresh backend after a context loss.
69
+ *
70
+ * Per-region, shared-canvas and whole-view displays were three installers
71
+ * over this one diff, split by how a departed key was released and by the
72
+ * name of the backend's upload method. With `CellTarget` naming both, what is
73
+ * left per family is the key the display's map is keyed by.
74
+ *
75
+ * **Calling again only swaps the backend.** Displays wire this from
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.
78
+ *
79
+ * @see installUpload.test.ts for the upload, encode and release counts.
80
+ * @see ADR-078 for why this is one autorun and a diff.
81
+ */
82
+ export declare function installUpload<K, Data extends EncodedOf<B>, B extends CellTarget<K, unknown>>(self: LifecycleHost, backend: B, spec: {
83
+ cells: () => ReadonlyMap<K, Data>;
84
+ render: (backend: B, encoded: ReadonlyMap<K, Data>) => boolean;
85
+ encode?: never;
86
+ inputs?: never;
87
+ }): void;
88
+ export declare function installUpload<K, Data, Props, B extends CellTarget<K, unknown>>(self: LifecycleHost, backend: B, spec: UploadSpec<K, Data, Props, EncodedOf<B>, B> & {
89
+ encode: (data: Data, props: Props, key: K) => EncodedOf<B>;
90
+ }): void;
91
+ export {};
@@ -0,0 +1,94 @@
1
+ import { getType } from '@jbrowse/mobx-state-tree';
2
+ import { computed } from 'mobx';
3
+ import { createMapUploadSync } from './mapUploadSync.js';
4
+ import { isCheckedAtTheStore } from './regionDataMap.js';
5
+ /**
6
+ * The cells of a display that holds one payload for the whole view: the key
7
+ * while the payload is there, nothing while it is not. Absence is how such a
8
+ * display says "nothing to upload yet", so the diff releases the key rather
9
+ * than being handed `undefined` under it.
10
+ */
11
+ export function oneCell(key, value) {
12
+ const cells = new Map();
13
+ if (value !== undefined && value !== null) {
14
+ cells.set(key, value);
15
+ }
16
+ return cells;
17
+ }
18
+ const alreadyNamed = new WeakMap();
19
+ function checkPayloads(self, cells) {
20
+ if (isCheckedAtTheStore(cells)) {
21
+ return;
22
+ }
23
+ for (const [key, value] of cells) {
24
+ if (value === undefined || value === null) {
25
+ let named = alreadyNamed.get(self);
26
+ if (!named) {
27
+ named = new Set();
28
+ alreadyNamed.set(self, named);
29
+ }
30
+ if (!named.has(key)) {
31
+ named.add(key);
32
+ console.error(`[jbrowse display contract] ${getType(self).name}: cell ${String(key)} ` +
33
+ `handed to installUpload is \`${String(value)}\`. A fetch answers ` +
34
+ 'a payload or nothing; leave the key out until it has one.');
35
+ }
36
+ }
37
+ }
38
+ }
39
+ export function installUpload(self, backend, { cells, inputs, encode, render }) {
40
+ self.attachRenderingBackend(backend, () => {
41
+ const sync = createMapUploadSync({
42
+ upload: (b, key, data) => {
43
+ b.upload(key, data);
44
+ },
45
+ remove: (b, key) => {
46
+ b.release(key);
47
+ },
48
+ });
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;
66
+ return {
67
+ 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
+ }
88
+ }
89
+ return sync(b, encoded);
90
+ },
91
+ render: b => render(b, encoded),
92
+ };
93
+ });
94
+ }
@@ -0,0 +1,43 @@
1
+ export interface InstanceCacheOpts<TData> {
2
+ /**
3
+ * A field of `data` whose object identity changes exactly when the geometry
4
+ * does. Any one of the coordinate arrays works, because a refetch replaces
5
+ * them atomically — but it must be a coordinate array, not a color one.
6
+ */
7
+ geomToken: (data: TData) => object;
8
+ /** The per-instance packed colors, whose identity a recolor replaces. */
9
+ colors: (data: TData) => Uint32Array;
10
+ /** Pack every lane. Called only when the geometry moved. */
11
+ interleave: (data: TData) => ArrayBuffer;
12
+ /** Instance stride and color-lane offset, in u32 words. */
13
+ strideWords: number;
14
+ colorOffsetWords: number;
15
+ }
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.
20
+ *
21
+ * The split is worth having because the two inputs move on completely
22
+ * different schedules — geometry on an RPC refetch, colors on a `colorBy`
23
+ * toggle or a palette change, which is interactive. Synteny re-packs 12 lanes
24
+ * and dotplot 5; both had measured the patch as the difference between a
25
+ * toggle that feels instant and one that doesn't.
26
+ *
27
+ * Shared because both had written it, identically, down to a sync comment on
28
+ * the patch loop saying the color write must mirror the interleave. What
29
+ * makes it worth a helper rather than two copies is the invalidation: get the
30
+ * `geomToken` comparison or the `colors` write-back wrong and the failure is
31
+ * stale geometry on screen, which no test catches and which looks like a data
32
+ * problem. `installUpload` exists for the same reason one
33
+ * layer up — an upload memo is not a hand-rolled `let`.
34
+ *
35
+ * The GPU re-upload still happens on a recolor (no HAL does partial buffer
36
+ * updates); what is skipped is the dominant CPU interleave.
37
+ */
38
+ 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;
43
+ };
@@ -0,0 +1,53 @@
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.
5
+ *
6
+ * The split is worth having because the two inputs move on completely
7
+ * different schedules — geometry on an RPC refetch, colors on a `colorBy`
8
+ * toggle or a palette change, which is interactive. Synteny re-packs 12 lanes
9
+ * and dotplot 5; both had measured the patch as the difference between a
10
+ * toggle that feels instant and one that doesn't.
11
+ *
12
+ * Shared because both had written it, identically, down to a sync comment on
13
+ * the patch loop saying the color write must mirror the interleave. What
14
+ * makes it worth a helper rather than two copies is the invalidation: get the
15
+ * `geomToken` comparison or the `colors` write-back wrong and the failure is
16
+ * stale geometry on screen, which no test catches and which looks like a data
17
+ * problem. `installUpload` exists for the same reason one
18
+ * layer up — an upload memo is not a hand-rolled `let`.
19
+ *
20
+ * The GPU re-upload still happens on a recolor (no HAL does partial buffer
21
+ * updates); what is skipped is the dominant CPU interleave.
22
+ */
23
+ export function createInstanceCache(opts) {
24
+ const { geomToken, colors, interleave, strideWords, colorOffsetWords } = opts;
25
+ const cache = new Map();
26
+ return {
27
+ /** Packed bytes for `key`, re-packing or re-coloring only as needed. */
28
+ get(key, data) {
29
+ const geom = geomToken(data);
30
+ const cols = colors(data);
31
+ const hit = cache.get(key);
32
+ if (hit?.geom === geom) {
33
+ if (hit.colors !== cols) {
34
+ const u32 = new Uint32Array(hit.buf);
35
+ for (let i = 0, n = cols.length; i < n; i++) {
36
+ u32[i * strideWords + colorOffsetWords] = cols[i];
37
+ }
38
+ hit.colors = cols;
39
+ }
40
+ return hit.buf;
41
+ }
42
+ const buf = interleave(data);
43
+ cache.set(key, { geom, colors: cols, buf });
44
+ return buf;
45
+ },
46
+ delete(key) {
47
+ cache.delete(key);
48
+ },
49
+ clear() {
50
+ cache.clear();
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,50 @@
1
+ import type { GpuHal, PipelineDescriptor } from './hal';
2
+ /**
3
+ * A shader pass bundled with the function that packs its instance buffer.
4
+ *
5
+ * A pass id, the packer that fills its buffer, and the upload that joins them
6
+ * are three statements of one fact, and renderers kept them in three places —
7
+ * so a pass could be registered and drawn while nothing uploaded to it, which
8
+ * paints nothing, silently, on the GPU backend only. Here they are one object:
9
+ *
10
+ * ```ts
11
+ * export const GAP_PASS = {
12
+ * ...slangPass({ id: 'gap', mod: gapShader }),
13
+ * pack: packGaps,
14
+ * }
15
+ * ```
16
+ *
17
+ * There is no constructor for this — `slangPass` is still how a pass is built,
18
+ * and a spread is how a packer joins it. That also covers the descriptor built
19
+ * somewhere payload-agnostic, like a shared glyph shape: `{ ...RectPass, pack }`
20
+ * is the same line.
21
+ *
22
+ * `TData` is the pass's own narrow payload, and stays narrow: a registry keyed
23
+ * on a wide payload accepts a packer of a supertype by contravariance, so
24
+ * declaring only the fields a pass actually reads costs nothing at the point of
25
+ * use.
26
+ */
27
+ export interface InstancePass<TData> extends PipelineDescriptor {
28
+ pack: (data: TData) => ArrayBuffer | ArrayBufferView;
29
+ }
30
+ /**
31
+ * Pack a pass's instances for one region and hand them to the HAL.
32
+ *
33
+ * **The instance count is the buffer's own**, not a second expression agreeing
34
+ * with it. The HAL multiplies the count by the stride to find the last
35
+ * instance, so a count past `byteLength / stride` reads off the end —
36
+ * undefined pixels, no throw. A packer that allocates `n * INSTANCE_STRIDE_BYTES`
37
+ * has already stated `n`; asking it again from the source arrays
38
+ * (`positions.length / 2`, `numInsertions`, a parallel array the worker sized
39
+ * separately) is a second expression free to disagree.
40
+ *
41
+ * **A packer that over-allocates must right-size before returning** — the count
42
+ * is read off the bytes, so trailing capacity would draw as instances. Prefer
43
+ * the copy to a subarray view: a view pins the whole allocation, and these
44
+ * payloads are retained per region for as long as the region is loaded.
45
+ *
46
+ * An empty pack is passed through rather than skipped: every HAL deletes the
47
+ * pass's prior buffer before looking at the count, so `count === 0` IS the
48
+ * "this pass has nothing this time" instruction.
49
+ */
50
+ export declare function uploadPass<TData>(hal: GpuHal, regionKey: number, pass: InstancePass<TData>, data: TData): void;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pack a pass's instances for one region and hand them to the HAL.
3
+ *
4
+ * **The instance count is the buffer's own**, not a second expression agreeing
5
+ * with it. The HAL multiplies the count by the stride to find the last
6
+ * instance, so a count past `byteLength / stride` reads off the end —
7
+ * undefined pixels, no throw. A packer that allocates `n * INSTANCE_STRIDE_BYTES`
8
+ * has already stated `n`; asking it again from the source arrays
9
+ * (`positions.length / 2`, `numInsertions`, a parallel array the worker sized
10
+ * separately) is a second expression free to disagree.
11
+ *
12
+ * **A packer that over-allocates must right-size before returning** — the count
13
+ * is read off the bytes, so trailing capacity would draw as instances. Prefer
14
+ * the copy to a subarray view: a view pins the whole allocation, and these
15
+ * payloads are retained per region for as long as the region is loaded.
16
+ *
17
+ * An empty pack is passed through rather than skipped: every HAL deletes the
18
+ * pass's prior buffer before looking at the count, so `count === 0` IS the
19
+ * "this pass has nothing this time" instruction.
20
+ */
21
+ export function uploadPass(hal, regionKey, pass, data) {
22
+ const buf = pass.pack(data);
23
+ const count = buf.byteLength / pass.instanceStride;
24
+ if (!Number.isInteger(count)) {
25
+ // Unreachable from a packer sizing itself off the shader's own
26
+ // `INSTANCE_STRIDE_BYTES`. Reachable if a buffer packed somewhere that
27
+ // cannot import the `.slang` — a worker package with a codegen target of
28
+ // its own — was built against a stride that has since drifted. Loud beats a
29
+ // fractional instance count reaching the HAL.
30
+ throw new Error(`pass ${pass.id}: ${buf.byteLength} bytes is not a whole number of ` +
31
+ `${pass.instanceStride}-byte instances`);
32
+ }
33
+ hal.uploadBuffer(regionKey, pass.id, buf, count);
34
+ }
@@ -0,0 +1,41 @@
1
+ import type { RenderingBackend } from './renderingBackendBase.ts';
2
+ /**
3
+ * Shared contract for **keyed shared-canvas** backends — one canvas painting
4
+ * several displays at once, each holding its own uploaded buffer under its own
5
+ * key. Dotplot (a key per display in the view) and multi-LGV synteny (a key per
6
+ * track level) are the two.
7
+ *
8
+ * The third shape beside per-region and monolithic, and genuinely neither:
9
+ * per-region drives off `renderBlocks(blocks, regions, state)` with the model's
10
+ * data map handed back each frame, and monolithic is one payload under the `data` key
11
+ * with no key at all. These key their uploads and render every key in one
12
+ * frame from state the caller assembled, so they own the map.
13
+ *
14
+ * Unlike the other two this is an interface with no abstract class under it:
15
+ * the shared *state* (`hal` + uniform scratch, or `canvas` + 2D context) is
16
+ * `GpuRenderingBackendBase` / `Canvas2DRenderingBackendBase`, which every keyed
17
+ * backend extends, and there is no shared *behavior* on top — the render loops
18
+ * genuinely differ. An empty abstract class between them would only be a place
19
+ * to look.
20
+ */
21
+ export interface KeyedRenderingBackend<UploadData, RenderState> extends RenderingBackend {
22
+ /** CSS-pixel size of the shared canvas. */
23
+ resize(width: number, height: number): void;
24
+ upload(key: number, data: UploadData): void;
25
+ release(key: number): void;
26
+ /** Paint every key the state names. */
27
+ render(state: RenderState): void;
28
+ }
29
+ /**
30
+ * Stable 32-bit slot for a display on a backend it shares with siblings, hashed
31
+ * from its MST node id (djb2). Collisions are vanishingly rare at display
32
+ * cardinalities, and the alternative — coordinating integer slots between
33
+ * displays that don't know about each other — is worse.
34
+ *
35
+ * Key by this, never by the display's index in its parent's list. An index
36
+ * renumbers when a sibling is hidden or reordered, which hands the survivor a
37
+ * slot holding another display's bytes: the keyed diff sees a changed
38
+ * reference and re-uploads every later display's whole buffer, and any frame
39
+ * drawn between the two mistakes one display's geometry for another's.
40
+ */
41
+ export declare function sharedBackendKey(id: string): number;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stable 32-bit slot for a display on a backend it shares with siblings, hashed
3
+ * from its MST node id (djb2). Collisions are vanishingly rare at display
4
+ * cardinalities, and the alternative — coordinating integer slots between
5
+ * displays that don't know about each other — is worse.
6
+ *
7
+ * Key by this, never by the display's index in its parent's list. An index
8
+ * renumbers when a sibling is hidden or reordered, which hands the survivor a
9
+ * slot holding another display's bytes: the keyed diff sees a changed
10
+ * reference and re-uploads every later display's whole buffer, and any frame
11
+ * drawn between the two mistakes one display's geometry for another's.
12
+ */
13
+ export function sharedBackendKey(id) {
14
+ let h = 5381;
15
+ for (let i = 0; i < id.length; i++) {
16
+ h = ((h << 5) + h + id.charCodeAt(i)) | 0;
17
+ }
18
+ return h >>> 0;
19
+ }
@@ -0,0 +1,31 @@
1
+ export interface MapUploadTarget<K, T, B> {
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
+ remove?: (backend: B, key: K) => void;
11
+ prune?: (backend: B, active: ReadonlySet<K>) => void;
12
+ }
13
+ /**
14
+ * The one reference diff every keyed GPU upload in the tree runs on. Uploads
15
+ * only entries whose data reference changed, releases entries no longer
16
+ * present (and forgets them, so a later same-reference re-arrival still
17
+ * re-uploads), and re-uploads everything when the backend identity changes —
18
+ * a context-loss recovery hands a fresh backend with empty GPU buffers.
19
+ *
20
+ * Returns whether anything reached the backend, which is what lets the upload
21
+ * autorun skip the redraw it would otherwise force after every run.
22
+ *
23
+ * For the skip to fire, the map must keep **stable references** for unchanged
24
+ * entries across recomputes. With an always-fresh map every entry re-uploads,
25
+ * which is correct but defeats the optimization.
26
+ *
27
+ * **A display does not hold one of these.** The lifecycle installers own it,
28
+ * built inside the `attachRenderingBackend` setup thunk so the memo lives
29
+ * exactly as long as the callbacks that read it.
30
+ */
31
+ export declare function createMapUploadSync<K, T, B>(target: MapUploadTarget<K, T, B>): (backend: B, entries: ReadonlyMap<K, T>) => boolean;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The one reference diff every keyed GPU upload in the tree runs on. Uploads
3
+ * only entries whose data reference changed, releases entries no longer
4
+ * present (and forgets them, so a later same-reference re-arrival still
5
+ * re-uploads), and re-uploads everything when the backend identity changes —
6
+ * a context-loss recovery hands a fresh backend with empty GPU buffers.
7
+ *
8
+ * Returns whether anything reached the backend, which is what lets the upload
9
+ * autorun skip the redraw it would otherwise force after every run.
10
+ *
11
+ * For the skip to fire, the map must keep **stable references** for unchanged
12
+ * entries across recomputes. With an always-fresh map every entry re-uploads,
13
+ * which is correct but defeats the optimization.
14
+ *
15
+ * **A display does not hold one of these.** The lifecycle installers own it,
16
+ * built inside the `attachRenderingBackend` setup thunk so the memo lives
17
+ * exactly as long as the callbacks that read it.
18
+ */
19
+ export function createMapUploadSync(target) {
20
+ const uploaded = new Map();
21
+ let lastRenderingBackend;
22
+ return function sync(backend, entries) {
23
+ if (backend !== lastRenderingBackend) {
24
+ uploaded.clear();
25
+ lastRenderingBackend = backend;
26
+ }
27
+ let changed = false;
28
+ for (const [key, data] of entries) {
29
+ if (!uploaded.has(key) || uploaded.get(key) !== data) {
30
+ target.upload(backend, key, data);
31
+ uploaded.set(key, data);
32
+ changed = true;
33
+ }
34
+ }
35
+ for (const key of uploaded.keys()) {
36
+ if (!entries.has(key)) {
37
+ target.remove?.(backend, key);
38
+ uploaded.delete(key);
39
+ changed = true;
40
+ }
41
+ }
42
+ target.prune?.(backend, new Set(entries.keys()));
43
+ return changed;
44
+ };
45
+ }
@@ -0,0 +1,29 @@
1
+ import type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ import type { IAutorunOptions } from 'mobx';
3
+ /**
4
+ * Dev-only. Records a named reaction against the node it belongs to, so a test
5
+ * can ask for its dependency set by name. Every autorun installer in the tree
6
+ * calls this; a display's afterAttach discards the disposer, and this is the
7
+ * one place it can still be found.
8
+ */
9
+ export declare function recordNamedReaction(self: object, name: string, disposer: () => void): void;
10
+ /**
11
+ * An autorun that is disposed with `self` and answerable by name.
12
+ *
13
+ * The two halves belong together: `reactionDependencies` can only see a
14
+ * reaction that was recorded, so an installer that calls `autorun` and
15
+ * `addDisposer` directly silently opts its dependency set out of the tests that
16
+ * are supposed to pin it — and nothing fails, which is the same shape of
17
+ * quiet-hole the mechanism exists to close. Every installer goes through here
18
+ * so there is no second spelling to forget.
19
+ */
20
+ export declare function namedAutorun(self: IAnyStateTreeNode, body: () => void, options: IAutorunOptions & {
21
+ name: string;
22
+ }): import("mobx").IReactionDisposer;
23
+ /**
24
+ * The observables a named reaction currently subscribes to, as sorted leaf
25
+ * names (`Model.prop`), computeds flattened to what they read. This is the
26
+ * dependency set MobX rebuilt on the reaction's last run, so it answers, for
27
+ * that state, the question no pure function can: which reads are tracked.
28
+ */
29
+ export declare function reactionDependencies(self: object, name: string): string[];
@@ -0,0 +1,62 @@
1
+ import { addDisposer } from '@jbrowse/mobx-state-tree';
2
+ import { autorun, getDependencyTree } from 'mobx';
3
+ const registry = new WeakMap();
4
+ /**
5
+ * Dev-only. Records a named reaction against the node it belongs to, so a test
6
+ * can ask for its dependency set by name. Every autorun installer in the tree
7
+ * calls this; a display's afterAttach discards the disposer, and this is the
8
+ * one place it can still be found.
9
+ */
10
+ export function recordNamedReaction(self, name, disposer) {
11
+ if (process.env.NODE_ENV !== 'production') {
12
+ const forNode = registry.get(self);
13
+ if (forNode) {
14
+ forNode.set(name, disposer);
15
+ }
16
+ else {
17
+ registry.set(self, new Map([[name, disposer]]));
18
+ }
19
+ }
20
+ }
21
+ /**
22
+ * An autorun that is disposed with `self` and answerable by name.
23
+ *
24
+ * The two halves belong together: `reactionDependencies` can only see a
25
+ * reaction that was recorded, so an installer that calls `autorun` and
26
+ * `addDisposer` directly silently opts its dependency set out of the tests that
27
+ * are supposed to pin it — and nothing fails, which is the same shape of
28
+ * quiet-hole the mechanism exists to close. Every installer goes through here
29
+ * so there is no second spelling to forget.
30
+ */
31
+ export function namedAutorun(self, body, options) {
32
+ const disposer = autorun(body, options);
33
+ recordNamedReaction(self, options.name, disposer);
34
+ addDisposer(self, disposer);
35
+ return disposer;
36
+ }
37
+ function leaves(tree, out) {
38
+ if (tree.dependencies?.length) {
39
+ for (const d of tree.dependencies) {
40
+ leaves(d, out);
41
+ }
42
+ }
43
+ else {
44
+ out.add(tree.name);
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * The observables a named reaction currently subscribes to, as sorted leaf
50
+ * names (`Model.prop`), computeds flattened to what they read. This is the
51
+ * dependency set MobX rebuilt on the reaction's last run, so it answers, for
52
+ * that state, the question no pure function can: which reads are tracked.
53
+ */
54
+ export function reactionDependencies(self, name) {
55
+ const disposer = registry.get(self)?.get(name);
56
+ if (!disposer) {
57
+ throw new Error(`no reaction named ${name} recorded on this node; known: ${[
58
+ ...(registry.get(self)?.keys() ?? []),
59
+ ].join(', ')}`);
60
+ }
61
+ return [...leaves(getDependencyTree(disposer), new Set())].sort();
62
+ }