@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.
- package/LICENSE +201 -0
- package/esm/OverlayCanvas.d.ts +29 -0
- package/esm/OverlayCanvas.js +43 -0
- package/esm/RenderCanvas.d.ts +76 -0
- package/esm/RenderCanvas.js +38 -0
- package/esm/RenderLifecycleMixin.d.ts +180 -0
- package/esm/RenderLifecycleMixin.js +279 -0
- package/esm/ScrollLockedOverlay.d.ts +28 -0
- package/esm/ScrollLockedOverlay.js +42 -0
- package/esm/blockClipUtils.d.ts +27 -0
- package/esm/blockClipUtils.js +101 -0
- package/esm/canvas2dUtils.d.ts +372 -0
- package/esm/canvas2dUtils.js +564 -0
- package/esm/canvasContext.d.ts +68 -0
- package/esm/canvasContext.js +74 -0
- package/esm/colorRampLut.d.ts +17 -0
- package/esm/colorRampLut.js +25 -0
- package/esm/coverageBand.d.ts +107 -0
- package/esm/coverageBand.js +109 -0
- package/esm/coverageBandBuffers.d.ts +30 -0
- package/esm/coverageBandBuffers.js +13 -0
- package/esm/createRenderingBackend.d.ts +52 -0
- package/esm/createRenderingBackend.js +58 -0
- package/esm/displayPhase.d.ts +155 -0
- package/esm/displayPhase.js +97 -0
- package/esm/globalRenderingBackend.d.ts +90 -0
- package/esm/globalRenderingBackend.js +53 -0
- package/esm/gpuDevice.d.ts +91 -0
- package/esm/gpuDevice.js +277 -0
- package/esm/graphicsCapabilities.d.ts +98 -0
- package/esm/graphicsCapabilities.js +165 -0
- package/esm/hal/createHal.d.ts +22 -0
- package/esm/hal/createHal.js +91 -0
- package/esm/hal/deviceGpuCache.d.ts +36 -0
- package/esm/hal/deviceGpuCache.js +120 -0
- package/esm/hal/gpuHalBase.d.ts +55 -0
- package/esm/hal/gpuHalBase.js +81 -0
- package/esm/hal/index.d.ts +7 -0
- package/esm/hal/index.js +5 -0
- package/esm/hal/mockHal.d.ts +153 -0
- package/esm/hal/mockHal.js +297 -0
- package/esm/hal/oomReporter.d.ts +15 -0
- package/esm/hal/oomReporter.js +22 -0
- package/esm/hal/passIds.d.ts +22 -0
- package/esm/hal/passIds.js +36 -0
- package/esm/hal/regionRegistry.d.ts +11 -0
- package/esm/hal/regionRegistry.js +66 -0
- package/esm/hal/types.d.ts +116 -0
- package/esm/hal/types.js +1 -0
- package/esm/hal/webgl2Hal.d.ts +47 -0
- package/esm/hal/webgl2Hal.js +490 -0
- package/esm/hal/webgpuHal.d.ts +169 -0
- package/esm/hal/webgpuHal.js +722 -0
- package/esm/installUpload.d.ts +91 -0
- package/esm/installUpload.js +94 -0
- package/esm/instanceCache.d.ts +43 -0
- package/esm/instanceCache.js +53 -0
- package/esm/instancePass.d.ts +50 -0
- package/esm/instancePass.js +34 -0
- package/esm/keyedRenderingBackend.d.ts +41 -0
- package/esm/keyedRenderingBackend.js +19 -0
- package/esm/mapUploadSync.d.ts +31 -0
- package/esm/mapUploadSync.js +45 -0
- package/esm/namedReactions.d.ts +29 -0
- package/esm/namedReactions.js +62 -0
- package/esm/perRegionRenderingBackend.d.ts +114 -0
- package/esm/perRegionRenderingBackend.js +93 -0
- package/esm/recoveryBudget.d.ts +40 -0
- package/esm/recoveryBudget.js +56 -0
- package/esm/regionDataMap.d.ts +60 -0
- package/esm/regionDataMap.js +98 -0
- package/esm/renderBlock.d.ts +25 -0
- package/esm/renderBlock.js +10 -0
- package/esm/renderingBackendBase.d.ts +71 -0
- package/esm/renderingBackendBase.js +50 -0
- package/esm/shaders/antialias.js.generated.d.ts +4 -0
- package/esm/shaders/antialias.js.generated.js +25 -0
- package/esm/shaders/capsule.generated.d.ts +1 -0
- package/esm/shaders/capsule.generated.js +3 -0
- package/esm/shaders/colorRampLut.generated.d.ts +1 -0
- package/esm/shaders/colorRampLut.generated.js +3 -0
- package/esm/shaders/coverageBand.generated.d.ts +2 -0
- package/esm/shaders/coverageBand.generated.js +4 -0
- package/esm/shaders/coverageBar.generated.d.ts +4 -0
- package/esm/shaders/coverageBar.generated.js +6 -0
- package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageBar.iface.generated.js +114 -0
- package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
- package/esm/shaders/coverageIndicator.generated.js +6 -0
- package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
- package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
- package/esm/shaders/coverageInterbase.generated.js +6 -0
- package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
- package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
- package/esm/shaders/coverageMod.generated.d.ts +4 -0
- package/esm/shaders/coverageMod.generated.js +6 -0
- package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageMod.iface.generated.js +144 -0
- package/esm/shaders/coverageSnp.generated.d.ts +4 -0
- package/esm/shaders/coverageSnp.generated.js +6 -0
- package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageSnp.iface.generated.js +144 -0
- package/esm/shaders/hpmath.js.generated.d.ts +3 -0
- package/esm/shaders/hpmath.js.generated.js +48 -0
- package/esm/shaders/pointGlyph.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.generated.js +3 -0
- package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.js.generated.js +7 -0
- package/esm/shaders/rowRect.generated.d.ts +3 -0
- package/esm/shaders/rowRect.generated.js +5 -0
- package/esm/shaders/rowRect.js.generated.d.ts +2 -0
- package/esm/shaders/rowRect.js.generated.js +13 -0
- package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
- package/esm/shaders/scoreScale.js.generated.js +46 -0
- package/esm/slangPass.d.ts +49 -0
- package/esm/slangPass.js +22 -0
- package/esm/useEventCallback.d.ts +1 -0
- package/esm/useEventCallback.js +19 -0
- package/esm/useGraphicsCapabilities.d.ts +17 -0
- package/esm/useGraphicsCapabilities.js +38 -0
- package/esm/useRenderingBackend.d.ts +83 -0
- package/esm/useRenderingBackend.js +422 -0
- package/esm/useTabVisibilityRerender.d.ts +13 -0
- package/esm/useTabVisibilityRerender.js +39 -0
- package/esm/webgpuUtils.d.ts +23 -0
- package/esm/webgpuUtils.js +76 -0
- package/package.json +91 -0
- package/src/shaders/antialias.js.generated.ts +33 -0
- package/src/shaders/antialias.slang +112 -0
- package/src/shaders/capsule.generated.ts +4 -0
- package/src/shaders/capsule.slang +62 -0
- package/src/shaders/colorPack.slang +26 -0
- package/src/shaders/colorRampLut.generated.ts +4 -0
- package/src/shaders/colorRampLut.slang +51 -0
- package/src/shaders/coverageBand.generated.ts +6 -0
- package/src/shaders/coverageBand.slang +276 -0
- package/src/shaders/coverageBar.generated.ts +10 -0
- package/src/shaders/coverageBar.iface.generated.ts +172 -0
- package/src/shaders/coverageBar.slang +37 -0
- package/src/shaders/coverageIndicator.generated.ts +10 -0
- package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
- package/src/shaders/coverageIndicator.slang +61 -0
- package/src/shaders/coverageInterbase.generated.ts +10 -0
- package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
- package/src/shaders/coverageInterbase.slang +63 -0
- package/src/shaders/coverageMod.generated.ts +10 -0
- package/src/shaders/coverageMod.iface.generated.ts +211 -0
- package/src/shaders/coverageMod.slang +36 -0
- package/src/shaders/coverageSnp.generated.ts +10 -0
- package/src/shaders/coverageSnp.iface.generated.ts +211 -0
- package/src/shaders/coverageSnp.slang +52 -0
- package/src/shaders/diagonalGrid.slang +25 -0
- package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
- package/src/shaders/hpmath.js.generated.ts +47 -0
- package/src/shaders/hpmath.slang +226 -0
- package/src/shaders/pointGlyph.generated.ts +4 -0
- package/src/shaders/pointGlyph.js.generated.ts +9 -0
- package/src/shaders/pointGlyph.slang +93 -0
- package/src/shaders/rowRect.generated.ts +8 -0
- package/src/shaders/rowRect.js.generated.ts +17 -0
- package/src/shaders/rowRect.slang +160 -0
- package/src/shaders/scoreScale.js.generated.ts +51 -0
- 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
|
+
}
|