@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,114 @@
|
|
|
1
|
+
import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase } from './renderingBackendBase.ts';
|
|
2
|
+
import type { BlockClipResult } from './blockClipUtils.ts';
|
|
3
|
+
import type { InstancePass } from './instancePass.ts';
|
|
4
|
+
import type { RenderBlock } from './renderBlock.ts';
|
|
5
|
+
import type { FrameDimensions, RenderingBackend } from './renderingBackendBase.ts';
|
|
6
|
+
export type { FrameDimensions } from './renderingBackendBase.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Shared contract for per-region streamed GPU backends.
|
|
9
|
+
*
|
|
10
|
+
* The model owns the data (`rpcDataMap` / `laidOutDataMap`); the backend
|
|
11
|
+
* uploads what's needed to the GPU (`upload`, one region per key) and draws
|
|
12
|
+
* against a data map passed back through `renderBlocks` each frame.
|
|
13
|
+
*
|
|
14
|
+
* `UploadData` is what the upload-side autorun pushes per region;
|
|
15
|
+
* `RenderData` is what the render-side reads. They default to the same
|
|
16
|
+
* type — MAF is the lone case where they diverge (upload payload also
|
|
17
|
+
* carries a pre-encoded GPU buffer that the render side doesn't need).
|
|
18
|
+
*
|
|
19
|
+
* Renderers are stateless: they keep no per-region cache. Canvas2D backends
|
|
20
|
+
* stub `upload` / `release` as no-ops since they read everything from the
|
|
21
|
+
* `regions` map at render time. GPU backends use `upload` to push bytes into
|
|
22
|
+
* HAL buffers, and `release` to drop a departed region's buffers.
|
|
23
|
+
*/
|
|
24
|
+
export interface PerRegionRenderingBackend<UploadData, RenderState, Block = RenderBlock, RenderData = UploadData> extends RenderingBackend {
|
|
25
|
+
upload(displayedRegionIndex: number, data: UploadData): void;
|
|
26
|
+
release(displayedRegionIndex: number): void;
|
|
27
|
+
/**
|
|
28
|
+
* Paint one frame and report whether real content reached the canvas. The
|
|
29
|
+
* render callback forwards this straight to `RenderLifecycleMixin`, which
|
|
30
|
+
* flips `canvasDrawn` (the loading scrim, `data-display-drawn`, every browser
|
|
31
|
+
* test's wait) only on `true` — so a tick that drew nothing must answer
|
|
32
|
+
* `false` rather than let a display assert doneness over an empty canvas.
|
|
33
|
+
*
|
|
34
|
+
* The two bases answer at different precision on purpose. GPU knows exactly
|
|
35
|
+
* ("a `drawRegion` ran", after the region-absent and offscreen-clip guards);
|
|
36
|
+
* Canvas2D delegates clipping to the plugin's `drawXxxBlocks` and so answers
|
|
37
|
+
* "some block had region data", which is the same for every case a display
|
|
38
|
+
* can distinguish and avoids re-running `clipBlockForCanvas` purely for a
|
|
39
|
+
* predicate.
|
|
40
|
+
*/
|
|
41
|
+
renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Canvas2D-side base for `PerRegionRenderingBackend` implementations. Inherits
|
|
45
|
+
* the `canvas` + 2D context (and no-op `dispose`) from
|
|
46
|
+
* `Canvas2DRenderingBackendBase`; stubs the upload/prune hooks the backend shape
|
|
47
|
+
* requires but Canvas2D doesn't need (everything comes through `draw` from the
|
|
48
|
+
* model's data map).
|
|
49
|
+
*
|
|
50
|
+
* `renderBlocks` is concrete: it runs `prepareCanvas` (DPR-aware backing-store
|
|
51
|
+
* sizing — symmetric to the GPU base owning `resize`/scissor) and then calls
|
|
52
|
+
* the abstract `draw`. Subclasses implement only `draw`, which delegates to the
|
|
53
|
+
* plugin's pure `drawXxxBlocks` function (also shared with SVG export). Keeping
|
|
54
|
+
* `prepareCanvas` in the base means a renderer can't forget it and render
|
|
55
|
+
* blurry on hi-DPI.
|
|
56
|
+
*/
|
|
57
|
+
export declare abstract class Canvas2DPerRegionRenderingBackend<UploadData, RenderState extends FrameDimensions, Block extends RenderBlock = RenderBlock, RenderData = UploadData> extends Canvas2DRenderingBackendBase implements PerRegionRenderingBackend<UploadData, RenderState, Block, RenderData> {
|
|
58
|
+
upload(): void;
|
|
59
|
+
release(): void;
|
|
60
|
+
renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Paint the visible blocks into `this.ctx` (already DPR-prepared). Delegates
|
|
63
|
+
* to the plugin's pure `drawXxxBlocks(ctx, regions, blocks, state)` function.
|
|
64
|
+
*/
|
|
65
|
+
protected abstract draw(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* GPU-side base for `PerRegionRenderingBackend` implementations. Inherits the
|
|
69
|
+
* `hal` reference, the pre-allocated uniform scratch `ArrayBuffer`, and the
|
|
70
|
+
* HAL-delegating `dispose` from `GpuRenderingBackendBase`; adds the shared
|
|
71
|
+
* `release` (delegates to HAL).
|
|
72
|
+
*
|
|
73
|
+
* `renderBlocks` is a concrete frame scaffold shared by every per-region GPU
|
|
74
|
+
* renderer: resize the backing store, `beginFrame`/`endFrame`, and per block
|
|
75
|
+
* compute the scissor clip + set scissor/viewport. The scaffold's invariants
|
|
76
|
+
* (paired begin/end, cleared scissor/viewport, the skip-absent-region and
|
|
77
|
+
* skip-offscreen guards) are owned here so a subclass can't forget them.
|
|
78
|
+
* Subclasses implement only `drawRegion`, which writes uniforms and issues
|
|
79
|
+
* draw passes for one already-clipped block.
|
|
80
|
+
*/
|
|
81
|
+
export declare abstract class GpuPerRegionRenderingBackend<UploadData, RenderState extends FrameDimensions, Block extends RenderBlock = RenderBlock, RenderData = UploadData> extends GpuRenderingBackendBase implements PerRegionRenderingBackend<UploadData, RenderState, Block, RenderData> {
|
|
82
|
+
release(displayedRegionIndex: number): void;
|
|
83
|
+
/**
|
|
84
|
+
* The passes this backend fills from one region's payload, each carrying its
|
|
85
|
+
* own packer. Usually the same array handed to `createRenderingBackend` —
|
|
86
|
+
* minus any pass that draws off another's buffer (wiggle's line passes,
|
|
87
|
+
* canvas's chevron), which is registered but never uploaded to.
|
|
88
|
+
*/
|
|
89
|
+
protected abstract regionPasses: InstancePass<UploadData>[];
|
|
90
|
+
/**
|
|
91
|
+
* Pack and upload every pass for one region.
|
|
92
|
+
*
|
|
93
|
+
* Concrete for the same reason `renderBlocks` below is: the invariants are
|
|
94
|
+
* the base's to keep. Each subclass used to write this, and each wrote the
|
|
95
|
+
* empty case differently — `if (count === 0) deleteRegion() else upload()`
|
|
96
|
+
* five times, an unconditional leading `deleteRegion()` once. Both spellings
|
|
97
|
+
* exist because a pass whose data went empty must not keep drawing its last
|
|
98
|
+
* buffer, and `uploadPass` gets that from the HAL: an empty pack deletes that
|
|
99
|
+
* pass's buffer, so a multi-pass backend clears exactly the passes that went
|
|
100
|
+
* empty instead of dropping the whole region and rebuilding it.
|
|
101
|
+
*
|
|
102
|
+
* Override only for a payload no packer can take — none does today.
|
|
103
|
+
*/
|
|
104
|
+
upload(displayedRegionIndex: number, data: UploadData): void;
|
|
105
|
+
renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Write uniforms and issue draw passes for one block whose scissor/viewport
|
|
108
|
+
* are already set to its clipped span. `clip` carries the HP-split bp range
|
|
109
|
+
* and pixel dimensions; `region` is the model's per-region data (used by
|
|
110
|
+
* renderers that read per-region uniforms like an outline color, ignored by
|
|
111
|
+
* those whose draw state is fully in the uploaded buffer).
|
|
112
|
+
*/
|
|
113
|
+
protected abstract drawRegion(block: Block, clip: BlockClipResult, region: RenderData, state: RenderState): void;
|
|
114
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { clipBlock } from './blockClipUtils.js';
|
|
2
|
+
import { prepareCanvas } from './canvas2dUtils.js';
|
|
3
|
+
import { uploadPass } from './instancePass.js';
|
|
4
|
+
import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase, } from './renderingBackendBase.js';
|
|
5
|
+
/**
|
|
6
|
+
* Canvas2D-side base for `PerRegionRenderingBackend` implementations. Inherits
|
|
7
|
+
* the `canvas` + 2D context (and no-op `dispose`) from
|
|
8
|
+
* `Canvas2DRenderingBackendBase`; stubs the upload/prune hooks the backend shape
|
|
9
|
+
* requires but Canvas2D doesn't need (everything comes through `draw` from the
|
|
10
|
+
* model's data map).
|
|
11
|
+
*
|
|
12
|
+
* `renderBlocks` is concrete: it runs `prepareCanvas` (DPR-aware backing-store
|
|
13
|
+
* sizing — symmetric to the GPU base owning `resize`/scissor) and then calls
|
|
14
|
+
* the abstract `draw`. Subclasses implement only `draw`, which delegates to the
|
|
15
|
+
* plugin's pure `drawXxxBlocks` function (also shared with SVG export). Keeping
|
|
16
|
+
* `prepareCanvas` in the base means a renderer can't forget it and render
|
|
17
|
+
* blurry on hi-DPI.
|
|
18
|
+
*/
|
|
19
|
+
export class Canvas2DPerRegionRenderingBackend extends Canvas2DRenderingBackendBase {
|
|
20
|
+
upload() { }
|
|
21
|
+
release() { }
|
|
22
|
+
renderBlocks(blocks, regions, state) {
|
|
23
|
+
prepareCanvas(this.canvas, this.ctx, state.canvasWidth, state.canvasHeight);
|
|
24
|
+
this.draw(blocks, regions, state);
|
|
25
|
+
return blocks.some(block => regions.has(block.displayedRegionIndex));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* GPU-side base for `PerRegionRenderingBackend` implementations. Inherits the
|
|
30
|
+
* `hal` reference, the pre-allocated uniform scratch `ArrayBuffer`, and the
|
|
31
|
+
* HAL-delegating `dispose` from `GpuRenderingBackendBase`; adds the shared
|
|
32
|
+
* `release` (delegates to HAL).
|
|
33
|
+
*
|
|
34
|
+
* `renderBlocks` is a concrete frame scaffold shared by every per-region GPU
|
|
35
|
+
* renderer: resize the backing store, `beginFrame`/`endFrame`, and per block
|
|
36
|
+
* compute the scissor clip + set scissor/viewport. The scaffold's invariants
|
|
37
|
+
* (paired begin/end, cleared scissor/viewport, the skip-absent-region and
|
|
38
|
+
* skip-offscreen guards) are owned here so a subclass can't forget them.
|
|
39
|
+
* Subclasses implement only `drawRegion`, which writes uniforms and issues
|
|
40
|
+
* draw passes for one already-clipped block.
|
|
41
|
+
*/
|
|
42
|
+
export class GpuPerRegionRenderingBackend extends GpuRenderingBackendBase {
|
|
43
|
+
release(displayedRegionIndex) {
|
|
44
|
+
this.hal.deleteRegion(displayedRegionIndex);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pack and upload every pass for one region.
|
|
48
|
+
*
|
|
49
|
+
* Concrete for the same reason `renderBlocks` below is: the invariants are
|
|
50
|
+
* the base's to keep. Each subclass used to write this, and each wrote the
|
|
51
|
+
* empty case differently — `if (count === 0) deleteRegion() else upload()`
|
|
52
|
+
* five times, an unconditional leading `deleteRegion()` once. Both spellings
|
|
53
|
+
* exist because a pass whose data went empty must not keep drawing its last
|
|
54
|
+
* buffer, and `uploadPass` gets that from the HAL: an empty pack deletes that
|
|
55
|
+
* pass's buffer, so a multi-pass backend clears exactly the passes that went
|
|
56
|
+
* empty instead of dropping the whole region and rebuilding it.
|
|
57
|
+
*
|
|
58
|
+
* Override only for a payload no packer can take — none does today.
|
|
59
|
+
*/
|
|
60
|
+
upload(displayedRegionIndex, data) {
|
|
61
|
+
for (const pass of this.regionPasses) {
|
|
62
|
+
uploadPass(this.hal, displayedRegionIndex, pass, data);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
renderBlocks(blocks, regions, state) {
|
|
66
|
+
const { canvasWidth, canvasHeight } = state;
|
|
67
|
+
let painted = false;
|
|
68
|
+
// The scale the canvas ACTUALLY got, which is `getDpr()` until the backing
|
|
69
|
+
// store clamps and below it after. Read from the resize rather than the
|
|
70
|
+
// window, so a track dragged past the clamp draws at reduced resolution
|
|
71
|
+
// instead of asking for a viewport its target cannot hold.
|
|
72
|
+
const scale = this.hal.resize(canvasWidth, canvasHeight);
|
|
73
|
+
// Always pair beginFrame/endFrame so the canvas clears to transparent even
|
|
74
|
+
// when every block is skipped (e.g. all regions pruned by a density gate).
|
|
75
|
+
this.hal.beginFrame(0, 0, 0, 0);
|
|
76
|
+
for (const block of blocks) {
|
|
77
|
+
const region = regions.get(block.displayedRegionIndex);
|
|
78
|
+
if (region !== undefined) {
|
|
79
|
+
const clip = clipBlock(block, canvasWidth, canvasHeight, scale);
|
|
80
|
+
if (clip) {
|
|
81
|
+
this.hal.setScissor(clip.pxX, 0, clip.pxW, clip.pxH);
|
|
82
|
+
this.hal.setViewport(clip.pxX, 0, clip.pxW, clip.pxH);
|
|
83
|
+
this.drawRegion(block, clip, region, state);
|
|
84
|
+
painted = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.hal.clearScissor();
|
|
89
|
+
this.hal.clearViewport();
|
|
90
|
+
this.hal.endFrame();
|
|
91
|
+
return painted;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const MAX_RECOVERIES = 2;
|
|
2
|
+
export declare const RECOVERY_WINDOW_MS = 60000;
|
|
3
|
+
export type RecoveryVerdict = 'recover' | 'give-up';
|
|
4
|
+
/**
|
|
5
|
+
* The recovery budget a display spends re-initializing its rendering backend
|
|
6
|
+
* after a lost WebGL context or a lost WebGPU device.
|
|
7
|
+
*
|
|
8
|
+
* The budget is **windowed, not lifetime**. A context that resolves and then
|
|
9
|
+
* immediately re-loses has to keep climbing toward the cap — that is the flap
|
|
10
|
+
* the cap exists for, and it happens within seconds. But two unrelated losses
|
|
11
|
+
* an hour apart are not a flap, and a lifetime counter cannot tell them apart:
|
|
12
|
+
* it spends the second loss's budget on the first one's and leaves a
|
|
13
|
+
* long-running session permanently unable to auto-recover. Counting over a
|
|
14
|
+
* window separates the two without needing to know which kind of loss it saw.
|
|
15
|
+
*
|
|
16
|
+
* `now` is passed in rather than read, so the caller owns the clock and this
|
|
17
|
+
* stays testable without fake timers.
|
|
18
|
+
*/
|
|
19
|
+
export declare class RecoveryBudget {
|
|
20
|
+
private count;
|
|
21
|
+
private lastAt;
|
|
22
|
+
private readonly max;
|
|
23
|
+
private readonly windowMs;
|
|
24
|
+
constructor(max?: number, windowMs?: number);
|
|
25
|
+
/**
|
|
26
|
+
* Record a loss and say whether to recover from it. Call this **only when
|
|
27
|
+
* about to act** on the answer — it is a mutation, and calling it
|
|
28
|
+
* speculatively (this hook's recovery effect runs on every render) would
|
|
29
|
+
* refresh the window forever and the count would never lapse.
|
|
30
|
+
*/
|
|
31
|
+
record(now: number): RecoveryVerdict;
|
|
32
|
+
/** Which attempt the loss just recorded is — drives the backoff exponent. */
|
|
33
|
+
get attempt(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Back to a clean slate, for the two events that mean the trouble is over: a
|
|
36
|
+
* genuine `webglcontextrestored` and a manual Retry. A successful re-init is
|
|
37
|
+
* deliberately NOT one of them — every flap contains one.
|
|
38
|
+
*/
|
|
39
|
+
reset(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// How many recoveries a display may attempt before it stops and leaves the
|
|
2
|
+
// manual Retry button, and the window those attempts are counted over.
|
|
3
|
+
export const MAX_RECOVERIES = 2;
|
|
4
|
+
export const RECOVERY_WINDOW_MS = 60_000;
|
|
5
|
+
/**
|
|
6
|
+
* The recovery budget a display spends re-initializing its rendering backend
|
|
7
|
+
* after a lost WebGL context or a lost WebGPU device.
|
|
8
|
+
*
|
|
9
|
+
* The budget is **windowed, not lifetime**. A context that resolves and then
|
|
10
|
+
* immediately re-loses has to keep climbing toward the cap — that is the flap
|
|
11
|
+
* the cap exists for, and it happens within seconds. But two unrelated losses
|
|
12
|
+
* an hour apart are not a flap, and a lifetime counter cannot tell them apart:
|
|
13
|
+
* it spends the second loss's budget on the first one's and leaves a
|
|
14
|
+
* long-running session permanently unable to auto-recover. Counting over a
|
|
15
|
+
* window separates the two without needing to know which kind of loss it saw.
|
|
16
|
+
*
|
|
17
|
+
* `now` is passed in rather than read, so the caller owns the clock and this
|
|
18
|
+
* stays testable without fake timers.
|
|
19
|
+
*/
|
|
20
|
+
export class RecoveryBudget {
|
|
21
|
+
count = 0;
|
|
22
|
+
// "No loss yet" — any first `record` yields 1 down either branch, so nothing
|
|
23
|
+
// turns on the value, but -Infinity says it in a way a 0 read off the same
|
|
24
|
+
// clock as `now` does not.
|
|
25
|
+
lastAt = -Infinity;
|
|
26
|
+
max;
|
|
27
|
+
windowMs;
|
|
28
|
+
constructor(max = MAX_RECOVERIES, windowMs = RECOVERY_WINDOW_MS) {
|
|
29
|
+
this.max = max;
|
|
30
|
+
this.windowMs = windowMs;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Record a loss and say whether to recover from it. Call this **only when
|
|
34
|
+
* about to act** on the answer — it is a mutation, and calling it
|
|
35
|
+
* speculatively (this hook's recovery effect runs on every render) would
|
|
36
|
+
* refresh the window forever and the count would never lapse.
|
|
37
|
+
*/
|
|
38
|
+
record(now) {
|
|
39
|
+
this.count = now - this.lastAt > this.windowMs ? 1 : this.count + 1;
|
|
40
|
+
this.lastAt = now;
|
|
41
|
+
return this.count > this.max ? 'give-up' : 'recover';
|
|
42
|
+
}
|
|
43
|
+
/** Which attempt the loss just recorded is — drives the backoff exponent. */
|
|
44
|
+
get attempt() {
|
|
45
|
+
return this.count;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Back to a clean slate, for the two events that mean the trouble is over: a
|
|
49
|
+
* genuine `webglcontextrestored` and a manual Retry. A successful re-init is
|
|
50
|
+
* deliberately NOT one of them — every flap contains one.
|
|
51
|
+
*/
|
|
52
|
+
reset() {
|
|
53
|
+
this.count = 0;
|
|
54
|
+
this.lastAt = -Infinity;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ObservableMap } from 'mobx';
|
|
2
|
+
export declare function isCheckedAtTheStore(map: ReadonlyMap<unknown, unknown>): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The volatile a display keys its per-region payloads by
|
|
5
|
+
* (`displayedRegionIndex` → result). Every one of them in tree is built with
|
|
6
|
+
* this, so "how is per-region data represented" has one answer — see
|
|
7
|
+
* ADR-060.
|
|
8
|
+
*
|
|
9
|
+
* **Shallow, and that follows from an invariant the codebase already states**:
|
|
10
|
+
* per-region values are freshly constructed and never mutated (agent-docs
|
|
11
|
+
* CLAUDE.md; backends diff by reference identity). Nothing inside an entry can
|
|
12
|
+
* therefore change, so the deep enhancer's field-level atoms can never fire —
|
|
13
|
+
* they are unreachable reactivity, not a safety margin. What they do cost is
|
|
14
|
+
* paid on every insert and every read:
|
|
15
|
+
*
|
|
16
|
+
* - **Insert.** `deepEnhancer` recursively rebuilds each payload as an
|
|
17
|
+
* observable object graph — the stored value is not the object the worker
|
|
18
|
+
* produced. A multi-wiggle region is one atom per field per source, so a
|
|
19
|
+
* thousand-sample track pays ~18k on every pan; MAF pays the whole set again
|
|
20
|
+
* for every cached region on every row reorder, since `placeFetchedRows`
|
|
21
|
+
* re-places them all.
|
|
22
|
+
* - **Read.** Each field access goes through `getObservablePropValue_`. Hot
|
|
23
|
+
* loops that hoist their typed arrays are fine, but the ones that don't were
|
|
24
|
+
* paying it per iteration.
|
|
25
|
+
*
|
|
26
|
+
* Typed arrays and class instances (Flatbush) pass through the deep enhancer
|
|
27
|
+
* untouched, so the arrays were never the cost — the objects holding them were.
|
|
28
|
+
*
|
|
29
|
+
* Coarser tracking is the only behavioral difference, and it is unobservable
|
|
30
|
+
* here: an entry is only ever replaced whole, so the keys atom and the entry's
|
|
31
|
+
* own atom fire on exactly the same `.set`/`.delete`/`.clear`.
|
|
32
|
+
*
|
|
33
|
+
* Not for maps of primitives (`groupMaxHeightOverrides`,
|
|
34
|
+
* `detectedModifications`), where the enhancer is a no-op, nor for UI state
|
|
35
|
+
* whose values are mutated in place.
|
|
36
|
+
*
|
|
37
|
+
* **Dev-only, every entry is checked on the way in.** A fetch RPC answers a
|
|
38
|
+
* payload or a `regionTooLarge` refusal, and every reader of this map is
|
|
39
|
+
* written for the payload — so a value that is not one is a fetch that did not
|
|
40
|
+
* happen, stored as though it had. There is no type to catch it: `onResult`
|
|
41
|
+
* receives whatever the RPC resolved, typed as the payload it was supposed to
|
|
42
|
+
* be, and the mock in the display harness resolved `undefined` for every
|
|
43
|
+
* un-stubbed method for as long as the harness existed. Six reactions across
|
|
44
|
+
* four packages then threw `Cannot read properties of undefined` on every run,
|
|
45
|
+
* inside autoruns MobX catches and logs — invisible until the reaction gate
|
|
46
|
+
* went in.
|
|
47
|
+
*
|
|
48
|
+
* Checked HERE because this is the one constructor all of them share, so the
|
|
49
|
+
* store is one place while the readers are hundreds, and because a payload
|
|
50
|
+
* caught at the `set` names the region and the display instead of surfacing as
|
|
51
|
+
* a TypeError in whichever getter happened to read it first.
|
|
52
|
+
*
|
|
53
|
+
* **`T extends object` is the same statement as the check, in the one place a
|
|
54
|
+
* type can make it.** The runtime predicate is "a non-null object", and
|
|
55
|
+
* `object` is that predicate spelled as a constraint — so a map cannot be
|
|
56
|
+
* *declared* as holding something the check would then report, which is the
|
|
57
|
+
* only way the two could come apart. It costs nothing: every payload in tree is
|
|
58
|
+
* an interface, an array, an intersection or a class.
|
|
59
|
+
*/
|
|
60
|
+
export declare function regionDataMap<T extends object>(name: string): ObservableMap<number, T>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { observable } from 'mobx';
|
|
2
|
+
// Maps built here check themselves at the `set`, so the upload seam skips them
|
|
3
|
+
// and a bad entry is reported once, with the field named.
|
|
4
|
+
//
|
|
5
|
+
// Module state, and duplication-safe in the only way that matters here: two
|
|
6
|
+
// live copies of this package would each hold their own set, so a map built by
|
|
7
|
+
// one and installed through the other is checked twice rather than missed.
|
|
8
|
+
const checkedAtTheStore = new WeakSet();
|
|
9
|
+
export function isCheckedAtTheStore(map) {
|
|
10
|
+
return checkedAtTheStore.has(map);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The volatile a display keys its per-region payloads by
|
|
14
|
+
* (`displayedRegionIndex` → result). Every one of them in tree is built with
|
|
15
|
+
* this, so "how is per-region data represented" has one answer — see
|
|
16
|
+
* ADR-060.
|
|
17
|
+
*
|
|
18
|
+
* **Shallow, and that follows from an invariant the codebase already states**:
|
|
19
|
+
* per-region values are freshly constructed and never mutated (agent-docs
|
|
20
|
+
* CLAUDE.md; backends diff by reference identity). Nothing inside an entry can
|
|
21
|
+
* therefore change, so the deep enhancer's field-level atoms can never fire —
|
|
22
|
+
* they are unreachable reactivity, not a safety margin. What they do cost is
|
|
23
|
+
* paid on every insert and every read:
|
|
24
|
+
*
|
|
25
|
+
* - **Insert.** `deepEnhancer` recursively rebuilds each payload as an
|
|
26
|
+
* observable object graph — the stored value is not the object the worker
|
|
27
|
+
* produced. A multi-wiggle region is one atom per field per source, so a
|
|
28
|
+
* thousand-sample track pays ~18k on every pan; MAF pays the whole set again
|
|
29
|
+
* for every cached region on every row reorder, since `placeFetchedRows`
|
|
30
|
+
* re-places them all.
|
|
31
|
+
* - **Read.** Each field access goes through `getObservablePropValue_`. Hot
|
|
32
|
+
* loops that hoist their typed arrays are fine, but the ones that don't were
|
|
33
|
+
* paying it per iteration.
|
|
34
|
+
*
|
|
35
|
+
* Typed arrays and class instances (Flatbush) pass through the deep enhancer
|
|
36
|
+
* untouched, so the arrays were never the cost — the objects holding them were.
|
|
37
|
+
*
|
|
38
|
+
* Coarser tracking is the only behavioral difference, and it is unobservable
|
|
39
|
+
* here: an entry is only ever replaced whole, so the keys atom and the entry's
|
|
40
|
+
* own atom fire on exactly the same `.set`/`.delete`/`.clear`.
|
|
41
|
+
*
|
|
42
|
+
* Not for maps of primitives (`groupMaxHeightOverrides`,
|
|
43
|
+
* `detectedModifications`), where the enhancer is a no-op, nor for UI state
|
|
44
|
+
* whose values are mutated in place.
|
|
45
|
+
*
|
|
46
|
+
* **Dev-only, every entry is checked on the way in.** A fetch RPC answers a
|
|
47
|
+
* payload or a `regionTooLarge` refusal, and every reader of this map is
|
|
48
|
+
* written for the payload — so a value that is not one is a fetch that did not
|
|
49
|
+
* happen, stored as though it had. There is no type to catch it: `onResult`
|
|
50
|
+
* receives whatever the RPC resolved, typed as the payload it was supposed to
|
|
51
|
+
* be, and the mock in the display harness resolved `undefined` for every
|
|
52
|
+
* un-stubbed method for as long as the harness existed. Six reactions across
|
|
53
|
+
* four packages then threw `Cannot read properties of undefined` on every run,
|
|
54
|
+
* inside autoruns MobX catches and logs — invisible until the reaction gate
|
|
55
|
+
* went in.
|
|
56
|
+
*
|
|
57
|
+
* Checked HERE because this is the one constructor all of them share, so the
|
|
58
|
+
* store is one place while the readers are hundreds, and because a payload
|
|
59
|
+
* caught at the `set` names the region and the display instead of surfacing as
|
|
60
|
+
* a TypeError in whichever getter happened to read it first.
|
|
61
|
+
*
|
|
62
|
+
* **`T extends object` is the same statement as the check, in the one place a
|
|
63
|
+
* type can make it.** The runtime predicate is "a non-null object", and
|
|
64
|
+
* `object` is that predicate spelled as a constraint — so a map cannot be
|
|
65
|
+
* *declared* as holding something the check would then report, which is the
|
|
66
|
+
* only way the two could come apart. It costs nothing: every payload in tree is
|
|
67
|
+
* an interface, an array, an intersection or a class.
|
|
68
|
+
*/
|
|
69
|
+
export function regionDataMap(
|
|
70
|
+
// the field this map is stored on, so a violation names the map rather than
|
|
71
|
+
// the thirteen that share this constructor. No default — an omitted name is
|
|
72
|
+
// the anonymous message this parameter exists to remove, and it would be
|
|
73
|
+
// reached by forgetting rather than by deciding.
|
|
74
|
+
name) {
|
|
75
|
+
const map = observable.map(undefined, { deep: false, name });
|
|
76
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
77
|
+
checkedAtTheStore.add(map);
|
|
78
|
+
const set = map.set.bind(map);
|
|
79
|
+
map.set = (key, value) => {
|
|
80
|
+
// as `unknown`, because the claim that this is a `T` is the thing in
|
|
81
|
+
// doubt — checking it against its own declared type narrows to `never`
|
|
82
|
+
// and checks nothing
|
|
83
|
+
const stored = value;
|
|
84
|
+
if (typeof stored !== 'object' || stored === null) {
|
|
85
|
+
// `console.error`, never `throw`: the store runs inside the fetch's own
|
|
86
|
+
// result handler, where a throw is caught and reported as a failed
|
|
87
|
+
// region — which would hide the violation being reported.
|
|
88
|
+
console.error(`[jbrowse display contract] ${name}: region ${key} stored ` +
|
|
89
|
+
`\`${stored === null ? 'null' : typeof stored}\`, not a ` +
|
|
90
|
+
'payload. A fetch RPC answers a payload or a regionTooLarge ' +
|
|
91
|
+
'refusal; storing anything else leaves every reader of this map ' +
|
|
92
|
+
'reading through it.');
|
|
93
|
+
}
|
|
94
|
+
return set(key, value);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return map;
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A region's genomic span paired with its on-screen pixel span. The base
|
|
3
|
+
* geometry every backend works in: GPU clip math (`clipBlock`), Canvas2D clip
|
|
4
|
+
* (`clipBlockForCanvas`), the bp→px mapper (`makeBpMapper`), and canvas
|
|
5
|
+
* SVG/label/peptide positioning all consume this shape.
|
|
6
|
+
*/
|
|
7
|
+
export interface BpRegionBounds {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
screenStartPx: number;
|
|
11
|
+
screenEndPx: number;
|
|
12
|
+
reversed?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A `BpRegionBounds` tagged with the `displayedRegionIndex` that joins it to
|
|
16
|
+
* `rpcDataMap` / HAL buffers. `reversed` is resolved (always present) once a
|
|
17
|
+
* block reaches the render path.
|
|
18
|
+
*/
|
|
19
|
+
export interface RenderBlock extends BpRegionBounds {
|
|
20
|
+
displayedRegionIndex: number;
|
|
21
|
+
reversed: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildRenderBlocks(regions: (BpRegionBounds & {
|
|
24
|
+
displayedRegionIndex: number;
|
|
25
|
+
})[]): RenderBlock[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function buildRenderBlocks(regions) {
|
|
2
|
+
return regions.map(r => ({
|
|
3
|
+
displayedRegionIndex: r.displayedRegionIndex,
|
|
4
|
+
start: r.start,
|
|
5
|
+
end: r.end,
|
|
6
|
+
screenStartPx: r.screenStartPx,
|
|
7
|
+
screenEndPx: r.screenEndPx,
|
|
8
|
+
reversed: r.reversed ?? false,
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { GpuHal } from './hal/types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Minimum render-state shape a frame scaffold needs: the CSS-pixel canvas
|
|
4
|
+
* dimensions used to size the backing store, and — on the per-region side —
|
|
5
|
+
* to clip each block.
|
|
6
|
+
*
|
|
7
|
+
* Here rather than beside either family because both bound their render state
|
|
8
|
+
* by it: `GpuPerRegionRenderingBackend.renderBlocks` and
|
|
9
|
+
* `GpuGlobalRenderingBackend.render` open with the same `hal.resize`, and the
|
|
10
|
+
* two Canvas2D bases with the same `prepareCanvas`.
|
|
11
|
+
*/
|
|
12
|
+
export interface FrameDimensions {
|
|
13
|
+
canvasWidth: number;
|
|
14
|
+
canvasHeight: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What every rendering backend has, whatever its upload shape — the three
|
|
18
|
+
* shape contracts (`PerRegionRenderingBackend`, `GlobalRenderingBackend`,
|
|
19
|
+
* `KeyedRenderingBackend`) all extend this, and `useRenderingBackend` is
|
|
20
|
+
* bounded by it.
|
|
21
|
+
*
|
|
22
|
+
* `setErrorHandler` is here rather than optional at the hook because it was
|
|
23
|
+
* optional at the hook, and the three backends that then went without it —
|
|
24
|
+
* alignments, dotplot and multi-LGV synteny, which allocate the largest vertex
|
|
25
|
+
* buffers in the app — were the three whose over-limit allocations reached
|
|
26
|
+
* nobody: the HAL reported "too much data to render on this GPU, zoom in", the
|
|
27
|
+
* reporter's handler was null, and the view painted blank with a console line.
|
|
28
|
+
* The displays had their error banners built and wired the whole time.
|
|
29
|
+
*
|
|
30
|
+
* Extending `GpuRenderingBackendBase` or `Canvas2DRenderingBackendBase` below
|
|
31
|
+
* satisfies it, which is the intended way to get it.
|
|
32
|
+
*/
|
|
33
|
+
export interface RenderingBackend {
|
|
34
|
+
/**
|
|
35
|
+
* Route a HAL over-limit allocation to the display's `renderError`. Wired by
|
|
36
|
+
* `useRenderingBackend` once the backend is live; a no-op on Canvas2D, which
|
|
37
|
+
* allocates no GPU resources.
|
|
38
|
+
*/
|
|
39
|
+
setErrorHandler(handler: (error: Error) => void): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Shared GPU-side state for every GPU rendering backend — both per-region
|
|
44
|
+
* (`GpuPerRegionRenderingBackend`) and monolithic (`GpuGlobalRenderingBackend`).
|
|
45
|
+
* Owns the `hal` reference and a pre-allocated uniform scratch `ArrayBuffer`
|
|
46
|
+
* reused across frames to avoid per-frame GC churn. `dispose()` delegates to
|
|
47
|
+
* the HAL. Subclasses add only their upload/render shape.
|
|
48
|
+
*/
|
|
49
|
+
export declare abstract class GpuRenderingBackendBase {
|
|
50
|
+
protected hal: GpuHal;
|
|
51
|
+
protected uniformData: ArrayBuffer;
|
|
52
|
+
constructor(hal: GpuHal, uniformByteSize: number);
|
|
53
|
+
setErrorHandler(handler: (error: Error) => void): void;
|
|
54
|
+
dispose(): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Shared Canvas2D-side state for every Canvas2D rendering backend — both
|
|
58
|
+
* per-region (`Canvas2DPerRegionRenderingBackend`) and monolithic
|
|
59
|
+
* (`Canvas2DGlobalRenderingBackend`). Owns the `canvas` and its 2D context
|
|
60
|
+
* (constructor throws if unavailable). `dispose()` is a no-op since Canvas2D
|
|
61
|
+
* holds no GPU resources. Subclasses add only their render shape and stub the
|
|
62
|
+
* upload/prune hooks their interface requires (everything flows through the
|
|
63
|
+
* model's data map at render time).
|
|
64
|
+
*/
|
|
65
|
+
export declare abstract class Canvas2DRenderingBackendBase {
|
|
66
|
+
protected canvas: HTMLCanvasElement;
|
|
67
|
+
protected ctx: CanvasRenderingContext2D;
|
|
68
|
+
constructor(canvas: HTMLCanvasElement);
|
|
69
|
+
setErrorHandler(_handler: (error: Error) => void): void;
|
|
70
|
+
dispose(): void;
|
|
71
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { acquireCanvas2D } from './canvasContext.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared GPU-side state for every GPU rendering backend — both per-region
|
|
4
|
+
* (`GpuPerRegionRenderingBackend`) and monolithic (`GpuGlobalRenderingBackend`).
|
|
5
|
+
* Owns the `hal` reference and a pre-allocated uniform scratch `ArrayBuffer`
|
|
6
|
+
* reused across frames to avoid per-frame GC churn. `dispose()` delegates to
|
|
7
|
+
* the HAL. Subclasses add only their upload/render shape.
|
|
8
|
+
*/
|
|
9
|
+
export class GpuRenderingBackendBase {
|
|
10
|
+
hal;
|
|
11
|
+
uniformData;
|
|
12
|
+
constructor(hal, uniformByteSize) {
|
|
13
|
+
this.hal = hal;
|
|
14
|
+
this.uniformData = new ArrayBuffer(uniformByteSize);
|
|
15
|
+
}
|
|
16
|
+
// Forward OOM / over-limit allocation failures from the HAL to the display's
|
|
17
|
+
// renderError. Wired by useRenderingBackend once the backend is live.
|
|
18
|
+
setErrorHandler(handler) {
|
|
19
|
+
this.hal.setErrorHandler(handler);
|
|
20
|
+
}
|
|
21
|
+
dispose() {
|
|
22
|
+
this.hal.dispose();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Shared Canvas2D-side state for every Canvas2D rendering backend — both
|
|
27
|
+
* per-region (`Canvas2DPerRegionRenderingBackend`) and monolithic
|
|
28
|
+
* (`Canvas2DGlobalRenderingBackend`). Owns the `canvas` and its 2D context
|
|
29
|
+
* (constructor throws if unavailable). `dispose()` is a no-op since Canvas2D
|
|
30
|
+
* holds no GPU resources. Subclasses add only their render shape and stub the
|
|
31
|
+
* upload/prune hooks their interface requires (everything flows through the
|
|
32
|
+
* model's data map at render time).
|
|
33
|
+
*/
|
|
34
|
+
export class Canvas2DRenderingBackendBase {
|
|
35
|
+
canvas;
|
|
36
|
+
ctx;
|
|
37
|
+
constructor(canvas) {
|
|
38
|
+
// Canvas2D is the ladder's last rung, so this is where a re-init on a reused
|
|
39
|
+
// element lands — and a bare "not available" sends the reader looking for a
|
|
40
|
+
// missing browser feature instead of the committed context actually in the
|
|
41
|
+
// way. `acquireCanvas2D` names which (canvasContext.ts).
|
|
42
|
+
this.ctx = acquireCanvas2D(canvas);
|
|
43
|
+
this.canvas = canvas;
|
|
44
|
+
}
|
|
45
|
+
// Canvas2D allocates no GPU resources, so there is no OOM channel to forward.
|
|
46
|
+
// Present for symmetry with the GPU base so useRenderingBackend can wire both
|
|
47
|
+
// uniformly.
|
|
48
|
+
setErrorHandler(_handler) { }
|
|
49
|
+
dispose() { }
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// AUTO-GENERATED by packages/shader-tools/src/shader-codegen from antialias.slang.
|
|
2
|
+
// Do not edit. Run `pnpm gen:shaders` to regenerate.
|
|
3
|
+
// Scalar twins of antialias.slang, transliterated from slangc's WGSL so
|
|
4
|
+
// the Canvas2D and SVG paths run the shader's own math. See adr-051.
|
|
5
|
+
function _clamp(x, lo, hi) {
|
|
6
|
+
return _min(_max(x, lo), hi);
|
|
7
|
+
}
|
|
8
|
+
function _max(a, b) {
|
|
9
|
+
return b > a || Number.isNaN(a) ? b : a;
|
|
10
|
+
}
|
|
11
|
+
function _min(a, b) {
|
|
12
|
+
return b < a || Number.isNaN(a) ? b : a;
|
|
13
|
+
}
|
|
14
|
+
export function aaPx(dpr) {
|
|
15
|
+
return (1.0 / _max(dpr, 1.0));
|
|
16
|
+
}
|
|
17
|
+
export function aaHalfPx(dpr) {
|
|
18
|
+
return (0.5 * aaPx(dpr));
|
|
19
|
+
}
|
|
20
|
+
export function aaRamp(signedInk, widthPx) {
|
|
21
|
+
return _clamp(((signedInk / widthPx) + 0.5), 0.0, 1.0);
|
|
22
|
+
}
|
|
23
|
+
export function edgeCoverage(signedInkCssPx, dpr) {
|
|
24
|
+
return aaRamp(signedInkCssPx, aaPx(dpr));
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CAPSULE_MIN_LEN_PX = 0.0001;
|