@jbrowse/render-core 5.0.0-beta.4 → 5.0.0-beta.7
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/esm/RenderLifecycleMixin.d.ts +1 -0
- package/esm/RenderLifecycleMixin.js +8 -0
- package/esm/gpuContextLostError.d.ts +30 -0
- package/esm/gpuContextLostError.js +40 -0
- package/esm/gpuDevice.js +4 -1
- package/esm/hal/gpuHalBase.d.ts +3 -3
- package/esm/hal/index.d.ts +1 -1
- package/esm/hal/mockHal.d.ts +3 -3
- package/esm/hal/mockHal.js +3 -2
- package/esm/hal/oomReporter.d.ts +7 -0
- package/esm/hal/oomReporter.js +10 -0
- package/esm/hal/types.d.ts +8 -1
- package/esm/hal/webgl2Hal.d.ts +2 -2
- package/esm/hal/webgl2Hal.js +8 -1
- package/esm/hal/webgpuHal.d.ts +12 -2
- package/esm/hal/webgpuHal.js +43 -5
- package/esm/marks/barMark.d.ts +28 -0
- package/esm/marks/barMark.js +63 -0
- package/esm/marks/drawAgainstHit.d.ts +85 -14
- package/esm/marks/drawAgainstHit.js +260 -38
- package/esm/marks/glyphPaint.d.ts +10 -2
- package/esm/marks/glyphPaint.js +21 -0
- package/esm/marks/index.d.ts +9 -2
- package/esm/marks/index.js +4 -1
- package/esm/marks/markBackend.d.ts +14 -2
- package/esm/marks/markBackend.js +43 -13
- package/esm/marks/markHit.d.ts +8 -1
- package/esm/marks/markHit.js +15 -0
- package/esm/marks/markInk.d.ts +14 -0
- package/esm/marks/markInk.js +34 -0
- package/esm/marks/markPlan.d.ts +25 -0
- package/esm/marks/markPlan.js +20 -0
- package/esm/marks/markRamp.d.ts +43 -0
- package/esm/marks/markRamp.js +68 -0
- package/esm/marks/pointMark.d.ts +8 -4
- package/esm/marks/pointMark.js +32 -9
- package/esm/marks/spanMark.d.ts +8 -0
- package/esm/marks/spanMark.js +19 -14
- package/esm/marks/types.d.ts +90 -3
- package/esm/marks/types.js +56 -4
- package/esm/shaders/barMark.generated.d.ts +4 -0
- package/esm/shaders/barMark.generated.js +6 -0
- package/esm/shaders/barMark.iface.generated.d.ts +64 -0
- package/esm/shaders/barMark.iface.generated.js +110 -0
- package/esm/shaders/markColor.generated.d.ts +3 -0
- package/esm/shaders/markColor.generated.js +5 -0
- package/esm/shaders/pointMark.generated.d.ts +3 -3
- package/esm/shaders/pointMark.generated.js +3 -3
- package/esm/shaders/pointMark.iface.generated.d.ts +17 -6
- package/esm/shaders/pointMark.iface.generated.js +27 -9
- package/esm/shaders/pointMark.js.generated.d.ts +1 -0
- package/esm/shaders/pointMark.js.generated.js +39 -0
- package/esm/shaders/scoreScale.generated.d.ts +2 -0
- package/esm/shaders/scoreScale.generated.js +4 -0
- package/esm/useRenderingBackend.d.ts +1 -16
- package/esm/useRenderingBackend.js +2 -24
- package/package.json +4 -2
- package/src/shaders/barMark.generated.ts +10 -0
- package/src/shaders/barMark.iface.generated.ts +160 -0
- package/src/shaders/barMark.slang +109 -0
- package/src/shaders/markColor.generated.ts +8 -0
- package/src/shaders/markColor.slang +47 -0
- package/src/shaders/pointMark.generated.ts +3 -3
- package/src/shaders/pointMark.iface.generated.ts +34 -10
- package/src/shaders/pointMark.js.generated.ts +41 -0
- package/src/shaders/pointMark.slang +15 -26
- package/src/shaders/scoreScale.generated.ts +6 -0
- package/src/shaders/scoreScale.slang +1 -0
- package/src/shaders/valueScale.slang +52 -0
|
@@ -64,6 +64,7 @@ export interface RenderingBackendCallbacks<B> {
|
|
|
64
64
|
*/
|
|
65
65
|
export declare function RenderLifecycleMixin(): import("@jbrowse/mobx-state-tree").IModelType<{}, {
|
|
66
66
|
canvasDrawn: boolean;
|
|
67
|
+
paintCount: number;
|
|
67
68
|
currentRenderingBackend: unknown;
|
|
68
69
|
renderTick: number;
|
|
69
70
|
autorunsInstalled: boolean;
|
|
@@ -45,6 +45,13 @@ export function RenderLifecycleMixin() {
|
|
|
45
45
|
* flips true on first paint; read by test selectors to detect render
|
|
46
46
|
*/
|
|
47
47
|
canvasDrawn: false,
|
|
48
|
+
/**
|
|
49
|
+
* #volatile
|
|
50
|
+
* bumped after every frame the backend painted, so a consumer that
|
|
51
|
+
* reads this display's canvas — the circular view's ring, which copies
|
|
52
|
+
* the strip into a texture — knows when the pixels moved
|
|
53
|
+
*/
|
|
54
|
+
paintCount: 0,
|
|
48
55
|
/**
|
|
49
56
|
* #volatile
|
|
50
57
|
* current backend reference, updated on context-loss recovery.
|
|
@@ -188,6 +195,7 @@ export function RenderLifecycleMixin() {
|
|
|
188
195
|
* #action
|
|
189
196
|
*/
|
|
190
197
|
markCanvasDrawn() {
|
|
198
|
+
self.paintCount += 1;
|
|
191
199
|
if (!self.canvasDrawn) {
|
|
192
200
|
self.canvasDrawn = true;
|
|
193
201
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flagged rather than matched by message or `instanceof`, so the error UI can
|
|
3
|
+
* offer the remedy specific to a loss (switch the page to Canvas2D) without
|
|
4
|
+
* offering it for render errors whose remedy differs (an over-allocation says to
|
|
5
|
+
* zoom in).
|
|
6
|
+
*
|
|
7
|
+
* A lost WebGPU device carries the same flag deliberately: the two causes
|
|
8
|
+
* differ, but the remedy on offer — take the page off the GPU — is the same one.
|
|
9
|
+
*
|
|
10
|
+
* React-free, and split out of `useRenderingBackend` for that reason: the HAL
|
|
11
|
+
* raises the surface error below and must not import a hook to do it.
|
|
12
|
+
* `useRenderingBackend` re-exports these, so every existing import site keeps
|
|
13
|
+
* working.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createGpuContextLostError(message?: string): Error & {
|
|
16
|
+
gpuContextLost: true;
|
|
17
|
+
};
|
|
18
|
+
export declare function createGpuDeviceLostError(): Error & {
|
|
19
|
+
gpuContextLost: true;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A canvas whose swap-chain image the driver refuses. Distinct from a device
|
|
23
|
+
* loss, because the device is fine and still renders offscreen, and distinct
|
|
24
|
+
* from an over-limit allocation, because zooming in does not fix it. Canvas2D
|
|
25
|
+
* does, which is why this carries the flag that puts that button on the banner.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createGpuSurfaceLostError(detail: string): Error & {
|
|
28
|
+
gpuContextLost: true;
|
|
29
|
+
};
|
|
30
|
+
export declare function isGpuContextLostError(error: unknown): error is object & Record<"gpuContextLost", unknown>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const CONTEXT_LOST_MESSAGE = 'WebGL context lost. The browser reclaimed the GPU context for this display, ' +
|
|
2
|
+
'usually because too many GPU-rendered views are open at once.';
|
|
3
|
+
const DEVICE_LOST_MESSAGE = 'WebGPU device lost. The GPU device backing this display went away and kept ' +
|
|
4
|
+
'going away after several attempts to rebuild on a fresh one.';
|
|
5
|
+
const SURFACE_LOST_MESSAGE = 'WebGPU could not give this display a drawable surface. The GPU driver ' +
|
|
6
|
+
'rejected the canvas image, so every frame this display submits is discarded.';
|
|
7
|
+
/**
|
|
8
|
+
* Flagged rather than matched by message or `instanceof`, so the error UI can
|
|
9
|
+
* offer the remedy specific to a loss (switch the page to Canvas2D) without
|
|
10
|
+
* offering it for render errors whose remedy differs (an over-allocation says to
|
|
11
|
+
* zoom in).
|
|
12
|
+
*
|
|
13
|
+
* A lost WebGPU device carries the same flag deliberately: the two causes
|
|
14
|
+
* differ, but the remedy on offer — take the page off the GPU — is the same one.
|
|
15
|
+
*
|
|
16
|
+
* React-free, and split out of `useRenderingBackend` for that reason: the HAL
|
|
17
|
+
* raises the surface error below and must not import a hook to do it.
|
|
18
|
+
* `useRenderingBackend` re-exports these, so every existing import site keeps
|
|
19
|
+
* working.
|
|
20
|
+
*/
|
|
21
|
+
export function createGpuContextLostError(message = CONTEXT_LOST_MESSAGE) {
|
|
22
|
+
return Object.assign(new Error(message), {
|
|
23
|
+
gpuContextLost: true,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function createGpuDeviceLostError() {
|
|
27
|
+
return createGpuContextLostError(DEVICE_LOST_MESSAGE);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A canvas whose swap-chain image the driver refuses. Distinct from a device
|
|
31
|
+
* loss, because the device is fine and still renders offscreen, and distinct
|
|
32
|
+
* from an over-limit allocation, because zooming in does not fix it. Canvas2D
|
|
33
|
+
* does, which is why this carries the flag that puts that button on the banner.
|
|
34
|
+
*/
|
|
35
|
+
export function createGpuSurfaceLostError(detail) {
|
|
36
|
+
return createGpuContextLostError(`${SURFACE_LOST_MESSAGE} (${detail})`);
|
|
37
|
+
}
|
|
38
|
+
export function isGpuContextLostError(error) {
|
|
39
|
+
return (typeof error === 'object' && error !== null && 'gpuContextLost' in error);
|
|
40
|
+
}
|
package/esm/gpuDevice.js
CHANGED
|
@@ -154,8 +154,11 @@ async function createDevice() {
|
|
|
154
154
|
// Surface any WebGPU validation / out-of-memory / internal errors that
|
|
155
155
|
// would otherwise be silently swallowed. Without this, a bad draw/pipeline
|
|
156
156
|
// results in a blank canvas with no console output.
|
|
157
|
+
// The constructor name, because the message alone does not say whether a
|
|
158
|
+
// failure is validation, out-of-memory or internal — and that is what
|
|
159
|
+
// decides which `pushErrorScope` filter could have caught it.
|
|
157
160
|
d.addEventListener('uncapturederror', (event) => {
|
|
158
|
-
console.error(
|
|
161
|
+
console.error(`[GPU] UNCAPTURED ${event.error.constructor.name}:`, event.error.message);
|
|
159
162
|
});
|
|
160
163
|
void d.lost.then(info => {
|
|
161
164
|
// Identity check: if the shared device has already been replaced (test
|
package/esm/hal/gpuHalBase.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OomReporter } from './oomReporter.ts';
|
|
2
2
|
import { RegionRegistry } from './regionRegistry.ts';
|
|
3
|
-
import type { PipelineDescriptor, TextureBinding } from './types.ts';
|
|
3
|
+
import type { PipelineDescriptor, TextureBinding, TextureSource } from './types.ts';
|
|
4
4
|
/**
|
|
5
5
|
* The device ceilings the shared upload shells refuse past. WebGPU asks the
|
|
6
6
|
* device, WebGL2 has no such parameter for buffers and carries its own constant
|
|
@@ -42,7 +42,7 @@ export declare abstract class GpuHalBase<Buf extends {
|
|
|
42
42
|
* reached for a pass that declares a binding, and only within
|
|
43
43
|
* `maxTextureDimensionPx`.
|
|
44
44
|
*/
|
|
45
|
-
protected abstract createTexture(passId: string, binding: TextureBinding, data:
|
|
45
|
+
protected abstract createTexture(passId: string, binding: TextureBinding, data: TextureSource, width: number, height: number): void;
|
|
46
46
|
/** Release this HAL's own GPU objects. Runs exactly once, from `dispose`. */
|
|
47
47
|
protected abstract releaseResources(): void;
|
|
48
48
|
setErrorHandler(handler: (error: Error) => void): void;
|
|
@@ -50,6 +50,6 @@ export declare abstract class GpuHalBase<Buf extends {
|
|
|
50
50
|
getBufferCount(regionKey: number, passId: string): number;
|
|
51
51
|
deleteBuffer(regionKey: number, passId: string): void;
|
|
52
52
|
deleteRegion(regionKey: number): void;
|
|
53
|
-
uploadTexture(passId: string, data:
|
|
53
|
+
uploadTexture(passId: string, data: TextureSource, width: number, height: number): void;
|
|
54
54
|
dispose(): void;
|
|
55
55
|
}
|
package/esm/hal/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { assertUniquePassIds } from './passIds.ts';
|
|
|
4
4
|
export { WebGL2Hal } from './webgl2Hal.ts';
|
|
5
5
|
export { WebGPUHal } from './webgpuHal.ts';
|
|
6
6
|
export type { GpuHalOptions } from './createHal.ts';
|
|
7
|
-
export type { BlendState, VertexAttributeLayout, GpuHal, PipelineDescriptor, SampleCount, ShaderBinding, TextureBinding, } from './types.ts';
|
|
7
|
+
export type { BlendState, VertexAttributeLayout, GpuHal, PipelineDescriptor, SampleCount, ShaderBinding, TextureBinding, TextureSource, } from './types.ts';
|
package/esm/hal/mockHal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GpuHalBase } from './gpuHalBase.ts';
|
|
2
|
-
import type { GpuHal, PipelineDescriptor, TextureBinding } from './types.ts';
|
|
2
|
+
import type { GpuHal, PipelineDescriptor, TextureBinding, TextureSource } from './types.ts';
|
|
3
3
|
export interface MockCall {
|
|
4
4
|
method: string;
|
|
5
5
|
args: unknown[];
|
|
@@ -90,7 +90,7 @@ export declare class MockHal extends GpuHalBase<MockBuffer> implements GpuHal {
|
|
|
90
90
|
};
|
|
91
91
|
protected destroyBuffer(): void;
|
|
92
92
|
private textures;
|
|
93
|
-
protected createTexture(passId: string, _binding: TextureBinding, data:
|
|
93
|
+
protected createTexture(passId: string, _binding: TextureBinding, data: TextureSource, width: number, height: number): void;
|
|
94
94
|
protected releaseResources(): void;
|
|
95
95
|
private record;
|
|
96
96
|
errorHandler: ((error: Error) => void) | null;
|
|
@@ -120,7 +120,7 @@ export declare class MockHal extends GpuHalBase<MockBuffer> implements GpuHal {
|
|
|
120
120
|
getUniformWritesF32(): Float32Array<ArrayBuffer>[];
|
|
121
121
|
getUniformWritesU32(): Uint32Array<ArrayBuffer>[];
|
|
122
122
|
getBuffer(regionKey: number, passId: string): MockBuffer | undefined;
|
|
123
|
-
getTexture(passId: string):
|
|
123
|
+
getTexture(passId: string): TextureSource | undefined;
|
|
124
124
|
callsOf(method: string): MockCall[];
|
|
125
125
|
/**
|
|
126
126
|
* Every draw of the frame with the state it went out under, in order — the
|
package/esm/hal/mockHal.js
CHANGED
|
@@ -77,8 +77,9 @@ export class MockHal extends GpuHalBase {
|
|
|
77
77
|
// samples needs the bytes, not the byteLength the call log records.
|
|
78
78
|
textures = new Map();
|
|
79
79
|
createTexture(passId, _binding, data, width, height) {
|
|
80
|
-
|
|
81
|
-
this.
|
|
80
|
+
const bytes = data instanceof Uint8Array;
|
|
81
|
+
this.textures.set(passId, bytes ? data.slice() : data);
|
|
82
|
+
this.record('uploadTexture', passId, bytes ? data.byteLength : 'canvas', width, height);
|
|
82
83
|
}
|
|
83
84
|
releaseResources() {
|
|
84
85
|
this.record('dispose');
|
package/esm/hal/oomReporter.d.ts
CHANGED
|
@@ -12,4 +12,11 @@ export declare class OomReporter {
|
|
|
12
12
|
constructor(hal: string);
|
|
13
13
|
setHandler(handler: (error: Error) => void): void;
|
|
14
14
|
report(message: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Report an error the HAL has already classified, for a failure whose remedy
|
|
17
|
+
* is not `report`'s "zoom in". Kept separate so the classification stays with
|
|
18
|
+
* the caller that knows it — flagging every over-limit allocation as a context
|
|
19
|
+
* loss would offer "Use Canvas2D" for a view that only needs zooming.
|
|
20
|
+
*/
|
|
21
|
+
reportClassified(error: Error): void;
|
|
15
22
|
}
|
package/esm/hal/oomReporter.js
CHANGED
|
@@ -19,4 +19,14 @@ export class OomReporter {
|
|
|
19
19
|
console.error(`[${this.hal}] ${message}`);
|
|
20
20
|
this.handler?.(new Error(message));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Report an error the HAL has already classified, for a failure whose remedy
|
|
24
|
+
* is not `report`'s "zoom in". Kept separate so the classification stays with
|
|
25
|
+
* the caller that knows it — flagging every over-limit allocation as a context
|
|
26
|
+
* loss would offer "Use Canvas2D" for a view that only needs zooming.
|
|
27
|
+
*/
|
|
28
|
+
reportClassified(error) {
|
|
29
|
+
console.error(`[${this.hal}] ${error.message}`);
|
|
30
|
+
this.handler?.(error);
|
|
31
|
+
}
|
|
22
32
|
}
|
package/esm/hal/types.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface ShaderBinding {
|
|
|
55
55
|
kind: 'uniform' | 'texture' | 'sampler' | 'storage' | 'read-only-storage';
|
|
56
56
|
name: string;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* What a pass's texture is filled from: RGBA bytes, or a canvas whose pixels
|
|
60
|
+
* are copied on the GPU — the circular view's ring samples a linear display's
|
|
61
|
+
* finished strip that way. A canvas source lands premultiplied, so a shader
|
|
62
|
+
* reading one returns its sample under `//! blend: premultiplied`.
|
|
63
|
+
*/
|
|
64
|
+
export type TextureSource = Uint8Array | HTMLCanvasElement | OffscreenCanvas;
|
|
58
65
|
export interface TextureBinding {
|
|
59
66
|
textureBinding: number;
|
|
60
67
|
samplerBinding: number;
|
|
@@ -106,7 +113,7 @@ export interface GpuHal {
|
|
|
106
113
|
getBufferCount(regionKey: number, passId: string): number;
|
|
107
114
|
deleteBuffer(regionKey: number, passId: string): void;
|
|
108
115
|
deleteRegion(regionKey: number): void;
|
|
109
|
-
uploadTexture(passId: string, data:
|
|
116
|
+
uploadTexture(passId: string, data: TextureSource, width: number, height: number): void;
|
|
110
117
|
writeUniforms(data: ArrayBuffer): void;
|
|
111
118
|
beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
|
|
112
119
|
drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
|
package/esm/hal/webgl2Hal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GpuHalBase } from './gpuHalBase.ts';
|
|
2
|
-
import type { GpuHal, PipelineDescriptor, TextureBinding } from './types.ts';
|
|
2
|
+
import type { GpuHal, PipelineDescriptor, TextureBinding, TextureSource } from './types.ts';
|
|
3
3
|
interface RegionPassBuffer {
|
|
4
4
|
vbo: WebGLBuffer;
|
|
5
5
|
count: number;
|
|
@@ -30,7 +30,7 @@ export declare class WebGL2Hal extends GpuHalBase<RegionPassBuffer> implements G
|
|
|
30
30
|
private compilePass;
|
|
31
31
|
private getPass;
|
|
32
32
|
resize(width: number, height: number): import("./types.ts").CanvasScale;
|
|
33
|
-
protected createTexture(passId: string, binding: TextureBinding, data:
|
|
33
|
+
protected createTexture(passId: string, binding: TextureBinding, data: TextureSource, width: number, height: number): void;
|
|
34
34
|
writeUniforms(data: ArrayBuffer): void;
|
|
35
35
|
beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
|
|
36
36
|
setScissor(x: number, y: number, w: number, h: number): void;
|
package/esm/hal/webgl2Hal.js
CHANGED
|
@@ -324,7 +324,14 @@ export class WebGL2Hal extends GpuHalBase {
|
|
|
324
324
|
}
|
|
325
325
|
const tex = gl.createTexture();
|
|
326
326
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
327
|
-
|
|
327
|
+
if (data instanceof Uint8Array) {
|
|
328
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
332
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
333
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
334
|
+
}
|
|
328
335
|
const filter = binding.filter === 'linear' ? gl.LINEAR : gl.NEAREST;
|
|
329
336
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
330
337
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
package/esm/hal/webgpuHal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GpuHalBase } from './gpuHalBase.ts';
|
|
2
|
-
import type { GpuHal, PipelineDescriptor, SampleCount, TextureBinding } from './types.ts';
|
|
2
|
+
import type { GpuHal, PipelineDescriptor, SampleCount, TextureBinding, TextureSource } from './types.ts';
|
|
3
3
|
interface RegionPassBuffer {
|
|
4
4
|
dataBuffer: GPUBuffer;
|
|
5
5
|
count: number;
|
|
@@ -47,6 +47,7 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
|
|
|
47
47
|
private appliedViewport;
|
|
48
48
|
private warnedSwapChainLoss;
|
|
49
49
|
private swapChainUnrecoverable;
|
|
50
|
+
private reportedSurfaceFailure;
|
|
50
51
|
private constructor();
|
|
51
52
|
protected limits(): {
|
|
52
53
|
maxBufferBytes: number;
|
|
@@ -97,7 +98,7 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
|
|
|
97
98
|
*/
|
|
98
99
|
private destroyWhenIdle;
|
|
99
100
|
private drainPendingDestroy;
|
|
100
|
-
protected createTexture(passId: string, binding: TextureBinding, data:
|
|
101
|
+
protected createTexture(passId: string, binding: TextureBinding, data: TextureSource, width: number, height: number): void;
|
|
101
102
|
writeUniforms(data: ArrayBuffer): void;
|
|
102
103
|
/**
|
|
103
104
|
* The canvas texture to draw this frame into, or null when the swap chain is
|
|
@@ -120,6 +121,15 @@ export declare class WebGPUHal extends GpuHalBase<RegionPassBuffer> implements G
|
|
|
120
121
|
* Retry is what builds a fresh HAL.
|
|
121
122
|
*/
|
|
122
123
|
private acquireTextureView;
|
|
124
|
+
/**
|
|
125
|
+
* Once per HAL, not once per frame: the refusal holds for every frame at this
|
|
126
|
+
* canvas size, and dotplot and synteny keep their canvas mounted through a
|
|
127
|
+
* `renderError`, so a per-frame report there never stops.
|
|
128
|
+
*
|
|
129
|
+
* Not terminal — a refusal tracks the canvas dimensions, so a resize can clear
|
|
130
|
+
* it. `swapChainUnrecoverable` stays for the case it was written for.
|
|
131
|
+
*/
|
|
132
|
+
private reportSurfaceFailure;
|
|
123
133
|
beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
|
|
124
134
|
drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
|
|
125
135
|
/**
|
package/esm/hal/webgpuHal.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="@webgpu/types" />
|
|
2
2
|
import { syncCanvasSize } from '../canvas2dUtils.js';
|
|
3
3
|
import { canvasConfiguredBy, canvasContextError, noteCanvasConfigured, noteCanvasContext, } from '../canvasContext.js';
|
|
4
|
+
import { createGpuSurfaceLostError } from '../gpuContextLostError.js';
|
|
4
5
|
import { getGpuDevice } from '../gpuDevice.js';
|
|
5
6
|
import { STANDARD_BLEND_STATE, createUniformOnlyBindGroup, createVertexBuffer, toGpuVertexFormat, } from '../webgpuUtils.js';
|
|
6
7
|
import { getDeviceLayouts, getOrBuildPipeline } from './deviceGpuCache.js';
|
|
@@ -198,6 +199,7 @@ export class WebGPUHal extends GpuHalBase {
|
|
|
198
199
|
// than once per frame, and a reconfigure that failed is not attempted again.
|
|
199
200
|
warnedSwapChainLoss = false;
|
|
200
201
|
swapChainUnrecoverable = false;
|
|
202
|
+
reportedSurfaceFailure = false;
|
|
201
203
|
constructor(device, canvas, context, descriptors, sampleCount, pipelines, layouts) {
|
|
202
204
|
super(descriptors, 'WebGPUHal');
|
|
203
205
|
this.device = device;
|
|
@@ -383,12 +385,20 @@ export class WebGPUHal extends GpuHalBase {
|
|
|
383
385
|
// safe.
|
|
384
386
|
this.destroyWhenIdle(existing.texture);
|
|
385
387
|
}
|
|
388
|
+
const bytes = data instanceof Uint8Array;
|
|
386
389
|
const texture = this.device.createTexture({
|
|
387
390
|
size: [width, height],
|
|
388
391
|
format: 'rgba8unorm',
|
|
389
|
-
usage: GPUTextureUsage.TEXTURE_BINDING |
|
|
392
|
+
usage: GPUTextureUsage.TEXTURE_BINDING |
|
|
393
|
+
GPUTextureUsage.COPY_DST |
|
|
394
|
+
(bytes ? 0 : GPUTextureUsage.RENDER_ATTACHMENT),
|
|
390
395
|
});
|
|
391
|
-
|
|
396
|
+
if (bytes) {
|
|
397
|
+
this.device.queue.writeTexture({ texture }, data, { bytesPerRow: width * 4 }, { width, height });
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.device.queue.copyExternalImageToTexture({ source: data }, { texture, premultipliedAlpha: true }, { width, height });
|
|
401
|
+
}
|
|
392
402
|
const sampler = this.device.createSampler({
|
|
393
403
|
magFilter: binding.filter,
|
|
394
404
|
minFilter: binding.filter,
|
|
@@ -474,6 +484,21 @@ export class WebGPUHal extends GpuHalBase {
|
|
|
474
484
|
}
|
|
475
485
|
}
|
|
476
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Once per HAL, not once per frame: the refusal holds for every frame at this
|
|
489
|
+
* canvas size, and dotplot and synteny keep their canvas mounted through a
|
|
490
|
+
* `renderError`, so a per-frame report there never stops.
|
|
491
|
+
*
|
|
492
|
+
* Not terminal — a refusal tracks the canvas dimensions, so a resize can clear
|
|
493
|
+
* it. `swapChainUnrecoverable` stays for the case it was written for.
|
|
494
|
+
*/
|
|
495
|
+
reportSurfaceFailure(message) {
|
|
496
|
+
if (this.reportedSurfaceFailure) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
this.reportedSurfaceFailure = true;
|
|
500
|
+
this.oom.reportClassified(createGpuSurfaceLostError(message));
|
|
501
|
+
}
|
|
477
502
|
beginFrame(clearR, clearG, clearB, clearA = 1) {
|
|
478
503
|
// Skip the frame entirely rather than encode one that cannot be valid.
|
|
479
504
|
// Zero-size canvas: nothing to draw. Missing MSAA target while MSAA is
|
|
@@ -489,10 +514,23 @@ export class WebGPUHal extends GpuHalBase {
|
|
|
489
514
|
(this.sampleCount > 1 && !this.msaaView)) {
|
|
490
515
|
return;
|
|
491
516
|
}
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
517
|
+
// A driver can refuse the canvas image without throwing — Dawn returns an
|
|
518
|
+
// error texture and an error view, and the frame encodes against it — so the
|
|
519
|
+
// acquisition needs a scope, and its own rather than the frame's: those stay
|
|
520
|
+
// pushed iff an encoder is created, and one left on the device's stack
|
|
521
|
+
// swallows every later frame's errors. Popped before the guard below, so
|
|
522
|
+
// every path balances. Awaiting the pop here would suspend across a
|
|
523
|
+
// GPU-process round trip and destroy the canvas texture mid-frame.
|
|
524
|
+
this.device.pushErrorScope('validation');
|
|
495
525
|
const textureView = this.acquireTextureView();
|
|
526
|
+
void this.device
|
|
527
|
+
.popErrorScope()
|
|
528
|
+
.then(err => {
|
|
529
|
+
if (err) {
|
|
530
|
+
this.reportSurfaceFailure(err.message);
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
.catch(() => { });
|
|
496
534
|
if (!textureView) {
|
|
497
535
|
return;
|
|
498
536
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ColorChannel } from './markRamp.ts';
|
|
2
|
+
import type { MarkRamp, MarkShape, MarkValueScaleType } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The `bar` shape's channels: a rectangle from `x` to `x2` standing between
|
|
5
|
+
* the baseline and `y` on the value scale.
|
|
6
|
+
*/
|
|
7
|
+
export interface BarChannels extends ColorChannel {
|
|
8
|
+
x: Uint32Array;
|
|
9
|
+
x2: Uint32Array;
|
|
10
|
+
y: Float32Array;
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BarParams {
|
|
14
|
+
/** `[min, max]` `y` and `origin` are read through. */
|
|
15
|
+
domain: [number, number];
|
|
16
|
+
/** How that domain is read; linear when absent. */
|
|
17
|
+
scaleType?: MarkValueScaleType;
|
|
18
|
+
/** The quantitative colour scale, for a bar whose colour is a ramp. */
|
|
19
|
+
ramp?: MarkRamp;
|
|
20
|
+
/** The value bars grow from; a bar below it hangs down. */
|
|
21
|
+
origin: number;
|
|
22
|
+
/**
|
|
23
|
+
* Narrowest a bar is painted, in CSS px, grown off the start edge. Zero is
|
|
24
|
+
* a no-op for a caller whose bars tile.
|
|
25
|
+
*/
|
|
26
|
+
minWidthPx: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const barMark: MarkShape<BarChannels, BarParams>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { bpRangeXTuple } from '../blockClipUtils.js';
|
|
2
|
+
import { getDpr, makeBpMapper, spanLeft } from '../canvas2dUtils.js';
|
|
3
|
+
import * as shader from '../shaders/barMark.generated.js';
|
|
4
|
+
import { valueToYPxScaled } from '../shaders/pointMark.js.generated.js';
|
|
5
|
+
import { slangPass } from '../slangPass.js';
|
|
6
|
+
import { makeAbgrFill } from './colorFill.js';
|
|
7
|
+
import { colorBits, paintColors, rampUniforms, valueScaleTypeCode, } from './markRamp.js';
|
|
8
|
+
import { blockPx } from './spanMark.js';
|
|
9
|
+
// The rect one instance paints, in the frame's CSS px, or undefined for a bar
|
|
10
|
+
// with no height — which draws nothing and so cannot be hovered.
|
|
11
|
+
function barRect(bpToPx, x, x2, y, params, canvasHeight) {
|
|
12
|
+
const xa = bpToPx(x);
|
|
13
|
+
const xb = bpToPx(x2);
|
|
14
|
+
const width = Math.max(params.minWidthPx, Math.abs(xb - xa));
|
|
15
|
+
const [domainMin, domainMax] = params.domain;
|
|
16
|
+
const st = valueScaleTypeCode(params.scaleType);
|
|
17
|
+
const valueY = valueToYPxScaled(y, domainMin, domainMax, canvasHeight, st);
|
|
18
|
+
const originY = valueToYPxScaled(params.origin, domainMin, domainMax, canvasHeight, st);
|
|
19
|
+
const top = Math.min(valueY, originY);
|
|
20
|
+
const height = Math.abs(valueY - originY);
|
|
21
|
+
return height === 0
|
|
22
|
+
? undefined
|
|
23
|
+
: { left: spanLeft(xa, xb, width), top, width, height };
|
|
24
|
+
}
|
|
25
|
+
export const barMark = {
|
|
26
|
+
id: 'bar',
|
|
27
|
+
pass: {
|
|
28
|
+
...slangPass({ id: 'bar', mod: shader }),
|
|
29
|
+
pack: c => shader.packInstances({ ...c, color: colorBits(c) }, c.count),
|
|
30
|
+
},
|
|
31
|
+
writeUniforms(scratch, clip, block, frame, params) {
|
|
32
|
+
shader.writeUniforms(scratch, {
|
|
33
|
+
bpRangeX: bpRangeXTuple(clip, block.reversed),
|
|
34
|
+
canvasHeight: frame.canvasHeight,
|
|
35
|
+
domainMin: params.domain[0],
|
|
36
|
+
domainMax: params.domain[1],
|
|
37
|
+
valueScaleType: valueScaleTypeCode(params.scaleType),
|
|
38
|
+
...rampUniforms(params.ramp),
|
|
39
|
+
origin: params.origin,
|
|
40
|
+
zero: 0,
|
|
41
|
+
minCellDenomPx: clip.scissorW,
|
|
42
|
+
minWidthPx: params.minWidthPx,
|
|
43
|
+
devicePixelRatio: getDpr(),
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
paintBlock(ctx, channels, block, frame, params) {
|
|
47
|
+
const { x, x2, y, count } = channels;
|
|
48
|
+
const color = paintColors(channels, count, params.ramp);
|
|
49
|
+
const bpToPx = makeBpMapper(block);
|
|
50
|
+
const setFill = makeAbgrFill(ctx);
|
|
51
|
+
for (let i = 0; i < count; i++) {
|
|
52
|
+
const r = barRect(bpToPx, x[i], x2[i], y[i], params, frame.canvasHeight);
|
|
53
|
+
if (r) {
|
|
54
|
+
setFill(color[i]);
|
|
55
|
+
ctx.fillRect(r.left, r.top, r.width, r.height);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
ink(channels, block, frame, params, i) {
|
|
60
|
+
const { x, x2, y } = channels;
|
|
61
|
+
return barRect(bp => blockPx(block, bp), x[i], x2[i], y[i], params, frame.canvasHeight);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -5,15 +5,16 @@ export interface RecordedRect {
|
|
|
5
5
|
y: number;
|
|
6
6
|
w: number;
|
|
7
7
|
h: number;
|
|
8
|
-
fillStyle:
|
|
8
|
+
fillStyle: MarkContext2D['fillStyle'];
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* A `MarkContext2D` that records every rect a painter fills, in paint order
|
|
12
|
-
* `fillRect` and path `rect` alike, so a shape that
|
|
13
|
-
* the same list as one that fills per instance. A
|
|
14
|
-
* `moveTo`/`lineTo` and filled records its
|
|
15
|
-
* shape whose hit test answers a polygon's box
|
|
16
|
-
* triangle) is held to
|
|
11
|
+
* A `MarkContext2D` that records every rect a painter fills, in paint order and
|
|
12
|
+
* in CANVAS coordinates — `fillRect` and path `rect` alike, so a shape that
|
|
13
|
+
* batches by colour records the same list as one that fills per instance. A
|
|
14
|
+
* path traced with `moveTo`/`lineTo`/`bezierCurveTo` and filled records its
|
|
15
|
+
* bounding box, which is what a shape whose hit test answers a polygon's box
|
|
16
|
+
* (the variant inversion triangle) is held to; a curve contributes its control
|
|
17
|
+
* points, whose hull contains it.
|
|
17
18
|
*
|
|
18
19
|
* A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
|
|
19
20
|
* edge's bounding box widened by half the current `lineWidth` — the extent a
|
|
@@ -22,17 +23,85 @@ export interface RecordedRect {
|
|
|
22
23
|
* otherwise record the run's hull as a single instance. `fillStyle` on such a
|
|
23
24
|
* record is the `strokeStyle` it went down in; the field is the colour of the
|
|
24
25
|
* ink either way.
|
|
26
|
+
*
|
|
27
|
+
* `save`/`restore` bracket the transform and the styles the way a real context
|
|
28
|
+
* does, and every recorded point goes through the CTM. The half-widths — a
|
|
29
|
+
* stroke's and a `strokeRect`'s — are measured AFTER it, so they are exact under
|
|
30
|
+
* a translation and approximate under a scale; no shape in tree strokes under
|
|
31
|
+
* one, and a rotated CTM records the ink's axis-aligned box rather than its
|
|
32
|
+
* silhouette.
|
|
25
33
|
*/
|
|
26
34
|
export declare function recordingContext(): {
|
|
27
|
-
ctx:
|
|
35
|
+
ctx: {
|
|
36
|
+
fillStyle: string;
|
|
37
|
+
strokeStyle: string;
|
|
38
|
+
lineWidth: number;
|
|
39
|
+
lineCap: "butt";
|
|
40
|
+
lineJoin: "miter";
|
|
41
|
+
save(): void;
|
|
42
|
+
restore(): void;
|
|
43
|
+
translate(x: number, y: number): void;
|
|
44
|
+
scale(x: number, y: number): void;
|
|
45
|
+
rotate(angle: number): void;
|
|
46
|
+
beginPath(): void;
|
|
47
|
+
clip(): void;
|
|
48
|
+
setLineDash(): void;
|
|
49
|
+
moveTo(x: number, y: number): void;
|
|
50
|
+
lineTo(x: number, y: number): void;
|
|
51
|
+
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
|
|
52
|
+
arc(x: number, y: number, radius: number): void;
|
|
53
|
+
ellipse(x: number, y: number, radiusX: number, radiusY: number): void;
|
|
54
|
+
closePath(): void;
|
|
55
|
+
fill(): void;
|
|
56
|
+
stroke(): void;
|
|
57
|
+
rect(x: number, y: number, w: number, h: number): void;
|
|
58
|
+
fillRect(x: number, y: number, w: number, h: number): void;
|
|
59
|
+
strokeRect(x: number, y: number, w: number, h: number): void;
|
|
60
|
+
};
|
|
28
61
|
calls: RecordedRect[];
|
|
29
62
|
};
|
|
30
63
|
/**
|
|
31
|
-
* The draw-against-hit gate
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
64
|
+
* The draw-against-hit gate: paint the block, then walk it in `step` px and
|
|
65
|
+
* hold `hitNearest` to what the painter actually put on the canvas.
|
|
66
|
+
*
|
|
67
|
+
* **What the painter drew for instance `i` has to be attributed to `i`,** and
|
|
68
|
+
* there are two ways to get that. A shape whose every instance is one
|
|
69
|
+
* `fillRect` gets it positionally, which is the default and needs nothing. A
|
|
70
|
+
* shape that batches a colour run into one path (`point`'s glyphs), or that
|
|
71
|
+
* skips an instance the view has scrolled past (`cell`'s off-canvas rows),
|
|
72
|
+
* cannot be read that way at all — the batch has fewer rects than instances, or
|
|
73
|
+
* the wrong ones. `sliceOne` is the way in for those: hand back one instance's
|
|
74
|
+
* channels and the sweep paints it alone, so the attribution holds by
|
|
75
|
+
* construction and a culled instance is visibly an empty painting.
|
|
76
|
+
*
|
|
77
|
+
* Four claims, all of them one-directional and true of every shape:
|
|
78
|
+
*
|
|
79
|
+
* - an instance that painted nothing is never the answer,
|
|
80
|
+
* - `hit.x`/`hit.y` — where the shape says its ink is — lies inside the box the
|
|
81
|
+
* painter drew for the instance the hit names,
|
|
82
|
+
* - `hit.distSq` is no smaller than the distance to that box, since the box
|
|
83
|
+
* contains the ink and cannot be farther than it,
|
|
84
|
+
* - `hit.distSq` IS the distance to `hit.x`/`hit.y`, which is `MarkHit`'s own
|
|
85
|
+
* contract rather than a fact about the painting: a shape that answers a
|
|
86
|
+
* distance its own reported point does not support has two spellings of where
|
|
87
|
+
* its ink is, and the nearest-wins walk is resolved by the one the caller
|
|
88
|
+
* cannot see.
|
|
89
|
+
*
|
|
90
|
+
* The fourth claim is the strongest and is not universal: **a point on the box
|
|
91
|
+
* has to answer that box**, the last-painted one where boxes overlap, since the
|
|
92
|
+
* sweep hands candidates back to front. It holds only where the painted box IS
|
|
93
|
+
* the hit target — every `fillRect` shape, and `cell`'s inversion triangle,
|
|
94
|
+
* which answers as its bounding box on purpose — and not for a glyph, whose box
|
|
95
|
+
* is a superset of its ink. So it runs exactly when the batch painted one rect
|
|
96
|
+
* per instance, which is that set.
|
|
97
|
+
*
|
|
98
|
+
* A shape declaring `ink` is held to a second clause, per instance: the box
|
|
99
|
+
* the painter recorded for `i` lies inside `ink(i)`, and each edge of `ink(i)`
|
|
100
|
+
* lies within `inkSlackPx` (default 1) of the painting's — so the rect is
|
|
101
|
+
* tight to the ink within a pixel on every side, which admits a seam overdraw
|
|
102
|
+
* or a pixel snap and rejects a rect that is the wrong row, the wrong width or
|
|
103
|
+
* the whole canvas. An instance that painted nothing must answer no ink, and
|
|
104
|
+
* one with ink must have painted.
|
|
36
105
|
*
|
|
37
106
|
* Returns the violations rather than asserting, so a test reads as
|
|
38
107
|
* `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
|
|
@@ -41,7 +110,9 @@ export declare function recordingContext(): {
|
|
|
41
110
|
*/
|
|
42
111
|
export declare function sweepDrawAgainstHit<C extends {
|
|
43
112
|
count: number;
|
|
44
|
-
}, P>(shape: MarkShape<C, P>, channels: C, block: RenderBlock, frame: MarkFrame, params: P, { maxDistSq, step }: {
|
|
113
|
+
}, P>(shape: MarkShape<C, P>, channels: C, block: RenderBlock, frame: MarkFrame, params: P, { maxDistSq, step, sliceOne, inkSlackPx, }: {
|
|
45
114
|
maxDistSq: number;
|
|
46
115
|
step?: number;
|
|
116
|
+
sliceOne?: (channels: C, index: number) => C;
|
|
117
|
+
inkSlackPx?: number;
|
|
47
118
|
}): string[];
|