@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,116 @@
|
|
|
1
|
+
import type { CanvasScale } from '../canvas2dUtils.ts';
|
|
2
|
+
export type { CanvasScale };
|
|
3
|
+
/**
|
|
4
|
+
* One field of a pass's **vertex input layout** — the map from bytes in the
|
|
5
|
+
* packed instance buffer to an input the shader declares. Both HALs build from
|
|
6
|
+
* it: WebGPU as a `GPUVertexBufferLayout` attribute, WebGL2 as a VAO pointer.
|
|
7
|
+
* Emitted from the shader by codegen as `VERTEX_ATTRIBUTES`; never hand-written.
|
|
8
|
+
*/
|
|
9
|
+
export interface VertexAttributeLayout {
|
|
10
|
+
name: string;
|
|
11
|
+
components: number;
|
|
12
|
+
type: 'float' | 'uint' | 'int';
|
|
13
|
+
offsetBytes: number;
|
|
14
|
+
integer: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Samples per pixel a display's WebGPU render target is allocated at.
|
|
18
|
+
*
|
|
19
|
+
* **1 and 4 are the whole set the WebGPU spec permits** ("must be either 1 or
|
|
20
|
+
* 4" — standard sample patterns are defined for no other count), so this is the
|
|
21
|
+
* API's constraint rather than a policy of ours, and 2 is not an option to add.
|
|
22
|
+
*
|
|
23
|
+
* At 4 the HAL allocates a multisampled colour attachment the size of the
|
|
24
|
+
* canvas and resolves it into the canvas texture; at 1 it allocates **nothing**
|
|
25
|
+
* and draws straight into the canvas texture. The bytes are canvas area x dpr²
|
|
26
|
+
* x samples x 4, unrelated to how much data the display holds, which is why
|
|
27
|
+
* this is worth stating per display — see ARCHITECTURAL_LIMITS.md §"The MSAA
|
|
28
|
+
* target is the largest per-display allocation".
|
|
29
|
+
*
|
|
30
|
+
* WebGL2 has no counterpart: it draws to the default framebuffer with
|
|
31
|
+
* `antialias: true`, so its multisampling is the browser's choice inside the
|
|
32
|
+
* browser's budget.
|
|
33
|
+
*/
|
|
34
|
+
export type SampleCount = 1 | 4;
|
|
35
|
+
export type BlendFactor = 'one' | 'src-alpha' | 'one-minus-src-alpha' | 'zero';
|
|
36
|
+
export type BlendState = {
|
|
37
|
+
op?: 'add';
|
|
38
|
+
srcFactor: BlendFactor;
|
|
39
|
+
dstFactor: BlendFactor;
|
|
40
|
+
} | {
|
|
41
|
+
op: 'max';
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* One entry of a shader's GPU binding table, as the Slang codegen reflects it.
|
|
45
|
+
*
|
|
46
|
+
* `kind` is spelled the way WebGPU spells it so a consumer can build a
|
|
47
|
+
* `createBindGroupLayout` entry from it directly. That is the point: the binding
|
|
48
|
+
* indices used to be asserted by hand in three places that never consulted the
|
|
49
|
+
* shader — this HAL's two hardcoded render layouts, and the LD compute driver's
|
|
50
|
+
* own. A shader's `BINDINGS` export is the reflected truth, and `pnpm
|
|
51
|
+
* gen:shaders` now refuses a render shader whose table is not one the HALs bind.
|
|
52
|
+
*/
|
|
53
|
+
export interface ShaderBinding {
|
|
54
|
+
index: number;
|
|
55
|
+
kind: 'uniform' | 'texture' | 'sampler' | 'storage' | 'read-only-storage';
|
|
56
|
+
name: string;
|
|
57
|
+
}
|
|
58
|
+
export interface TextureBinding {
|
|
59
|
+
textureBinding: number;
|
|
60
|
+
samplerBinding: number;
|
|
61
|
+
glTextureUnit: number;
|
|
62
|
+
glUniformName: string;
|
|
63
|
+
filter: 'linear' | 'nearest';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A **pipeline state object (PSO)**: shaders, vertex input layout, blend state,
|
|
67
|
+
* primitive topology and texture bindings, baked into one immutable
|
|
68
|
+
* description. One of these compiles to exactly one `GPURenderPipeline`
|
|
69
|
+
* (`webgpuHal`) or one linked program + VAO (`webgl2Hal`), up front at HAL
|
|
70
|
+
* construction — never lazily mid-frame.
|
|
71
|
+
*
|
|
72
|
+
* `id` is the join key everything else calls a *pass*: `drawPass(id, …)` binds
|
|
73
|
+
* this pipeline and issues one instanced draw, and the HAL's buffer registry is
|
|
74
|
+
* keyed `(regionKey, id)`. That word does **not** mean WebGPU's render pass,
|
|
75
|
+
* which is the `beginFrame`/`endFrame` bracket and happens once per frame.
|
|
76
|
+
*/
|
|
77
|
+
export interface PipelineDescriptor {
|
|
78
|
+
id: string;
|
|
79
|
+
wgslSource: string;
|
|
80
|
+
glslVertex: string;
|
|
81
|
+
glslFragment: string;
|
|
82
|
+
instanceStride: number;
|
|
83
|
+
verticesPerInstance: number;
|
|
84
|
+
blend: boolean;
|
|
85
|
+
blendState?: BlendState;
|
|
86
|
+
vertexAttributes: readonly VertexAttributeLayout[];
|
|
87
|
+
topology?: 'triangle-list' | 'triangle-strip' | 'line-list';
|
|
88
|
+
textures?: readonly [TextureBinding, ...TextureBinding[]];
|
|
89
|
+
bindings?: readonly ShaderBinding[];
|
|
90
|
+
}
|
|
91
|
+
export interface GpuHal {
|
|
92
|
+
/**
|
|
93
|
+
* Size the canvas, and report the scale its backing store actually got.
|
|
94
|
+
* Callers derive every device-px rect from THAT rather than from `getDpr()`:
|
|
95
|
+
* past `MAX_CANVAS_DIM_PX` the two differ, and a rect built from the true dpr
|
|
96
|
+
* exceeds the target it draws into (WebGPU rejects the whole pass and the
|
|
97
|
+
* track goes blank; WebGL2 clamps and paints at the wrong scale).
|
|
98
|
+
*/
|
|
99
|
+
resize(width: number, height: number): CanvasScale;
|
|
100
|
+
uploadBuffer(regionKey: number, passId: string, data: ArrayBuffer | ArrayBufferView, count: number): void;
|
|
101
|
+
getBufferCount(regionKey: number, passId: string): number;
|
|
102
|
+
deleteBuffer(regionKey: number, passId: string): void;
|
|
103
|
+
deleteRegion(regionKey: number): void;
|
|
104
|
+
pruneRegions(active: Iterable<number>): void;
|
|
105
|
+
uploadTexture(passId: string, data: Uint8Array, width: number, height: number): void;
|
|
106
|
+
writeUniforms(data: ArrayBuffer): void;
|
|
107
|
+
beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
|
|
108
|
+
drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
|
|
109
|
+
endFrame(): void;
|
|
110
|
+
setScissor(x: number, y: number, w: number, h: number): void;
|
|
111
|
+
clearScissor(): void;
|
|
112
|
+
setViewport(x: number, y: number, w: number, h: number): void;
|
|
113
|
+
clearViewport(): void;
|
|
114
|
+
setErrorHandler(handler: (error: Error) => void): void;
|
|
115
|
+
dispose(): void;
|
|
116
|
+
}
|
package/esm/hal/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { GpuHalBase } from './gpuHalBase.ts';
|
|
2
|
+
import type { GpuHal, PipelineDescriptor, TextureBinding } from './types.ts';
|
|
3
|
+
interface RegionPassBuffer {
|
|
4
|
+
vbo: WebGLBuffer;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class WebGL2Hal extends GpuHalBase<RegionPassBuffer> implements GpuHal {
|
|
8
|
+
private gl;
|
|
9
|
+
private canvas;
|
|
10
|
+
private passes;
|
|
11
|
+
private ubo;
|
|
12
|
+
private maxTextureDim;
|
|
13
|
+
private debug;
|
|
14
|
+
private instanceId;
|
|
15
|
+
private firstDrawSeen;
|
|
16
|
+
private contextWasLost;
|
|
17
|
+
private contextLostListener;
|
|
18
|
+
private contextRestoredListener;
|
|
19
|
+
private checkGlError;
|
|
20
|
+
constructor(canvas: HTMLCanvasElement, descriptors: PipelineDescriptor[], uniformByteSize: number);
|
|
21
|
+
protected limits(): {
|
|
22
|
+
maxBufferBytes: number;
|
|
23
|
+
maxTextureDimensionPx: number;
|
|
24
|
+
};
|
|
25
|
+
protected createBuffer(data: ArrayBuffer | ArrayBufferView, count: number): {
|
|
26
|
+
vbo: WebGLBuffer;
|
|
27
|
+
count: number;
|
|
28
|
+
};
|
|
29
|
+
protected destroyBuffer(buf: RegionPassBuffer): void;
|
|
30
|
+
private compilePass;
|
|
31
|
+
private getPass;
|
|
32
|
+
resize(width: number, height: number): import("./types.ts").CanvasScale;
|
|
33
|
+
protected createTexture(passId: string, binding: TextureBinding, data: Uint8Array, width: number, height: number): void;
|
|
34
|
+
writeUniforms(data: ArrayBuffer): void;
|
|
35
|
+
beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
|
|
36
|
+
setScissor(x: number, y: number, w: number, h: number): void;
|
|
37
|
+
clearScissor(): void;
|
|
38
|
+
setViewport(x: number, y: number, w: number, h: number): void;
|
|
39
|
+
clearViewport(): void;
|
|
40
|
+
drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
|
|
41
|
+
endFrame(): void;
|
|
42
|
+
protected releaseResources(): void;
|
|
43
|
+
private applyBlendState;
|
|
44
|
+
private bindTextures;
|
|
45
|
+
private bindAttributes;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { syncCanvasSize } from '../canvas2dUtils.js';
|
|
2
|
+
import { canvasContextError, noteCanvasContext } from '../canvasContext.js';
|
|
3
|
+
import { GpuHalBase } from './gpuHalBase.js';
|
|
4
|
+
function createShader(gl, type, source) {
|
|
5
|
+
const shader = gl.createShader(type);
|
|
6
|
+
gl.shaderSource(shader, source);
|
|
7
|
+
gl.compileShader(shader);
|
|
8
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
9
|
+
const info = gl.getShaderInfoLog(shader);
|
|
10
|
+
gl.deleteShader(shader);
|
|
11
|
+
throw new Error(`Shader compile error: ${info}`);
|
|
12
|
+
}
|
|
13
|
+
return shader;
|
|
14
|
+
}
|
|
15
|
+
function createProgram(gl, vsSource, fsSource) {
|
|
16
|
+
const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
17
|
+
let fs;
|
|
18
|
+
try {
|
|
19
|
+
fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
gl.deleteShader(vs);
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
const program = gl.createProgram();
|
|
26
|
+
gl.attachShader(program, vs);
|
|
27
|
+
gl.attachShader(program, fs);
|
|
28
|
+
gl.linkProgram(program);
|
|
29
|
+
gl.detachShader(program, vs);
|
|
30
|
+
gl.detachShader(program, fs);
|
|
31
|
+
gl.deleteShader(vs);
|
|
32
|
+
gl.deleteShader(fs);
|
|
33
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
34
|
+
const info = gl.getProgramInfoLog(program);
|
|
35
|
+
gl.deleteProgram(program);
|
|
36
|
+
throw new Error(`Program link error: ${info}`);
|
|
37
|
+
}
|
|
38
|
+
return program;
|
|
39
|
+
}
|
|
40
|
+
function bindUniformBlock(gl, program, blockName, bindingPoint) {
|
|
41
|
+
const idx = gl.getUniformBlockIndex(program, blockName);
|
|
42
|
+
if (idx !== gl.INVALID_INDEX) {
|
|
43
|
+
gl.uniformBlockBinding(program, idx, bindingPoint);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Set `DEBUG.webgl2 = true` in devtools (or `?webgl2-debug=1` in URL) to
|
|
47
|
+
// enable verbose logging. Kept guarded so production builds stay quiet.
|
|
48
|
+
function debugEnabled() {
|
|
49
|
+
if (typeof window === 'undefined') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const w = window;
|
|
53
|
+
if (w.DEBUG?.webgl2) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return /(?:\?|&)webgl2-debug=1\b/.test(window.location.search);
|
|
57
|
+
}
|
|
58
|
+
function glErrorName(gl, code) {
|
|
59
|
+
if (code === gl.NO_ERROR) {
|
|
60
|
+
return 'NO_ERROR';
|
|
61
|
+
}
|
|
62
|
+
if (code === gl.INVALID_ENUM) {
|
|
63
|
+
return 'INVALID_ENUM';
|
|
64
|
+
}
|
|
65
|
+
if (code === gl.INVALID_VALUE) {
|
|
66
|
+
return 'INVALID_VALUE';
|
|
67
|
+
}
|
|
68
|
+
if (code === gl.INVALID_OPERATION) {
|
|
69
|
+
return 'INVALID_OPERATION';
|
|
70
|
+
}
|
|
71
|
+
if (code === gl.INVALID_FRAMEBUFFER_OPERATION) {
|
|
72
|
+
return 'INVALID_FRAMEBUFFER_OPERATION';
|
|
73
|
+
}
|
|
74
|
+
if (code === gl.OUT_OF_MEMORY) {
|
|
75
|
+
return 'OUT_OF_MEMORY';
|
|
76
|
+
}
|
|
77
|
+
if (code === gl.CONTEXT_LOST_WEBGL) {
|
|
78
|
+
return 'CONTEXT_LOST_WEBGL';
|
|
79
|
+
}
|
|
80
|
+
return `0x${code.toString(16)}`;
|
|
81
|
+
}
|
|
82
|
+
function glBlendFactor(gl, factor) {
|
|
83
|
+
switch (factor) {
|
|
84
|
+
case 'one':
|
|
85
|
+
return gl.ONE;
|
|
86
|
+
case 'zero':
|
|
87
|
+
return gl.ZERO;
|
|
88
|
+
case 'src-alpha':
|
|
89
|
+
return gl.SRC_ALPHA;
|
|
90
|
+
case 'one-minus-src-alpha':
|
|
91
|
+
return gl.ONE_MINUS_SRC_ALPHA;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Module-scope lifecycle tracking — Firefox caps active WebGL contexts
|
|
95
|
+
// around 16 and Chrome around 8. Context leaks force the oldest contexts to
|
|
96
|
+
// lose. These counters surface the leak when it happens.
|
|
97
|
+
let totalCreated = 0;
|
|
98
|
+
let totalDisposed = 0;
|
|
99
|
+
// The vertex-buffer ceiling this HAL refuses past, in bytes.
|
|
100
|
+
//
|
|
101
|
+
// WebGL2 exposes no max-buffer-size parameter, so unlike WebGPU there is
|
|
102
|
+
// nothing to ask — but "ask the driver" was never what the guard was for. Its
|
|
103
|
+
// job is to turn one pathological upload into the "zoom in" banner, and without
|
|
104
|
+
// it the same upload that banners on WebGPU takes Chrome's context down here
|
|
105
|
+
// instead. That is strictly worse than a blank track on a page at the context
|
|
106
|
+
// ceiling: the loss evicts a sibling, whose recovery evicts another, and
|
|
107
|
+
// GPU_CONTEXT_BUDGET.md is that cascade.
|
|
108
|
+
//
|
|
109
|
+
// 256 MiB is WebGPU's *spec default* `maxBufferSize`. It is not parity with
|
|
110
|
+
// what the WebGPU backend actually refuses at: `gpuDevice.acquire` raises the
|
|
111
|
+
// limit to `adapter.limits.maxBufferSize`, which is 2147483644 bytes (~2 GiB)
|
|
112
|
+
// on the Firefox Nightly / Intel UHD 630 this was checked on, so WebGL2 is the
|
|
113
|
+
// stricter of the two and a region can banner here while rendering there. That
|
|
114
|
+
// asymmetry is accepted — a quarter-gigabyte single vertex buffer is not
|
|
115
|
+
// something to hand an API that answers a failed allocation by dropping the
|
|
116
|
+
// context.
|
|
117
|
+
//
|
|
118
|
+
// Deliberately a ceiling and not a budget: it catches the single upload
|
|
119
|
+
// nothing else bounds, and says nothing about the total, which is
|
|
120
|
+
// ARCHITECTURAL_LIMITS.md §"No session-level GPU memory budget".
|
|
121
|
+
const MAX_VERTEX_BUFFER_BYTES = 256 * 1024 * 1024;
|
|
122
|
+
// Behavioral parity with WebGPUHal is enforced by tests, not by this file:
|
|
123
|
+
// products/jbrowse-web/browser-tests/compare-backends.ts pixel-diffs webgl vs
|
|
124
|
+
// webgpu vs canvas2d output, and shared buffer bookkeeping is covered by
|
|
125
|
+
// hal/regionRegistry.test.ts. Neither HAL is where attribute layout is checked —
|
|
126
|
+
// `assertVertexInputsMatch` does that at `pnpm gen:shaders` time, per shader and
|
|
127
|
+
// per target. Mirror any behavior change in webgpuHal.ts — and note that
|
|
128
|
+
// `GpuHalBase` already holds the parts that were only ever mirrored: the
|
|
129
|
+
// descriptor map, the buffer registry, both upload shells and their over-limit
|
|
130
|
+
// wording, and the dispose guard.
|
|
131
|
+
export class WebGL2Hal extends GpuHalBase {
|
|
132
|
+
gl;
|
|
133
|
+
canvas;
|
|
134
|
+
// Compiled passes, filled on demand by `getPass`. A pass whose program failed
|
|
135
|
+
// to build caches `null` so the failure is reported once, not every frame.
|
|
136
|
+
passes;
|
|
137
|
+
ubo;
|
|
138
|
+
// Asked once: `getParameter` is a synchronous driver query and `limits()` is
|
|
139
|
+
// now read on every upload, not only on a texture one.
|
|
140
|
+
maxTextureDim;
|
|
141
|
+
debug = false;
|
|
142
|
+
instanceId = 0;
|
|
143
|
+
firstDrawSeen = new Set();
|
|
144
|
+
// Latched on context loss, never cleared. GL objects from before the loss are
|
|
145
|
+
// invalid even after restore — isContextLost()===false by then, so a live
|
|
146
|
+
// check alone can't guard dispose().
|
|
147
|
+
contextWasLost = false;
|
|
148
|
+
contextLostListener = null;
|
|
149
|
+
contextRestoredListener = null;
|
|
150
|
+
checkGlError(label) {
|
|
151
|
+
if (!this.debug) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const err = this.gl.getError();
|
|
155
|
+
if (err !== this.gl.NO_ERROR) {
|
|
156
|
+
console.error(`[WebGL2Hal] GL error at "${label}": ${glErrorName(this.gl, err)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
constructor(canvas, descriptors, uniformByteSize) {
|
|
160
|
+
super(descriptors, 'WebGL2Hal');
|
|
161
|
+
this.canvas = canvas;
|
|
162
|
+
this.debug = debugEnabled();
|
|
163
|
+
totalCreated += 1;
|
|
164
|
+
this.instanceId = totalCreated;
|
|
165
|
+
if (this.debug) {
|
|
166
|
+
console.warn(`[WebGL2Hal #${this.instanceId}] init (live=${totalCreated - totalDisposed}/${totalCreated}, passes declared=${descriptors.length}, compiled on first draw)`);
|
|
167
|
+
}
|
|
168
|
+
const onContextLost = (e) => {
|
|
169
|
+
const ev = e;
|
|
170
|
+
console.error(`[WebGL2Hal #${this.instanceId}] context LOST (statusMessage="${ev.statusMessage}", live=${totalCreated - totalDisposed})`);
|
|
171
|
+
this.contextWasLost = true;
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
};
|
|
174
|
+
const onContextRestored = () => {
|
|
175
|
+
console.warn(`[WebGL2Hal #${this.instanceId}] context restored`);
|
|
176
|
+
};
|
|
177
|
+
canvas.addEventListener('webglcontextlost', onContextLost, false);
|
|
178
|
+
canvas.addEventListener('webglcontextrestored', onContextRestored, false);
|
|
179
|
+
this.contextLostListener = onContextLost;
|
|
180
|
+
this.contextRestoredListener = onContextRestored;
|
|
181
|
+
// premultipliedAlpha:true is required for correct AA edge blending.
|
|
182
|
+
// The canvas is cleared to (0,0,0,0) and drawn with SRC_ALPHA,ONE_MINUS_SRC_ALPHA
|
|
183
|
+
// blend, which produces premultiplied-alpha values in the framebuffer
|
|
184
|
+
// (edge pixel: rgb = color*alpha, a = alpha). With premultipliedAlpha:true
|
|
185
|
+
// the browser compositor reads those as premultiplied and composites correctly:
|
|
186
|
+
// output = fb.rgb + bg*(1-fb.a)
|
|
187
|
+
// With premultipliedAlpha:false the compositor treats them as straight alpha and
|
|
188
|
+
// multiplies rgb by alpha a second time, making AA edges appear too dark.
|
|
189
|
+
// The WebGPU HAL uses alphaMode:'premultiplied' for the same reason.
|
|
190
|
+
const gl = canvas.getContext('webgl2', {
|
|
191
|
+
antialias: true,
|
|
192
|
+
premultipliedAlpha: true,
|
|
193
|
+
});
|
|
194
|
+
if (!gl) {
|
|
195
|
+
// Was a bare 'WebGL2 not supported', which is the wrong diagnosis for the
|
|
196
|
+
// case that actually reaches here in production: a re-init on a canvas the
|
|
197
|
+
// WebGPU rung already committed. See canvasContext.ts.
|
|
198
|
+
throw canvasContextError(canvas, 'webgl2');
|
|
199
|
+
}
|
|
200
|
+
noteCanvasContext(canvas, 'webgl2');
|
|
201
|
+
this.gl = gl;
|
|
202
|
+
this.maxTextureDim = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE));
|
|
203
|
+
this.ubo = gl.createBuffer();
|
|
204
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo);
|
|
205
|
+
gl.bufferData(gl.UNIFORM_BUFFER, uniformByteSize, gl.DYNAMIC_DRAW);
|
|
206
|
+
this.passes = new Map();
|
|
207
|
+
// Programs are built on first use (see `getPass`), not here: a renderer
|
|
208
|
+
// declares every pass it could ever draw — alignments alone declares 21,
|
|
209
|
+
// most of them behind a colorBy/arc/per-base setting — and linking one
|
|
210
|
+
// costs tens of ms of driver time on the main thread. A profile of a
|
|
211
|
+
// three-track LGV linked 29 programs and drew with 14.
|
|
212
|
+
//
|
|
213
|
+
// The first descriptor still links eagerly as a canary, so a GL stack that
|
|
214
|
+
// can't compile our shaders at all throws from the constructor and
|
|
215
|
+
// `createGpuHal` falls back to Canvas2D — the ladder only runs at
|
|
216
|
+
// construction. Per-pass compile failures after that are caught in
|
|
217
|
+
// `getPass` and surfaced through the error handler.
|
|
218
|
+
const canary = descriptors[0];
|
|
219
|
+
if (canary) {
|
|
220
|
+
this.compilePass(canary);
|
|
221
|
+
}
|
|
222
|
+
gl.enable(gl.BLEND);
|
|
223
|
+
}
|
|
224
|
+
// A fixed buffer ceiling rather than a queried one — see
|
|
225
|
+
// MAX_VERTEX_BUFFER_BYTES for why the guard exists at all and why the number
|
|
226
|
+
// is WebGPU's. Past it, an unguarded bufferData loses the context in Chrome
|
|
227
|
+
// and throws RangeError in Firefox; neither is a getError() case, so the
|
|
228
|
+
// base's report is the only place the display can be told.
|
|
229
|
+
limits() {
|
|
230
|
+
return {
|
|
231
|
+
maxBufferBytes: MAX_VERTEX_BUFFER_BYTES,
|
|
232
|
+
maxTextureDimensionPx: this.maxTextureDim,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
createBuffer(data, count) {
|
|
236
|
+
const gl = this.gl;
|
|
237
|
+
const vbo = gl.createBuffer();
|
|
238
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
239
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
240
|
+
return { vbo, count };
|
|
241
|
+
}
|
|
242
|
+
// No mid-frame deferral, unlike `WebGPUHal`'s hook: GL is immediate-mode, so
|
|
243
|
+
// a draw already issued has consumed the buffer and deleting it between
|
|
244
|
+
// beginFrame and endFrame is defined. The parity that matters is the
|
|
245
|
+
// caller's — the same upload sequence is legal on both.
|
|
246
|
+
destroyBuffer(buf) {
|
|
247
|
+
const gl = this.gl;
|
|
248
|
+
if (!this.contextWasLost && !gl.isContextLost()) {
|
|
249
|
+
gl.deleteBuffer(buf.vbo);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
compilePass(desc) {
|
|
253
|
+
const gl = this.gl;
|
|
254
|
+
const program = createProgram(gl, desc.glslVertex, desc.glslFragment);
|
|
255
|
+
bindUniformBlock(gl, program, 'Uniforms', 0);
|
|
256
|
+
this.checkGlError(`link pass "${desc.id}"`);
|
|
257
|
+
const attrLocs = desc.vertexAttributes.map(attr => gl.getAttribLocation(program, attr.name));
|
|
258
|
+
if (this.debug) {
|
|
259
|
+
const pairs = desc.vertexAttributes.map((a, i) => `${a.name}@${attrLocs[i]}`);
|
|
260
|
+
console.warn(`[WebGL2Hal] pass "${desc.id}" stride=${desc.instanceStride} attrs: ${pairs.join(', ')}`);
|
|
261
|
+
const missing = desc.vertexAttributes.filter((_, i) => attrLocs[i] < 0);
|
|
262
|
+
if (missing.length > 0) {
|
|
263
|
+
console.warn(`[WebGL2Hal] pass "${desc.id}" missing attribute locations: ${missing.map(a => a.name).join(', ')}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const vao = gl.createVertexArray();
|
|
267
|
+
gl.bindVertexArray(vao);
|
|
268
|
+
for (const loc of attrLocs) {
|
|
269
|
+
if (loc >= 0) {
|
|
270
|
+
gl.enableVertexAttribArray(loc);
|
|
271
|
+
gl.vertexAttribDivisor(loc, 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
gl.bindVertexArray(null);
|
|
275
|
+
let textureState = null;
|
|
276
|
+
const tb = desc.textures?.[0];
|
|
277
|
+
if (tb) {
|
|
278
|
+
// Bind the sampler uniform to the texture unit once — it never changes.
|
|
279
|
+
gl.useProgram(program);
|
|
280
|
+
gl.uniform1i(gl.getUniformLocation(program, tb.glUniformName), tb.glTextureUnit);
|
|
281
|
+
textureState = { texture: null, unit: tb.glTextureUnit };
|
|
282
|
+
}
|
|
283
|
+
const state = {
|
|
284
|
+
program,
|
|
285
|
+
vao,
|
|
286
|
+
descriptor: desc,
|
|
287
|
+
textureState,
|
|
288
|
+
attrLocs,
|
|
289
|
+
};
|
|
290
|
+
this.passes.set(desc.id, state);
|
|
291
|
+
return state;
|
|
292
|
+
}
|
|
293
|
+
// Compiled pass for `passId`, building it the first time it's asked for.
|
|
294
|
+
// Returns undefined for an unknown id or a pass whose program failed to link.
|
|
295
|
+
getPass(passId) {
|
|
296
|
+
const existing = this.passes.get(passId);
|
|
297
|
+
if (existing !== undefined) {
|
|
298
|
+
return existing ?? undefined;
|
|
299
|
+
}
|
|
300
|
+
const desc = this.descriptors.get(passId);
|
|
301
|
+
if (!desc) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
return this.compilePass(desc);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
this.passes.set(passId, null);
|
|
309
|
+
this.oom.report(`#${this.instanceId} could not build the "${passId}" shader on this GPU: ${e instanceof Error ? e.message : String(e)}`);
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
resize(width, height) {
|
|
314
|
+
return syncCanvasSize(this.canvas, width, height).scale;
|
|
315
|
+
}
|
|
316
|
+
createTexture(passId, binding, data, width, height) {
|
|
317
|
+
const gl = this.gl;
|
|
318
|
+
const ts = this.getPass(passId)?.textureState;
|
|
319
|
+
if (!ts) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (ts.texture) {
|
|
323
|
+
gl.deleteTexture(ts.texture);
|
|
324
|
+
}
|
|
325
|
+
const tex = gl.createTexture();
|
|
326
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
327
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
328
|
+
const filter = binding.filter === 'linear' ? gl.LINEAR : gl.NEAREST;
|
|
329
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
330
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
331
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
332
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
333
|
+
ts.texture = tex;
|
|
334
|
+
}
|
|
335
|
+
writeUniforms(data) {
|
|
336
|
+
const gl = this.gl;
|
|
337
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo);
|
|
338
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
|
|
339
|
+
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, this.ubo);
|
|
340
|
+
}
|
|
341
|
+
beginFrame(clearR, clearG, clearB, clearA = 1) {
|
|
342
|
+
const gl = this.gl;
|
|
343
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
344
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
345
|
+
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
346
|
+
gl.clearColor(clearR, clearG, clearB, clearA);
|
|
347
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
348
|
+
}
|
|
349
|
+
setScissor(x, y, w, h) {
|
|
350
|
+
const gl = this.gl;
|
|
351
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
352
|
+
gl.scissor(x, this.canvas.height - y - h, w, h);
|
|
353
|
+
}
|
|
354
|
+
clearScissor() {
|
|
355
|
+
this.gl.disable(this.gl.SCISSOR_TEST);
|
|
356
|
+
}
|
|
357
|
+
setViewport(x, y, w, h) {
|
|
358
|
+
this.gl.viewport(x, this.canvas.height - y - h, w, h);
|
|
359
|
+
}
|
|
360
|
+
clearViewport() {
|
|
361
|
+
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
362
|
+
}
|
|
363
|
+
drawPass(passId, regionKey, bufferPassId) {
|
|
364
|
+
const gl = this.gl;
|
|
365
|
+
// Buffer first, program second: a pass with nothing to draw must not be
|
|
366
|
+
// the reason its shader gets compiled.
|
|
367
|
+
const regionBuf = this.regions.get(regionKey, bufferPassId ?? passId);
|
|
368
|
+
if (!regionBuf || regionBuf.count === 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const pass = this.getPass(passId);
|
|
372
|
+
if (!pass) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
this.applyBlendState(pass.descriptor);
|
|
376
|
+
gl.useProgram(pass.program);
|
|
377
|
+
gl.bindVertexArray(pass.vao);
|
|
378
|
+
this.bindAttributes(pass, regionBuf.vbo);
|
|
379
|
+
this.bindTextures(pass);
|
|
380
|
+
const topo = pass.descriptor.topology ?? 'triangle-list';
|
|
381
|
+
const glMode = topo === 'triangle-strip'
|
|
382
|
+
? gl.TRIANGLE_STRIP
|
|
383
|
+
: topo === 'line-list'
|
|
384
|
+
? gl.LINES
|
|
385
|
+
: gl.TRIANGLES;
|
|
386
|
+
gl.drawArraysInstanced(glMode, 0, pass.descriptor.verticesPerInstance, regionBuf.count);
|
|
387
|
+
if (this.debug && !this.firstDrawSeen.has(passId)) {
|
|
388
|
+
this.firstDrawSeen.add(passId);
|
|
389
|
+
const err = gl.getError();
|
|
390
|
+
console.warn(`[WebGL2Hal #${this.instanceId}] first draw pass="${passId}" verts=${pass.descriptor.verticesPerInstance} instances=${regionBuf.count} err=${glErrorName(gl, err)}`);
|
|
391
|
+
}
|
|
392
|
+
gl.bindVertexArray(null);
|
|
393
|
+
}
|
|
394
|
+
endFrame() {
|
|
395
|
+
const gl = this.gl;
|
|
396
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
397
|
+
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
398
|
+
}
|
|
399
|
+
releaseResources() {
|
|
400
|
+
const gl = this.gl;
|
|
401
|
+
totalDisposed += 1;
|
|
402
|
+
if (this.debug) {
|
|
403
|
+
console.warn(`[WebGL2Hal #${this.instanceId}] DISPOSING context (live=${totalCreated - totalDisposed}/${totalCreated})`);
|
|
404
|
+
}
|
|
405
|
+
// Remove canvas event listeners to prevent closure references from keeping
|
|
406
|
+
// the context alive after disposal. This is critical for test suites where
|
|
407
|
+
// multiple contexts are created in sequence (e.g. Puppeteer page navigation).
|
|
408
|
+
// Double-dispose is short-circuited above, so listeners are guaranteed set.
|
|
409
|
+
if (this.contextLostListener) {
|
|
410
|
+
this.canvas.removeEventListener('webglcontextlost', this.contextLostListener);
|
|
411
|
+
}
|
|
412
|
+
if (this.contextRestoredListener) {
|
|
413
|
+
this.canvas.removeEventListener('webglcontextrestored', this.contextRestoredListener);
|
|
414
|
+
}
|
|
415
|
+
this.regions.deleteAll();
|
|
416
|
+
if (!this.contextWasLost && !gl.isContextLost()) {
|
|
417
|
+
for (const pass of this.passes.values()) {
|
|
418
|
+
if (pass) {
|
|
419
|
+
gl.deleteVertexArray(pass.vao);
|
|
420
|
+
gl.deleteProgram(pass.program);
|
|
421
|
+
if (pass.textureState?.texture) {
|
|
422
|
+
gl.deleteTexture(pass.textureState.texture);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
gl.deleteBuffer(this.ubo);
|
|
427
|
+
}
|
|
428
|
+
this.passes.clear();
|
|
429
|
+
// Firefox appears to treat WEBGL_lose_context.loseContext() as a
|
|
430
|
+
// driver-wide reset: calling it on one disposed HAL synchronously
|
|
431
|
+
// knocks out sibling live contexts too, so tracks go blank en masse.
|
|
432
|
+
// Chrome only needs this as a test-suite optimisation; for production we
|
|
433
|
+
// let the browser reclaim the context when the canvas is GC'd. If we
|
|
434
|
+
// need explicit release again, gate it on navigator.userAgent.
|
|
435
|
+
}
|
|
436
|
+
applyBlendState(desc) {
|
|
437
|
+
const gl = this.gl;
|
|
438
|
+
if (!desc.blend) {
|
|
439
|
+
gl.disable(gl.BLEND);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
gl.enable(gl.BLEND);
|
|
443
|
+
const bs = desc.blendState;
|
|
444
|
+
if (bs?.op === 'max') {
|
|
445
|
+
// MIN/MAX ignore blend factors: the framebuffer keeps the per-channel
|
|
446
|
+
// max(src, dst). Used by same-color AA lines so overlapping segments union
|
|
447
|
+
// instead of darkening. Reset explicitly below for every other pass.
|
|
448
|
+
gl.blendEquation(gl.MAX);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
452
|
+
// RGB and alpha get different blend factors (blendFuncSeparate):
|
|
453
|
+
// RGB: out = src_rgb * srcFactor + dst_rgb * dstFactor (default: src-alpha / 1-src-alpha)
|
|
454
|
+
// Alpha: out = src_alpha * 1 + dst_alpha * (1 - src_alpha)
|
|
455
|
+
// The alpha channel uses ONE/ONE_MINUS_SRC_ALPHA regardless of the custom blend state;
|
|
456
|
+
// using the RGB srcFactor for alpha too would give out_alpha = src_alpha² + ..., which is wrong.
|
|
457
|
+
const src = bs ? glBlendFactor(gl, bs.srcFactor) : gl.SRC_ALPHA;
|
|
458
|
+
const dst = bs ? glBlendFactor(gl, bs.dstFactor) : gl.ONE_MINUS_SRC_ALPHA;
|
|
459
|
+
gl.blendFuncSeparate(src, dst, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
bindTextures(pass) {
|
|
463
|
+
const gl = this.gl;
|
|
464
|
+
const ts = pass.textureState;
|
|
465
|
+
if (ts?.texture) {
|
|
466
|
+
gl.activeTexture(gl.TEXTURE0 + ts.unit);
|
|
467
|
+
gl.bindTexture(gl.TEXTURE_2D, ts.texture);
|
|
468
|
+
// sampler uniform set once at construction — no uniform1i here
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
bindAttributes(pass, vbo) {
|
|
472
|
+
const gl = this.gl;
|
|
473
|
+
const desc = pass.descriptor;
|
|
474
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
475
|
+
for (let i = 0; i < desc.vertexAttributes.length; i++) {
|
|
476
|
+
const loc = pass.attrLocs[i];
|
|
477
|
+
if (loc < 0) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const attr = desc.vertexAttributes[i];
|
|
481
|
+
if (attr.integer) {
|
|
482
|
+
const glType = attr.type === 'uint' ? gl.UNSIGNED_INT : gl.INT;
|
|
483
|
+
gl.vertexAttribIPointer(loc, attr.components, glType, desc.instanceStride, attr.offsetBytes);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
gl.vertexAttribPointer(loc, attr.components, gl.FLOAT, false, desc.instanceStride, attr.offsetBytes);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|