@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,7 @@
|
|
|
1
|
+
export { createGpuHal } from './createHal.ts';
|
|
2
|
+
export { MockHal } from './mockHal.ts';
|
|
3
|
+
export { assertUniquePassIds } from './passIds.ts';
|
|
4
|
+
export { WebGL2Hal } from './webgl2Hal.ts';
|
|
5
|
+
export { WebGPUHal } from './webgpuHal.ts';
|
|
6
|
+
export type { GpuHalOptions } from './createHal.ts';
|
|
7
|
+
export type { BlendState, VertexAttributeLayout, GpuHal, PipelineDescriptor, SampleCount, ShaderBinding, TextureBinding, } from './types.ts';
|
package/esm/hal/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { GpuHalBase } from './gpuHalBase.ts';
|
|
2
|
+
import type { GpuHal, PipelineDescriptor, TextureBinding } from './types.ts';
|
|
3
|
+
export interface MockCall {
|
|
4
|
+
method: string;
|
|
5
|
+
args: unknown[];
|
|
6
|
+
}
|
|
7
|
+
interface MockBuffer {
|
|
8
|
+
data: ArrayBufferLike;
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MockRect {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One draw, with the state that was in force when it was issued — the clip, and
|
|
19
|
+
* which `writeUniforms` it reads. `null` scissor/viewport means unclipped, the
|
|
20
|
+
* full canvas, which is what both HALs owe after a `clearScissor` /
|
|
21
|
+
* `clearViewport`.
|
|
22
|
+
*
|
|
23
|
+
* `uniformWrite` indexes {@link MockHal.getUniformWritesF32}, or is `-1` when
|
|
24
|
+
* nothing has been written yet. It is here for the same reason the clip is:
|
|
25
|
+
* uniforms are *state*, so "which values did this draw actually use" is a
|
|
26
|
+
* question about the order of two methods, and a test asking it off `calls` has
|
|
27
|
+
* to re-implement the pairing to find out.
|
|
28
|
+
*
|
|
29
|
+
* **The pairing is by adjacency, and the two HALs mean different things by
|
|
30
|
+
* it.** `WebGPUHal` stages each write into a ring slot and binds slot
|
|
31
|
+
* `uniformSlot - 1` — "whatever was written most recently" — while `WebGL2Hal`
|
|
32
|
+
* does an immediate `bufferSubData` into one UBO. They agree only while a
|
|
33
|
+
* renderer writes then draws, which is the convention every renderer in tree
|
|
34
|
+
* follows and nothing enforces: batching the writes and then issuing the draws
|
|
35
|
+
* is right on WebGL2 and Canvas2D and silently wrong on WebGPU. This field is
|
|
36
|
+
* what lets a backend test pin its renderer's pairing.
|
|
37
|
+
*/
|
|
38
|
+
export interface MockDraw {
|
|
39
|
+
passId: string;
|
|
40
|
+
regionKey: number;
|
|
41
|
+
bufferPassId: string | undefined;
|
|
42
|
+
scissor: MockRect | null;
|
|
43
|
+
viewport: MockRect | null;
|
|
44
|
+
uniformWrite: number;
|
|
45
|
+
}
|
|
46
|
+
export declare class MockHal extends GpuHalBase<MockBuffer> implements GpuHal {
|
|
47
|
+
calls: MockCall[];
|
|
48
|
+
private uniformWrites;
|
|
49
|
+
/**
|
|
50
|
+
* The clip each draw of the frame went out under — see {@link draws}.
|
|
51
|
+
*
|
|
52
|
+
* Scissor and viewport are the one part of the HAL contract a call log cannot
|
|
53
|
+
* show. They are *state*: set once, they hold over every later draw until
|
|
54
|
+
* changed, so "which columns did the mismatch pass actually paint into" is a
|
|
55
|
+
* question about the order of two different methods, and a test asking it off
|
|
56
|
+
* `calls` has to re-implement the state machine to find out.
|
|
57
|
+
*
|
|
58
|
+
* That is not hypothetical. `clearScissor` on WebGPU used to drop the stored
|
|
59
|
+
* rect without re-issuing a full-canvas one, so every draw after it went on
|
|
60
|
+
* being clipped to the previous block while WebGL2 drew them unclipped — right
|
|
61
|
+
* on Canvas2D, right on WebGL2, wrong on WebGPU alone. Nothing in tree clears
|
|
62
|
+
* mid-frame today, which is exactly why nothing caught it. Recording the
|
|
63
|
+
* effective clip per draw is what makes that assertable at all.
|
|
64
|
+
*/
|
|
65
|
+
private scissor;
|
|
66
|
+
private viewport;
|
|
67
|
+
private drawLog;
|
|
68
|
+
/**
|
|
69
|
+
* `${regionKey}:${passId}` for every buffer the open frame has drawn from,
|
|
70
|
+
* and the ones that were then replaced or deleted before `endFrame`.
|
|
71
|
+
*
|
|
72
|
+
* That sequence is legal — `WebGPUHal` defers the `destroy()` past its submit
|
|
73
|
+
* precisely so it is — but it is the sequence that used to blank a frame, and
|
|
74
|
+
* it stays worth pinning per renderer: alignments re-uploads `OVERLAY_REGION`
|
|
75
|
+
* once per section inside its block loop, so a chain selection spanning two
|
|
76
|
+
* sections lands here, while synteny's mid-frame delete of an undrawn pass
|
|
77
|
+
* does not. A renderer test asserts on which of the two shapes it has.
|
|
78
|
+
*/
|
|
79
|
+
private frameOpen;
|
|
80
|
+
private drawnThisFrame;
|
|
81
|
+
private replacedWhileDrawnLog;
|
|
82
|
+
constructor(passes: PipelineDescriptor[]);
|
|
83
|
+
protected limits(): {
|
|
84
|
+
maxBufferBytes: number;
|
|
85
|
+
maxTextureDimensionPx: number;
|
|
86
|
+
};
|
|
87
|
+
protected createBuffer(data: ArrayBuffer | ArrayBufferView, count: number): {
|
|
88
|
+
data: ArrayBuffer | SharedArrayBuffer;
|
|
89
|
+
count: number;
|
|
90
|
+
};
|
|
91
|
+
protected destroyBuffer(): void;
|
|
92
|
+
private textures;
|
|
93
|
+
protected createTexture(passId: string, _binding: TextureBinding, data: Uint8Array, width: number, height: number): void;
|
|
94
|
+
protected releaseResources(): void;
|
|
95
|
+
private record;
|
|
96
|
+
errorHandler: ((error: Error) => void) | null;
|
|
97
|
+
setErrorHandler(handler: (error: Error) => void): void;
|
|
98
|
+
resize(width: number, height: number): {
|
|
99
|
+
x: number;
|
|
100
|
+
y: number;
|
|
101
|
+
};
|
|
102
|
+
uploadBuffer(regionKey: number, passId: string, data: ArrayBuffer | ArrayBufferView, count: number): void;
|
|
103
|
+
deleteBuffer(regionKey: number, passId: string): void;
|
|
104
|
+
deleteRegion(regionKey: number): void;
|
|
105
|
+
pruneRegions(active: Iterable<number>): void;
|
|
106
|
+
private noteReplaced;
|
|
107
|
+
private noteRegionReplaced;
|
|
108
|
+
writeUniforms(data: ArrayBuffer): void;
|
|
109
|
+
beginFrame(clearR: number, clearG: number, clearB: number, clearA?: number): void;
|
|
110
|
+
drawPass(passId: string, regionKey: number, bufferPassId?: string): void;
|
|
111
|
+
private assertRegistered;
|
|
112
|
+
endFrame(): void;
|
|
113
|
+
setScissor(x: number, y: number, w: number, h: number): void;
|
|
114
|
+
clearScissor(): void;
|
|
115
|
+
setViewport(x: number, y: number, w: number, h: number): void;
|
|
116
|
+
clearViewport(): void;
|
|
117
|
+
private get lastUniforms();
|
|
118
|
+
getLastUniformsF32(): Float32Array<ArrayBuffer> | null;
|
|
119
|
+
getLastUniformsU32(): Uint32Array<ArrayBuffer> | null;
|
|
120
|
+
getLastUniformsI32(): Int32Array<ArrayBuffer> | null;
|
|
121
|
+
getUniformWritesF32(): Float32Array<ArrayBuffer>[];
|
|
122
|
+
getUniformWritesU32(): Uint32Array<ArrayBuffer>[];
|
|
123
|
+
getBuffer(regionKey: number, passId: string): MockBuffer | undefined;
|
|
124
|
+
getTexture(passId: string): Uint8Array<ArrayBufferLike> | undefined;
|
|
125
|
+
callsOf(method: string): MockCall[];
|
|
126
|
+
/**
|
|
127
|
+
* Every draw of the frame with the state it went out under, in order — the
|
|
128
|
+
* answer to "which columns did this pass paint into" and "which uniforms did
|
|
129
|
+
* it read", neither of which `callsOf('drawPass')` can give, because clip and
|
|
130
|
+
* uniforms are state rather than arguments. `scissor: null` is unclipped;
|
|
131
|
+
* `uniformWrite` indexes `getUniformWrites*`. See {@link MockDraw} for what
|
|
132
|
+
* each is guarding.
|
|
133
|
+
*/
|
|
134
|
+
draws(): MockDraw[];
|
|
135
|
+
/**
|
|
136
|
+
* The uniform bytes a recorded draw reads, as f32 — `draws()` joined to
|
|
137
|
+
* `getUniformWritesF32()` for you, since doing it by hand at every call site
|
|
138
|
+
* is how the index and the list drift apart. `null` when the draw preceded
|
|
139
|
+
* any write, which is the case no renderer should rely on: WebGPU clamps it
|
|
140
|
+
* to ring slot 0 and WebGL2 leaves the previous frame's UBO bound, so the two
|
|
141
|
+
* backends disagree about what it even means.
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* `${regionKey}:${passId}` for each buffer this run replaced or deleted while
|
|
145
|
+
* the open frame had already drawn from it, in order — empty when a renderer
|
|
146
|
+
* never does that. See the field for why the sequence is worth asserting on
|
|
147
|
+
* either way.
|
|
148
|
+
*/
|
|
149
|
+
replacedWhileDrawn(): string[];
|
|
150
|
+
uniformsOf(draw: MockDraw): Float32Array<ArrayBuffer> | null;
|
|
151
|
+
reset(): void;
|
|
152
|
+
}
|
|
153
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { getDpr } from '../canvas2dUtils.js';
|
|
2
|
+
import { GpuHalBase } from './gpuHalBase.js';
|
|
3
|
+
import { assertUniquePassIds } from './passIds.js';
|
|
4
|
+
export class MockHal extends GpuHalBase {
|
|
5
|
+
calls = [];
|
|
6
|
+
// Every write of the frame, in order, not just the last. A renderer that
|
|
7
|
+
// rewrites the UBO mid-frame — for a band that reads it differently, or a
|
|
8
|
+
// section with its own offsets — has invariants the final state cannot show:
|
|
9
|
+
// that each write carries the frame-constant slots, that a temporary
|
|
10
|
+
// overwrite doesn't outlive its pass, and how many writes a frame costs at
|
|
11
|
+
// all (the real HALs stage these into a fixed-size ring).
|
|
12
|
+
uniformWrites = [];
|
|
13
|
+
/**
|
|
14
|
+
* The clip each draw of the frame went out under — see {@link draws}.
|
|
15
|
+
*
|
|
16
|
+
* Scissor and viewport are the one part of the HAL contract a call log cannot
|
|
17
|
+
* show. They are *state*: set once, they hold over every later draw until
|
|
18
|
+
* changed, so "which columns did the mismatch pass actually paint into" is a
|
|
19
|
+
* question about the order of two different methods, and a test asking it off
|
|
20
|
+
* `calls` has to re-implement the state machine to find out.
|
|
21
|
+
*
|
|
22
|
+
* That is not hypothetical. `clearScissor` on WebGPU used to drop the stored
|
|
23
|
+
* rect without re-issuing a full-canvas one, so every draw after it went on
|
|
24
|
+
* being clipped to the previous block while WebGL2 drew them unclipped — right
|
|
25
|
+
* on Canvas2D, right on WebGL2, wrong on WebGPU alone. Nothing in tree clears
|
|
26
|
+
* mid-frame today, which is exactly why nothing caught it. Recording the
|
|
27
|
+
* effective clip per draw is what makes that assertable at all.
|
|
28
|
+
*/
|
|
29
|
+
scissor = null;
|
|
30
|
+
viewport = null;
|
|
31
|
+
drawLog = [];
|
|
32
|
+
/**
|
|
33
|
+
* `${regionKey}:${passId}` for every buffer the open frame has drawn from,
|
|
34
|
+
* and the ones that were then replaced or deleted before `endFrame`.
|
|
35
|
+
*
|
|
36
|
+
* That sequence is legal — `WebGPUHal` defers the `destroy()` past its submit
|
|
37
|
+
* precisely so it is — but it is the sequence that used to blank a frame, and
|
|
38
|
+
* it stays worth pinning per renderer: alignments re-uploads `OVERLAY_REGION`
|
|
39
|
+
* once per section inside its block loop, so a chain selection spanning two
|
|
40
|
+
* sections lands here, while synteny's mid-frame delete of an undrawn pass
|
|
41
|
+
* does not. A renderer test asserts on which of the two shapes it has.
|
|
42
|
+
*/
|
|
43
|
+
frameOpen = false;
|
|
44
|
+
drawnThisFrame = new Map();
|
|
45
|
+
replacedWhileDrawnLog = [];
|
|
46
|
+
// The pass list is here for parity with the WebGL2Hal / WebGPUHal
|
|
47
|
+
// constructors, and it validates for the same reason `createRenderingBackend`
|
|
48
|
+
// does: a duplicate pass id is a silent GPU-only mis-render, so the check
|
|
49
|
+
// belongs everywhere a display hands its registry over. Checking it in the
|
|
50
|
+
// mock is what puts it in front of a unit test — every backend test builds a
|
|
51
|
+
// `MockHal` from its display's real pass list.
|
|
52
|
+
constructor(passes) {
|
|
53
|
+
super(passes, 'MockHal');
|
|
54
|
+
assertUniquePassIds(passes);
|
|
55
|
+
}
|
|
56
|
+
// No device behind this HAL, so nothing is ever over-limit: the shells' checks
|
|
57
|
+
// are the real HALs' to make. `descriptors` — the base's map of the same pass
|
|
58
|
+
// list — is what `drawPass` and `uploadTexture` below answer from.
|
|
59
|
+
limits() {
|
|
60
|
+
return {
|
|
61
|
+
maxBufferBytes: Number.POSITIVE_INFINITY,
|
|
62
|
+
maxTextureDimensionPx: Number.POSITIVE_INFINITY,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// The bytes are copied because both real HALs hand them to the driver at
|
|
66
|
+
// upload time, so a caller reusing its scratch array must not be able to
|
|
67
|
+
// change what a test then reads back out of `getBuffer`.
|
|
68
|
+
createBuffer(data, count) {
|
|
69
|
+
const copy = ArrayBuffer.isView(data)
|
|
70
|
+
? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
71
|
+
: data.slice(0);
|
|
72
|
+
return { data: copy, count };
|
|
73
|
+
}
|
|
74
|
+
destroyBuffer() { }
|
|
75
|
+
// Copied for the same reason `createBuffer` copies, and kept for the same
|
|
76
|
+
// reason the real HALs keep their texture: a test asserting which LUT a pass
|
|
77
|
+
// samples needs the bytes, not the byteLength the call log records.
|
|
78
|
+
textures = new Map();
|
|
79
|
+
createTexture(passId, _binding, data, width, height) {
|
|
80
|
+
this.textures.set(passId, data.slice());
|
|
81
|
+
this.record('uploadTexture', passId, data.byteLength, width, height);
|
|
82
|
+
}
|
|
83
|
+
releaseResources() {
|
|
84
|
+
this.record('dispose');
|
|
85
|
+
this.regions.deleteAll();
|
|
86
|
+
}
|
|
87
|
+
record(method, ...args) {
|
|
88
|
+
this.calls.push({ method, args });
|
|
89
|
+
}
|
|
90
|
+
errorHandler = null;
|
|
91
|
+
setErrorHandler(handler) {
|
|
92
|
+
super.setErrorHandler(handler);
|
|
93
|
+
this.errorHandler = handler;
|
|
94
|
+
this.record('setErrorHandler');
|
|
95
|
+
}
|
|
96
|
+
resize(width, height) {
|
|
97
|
+
this.record('resize', width, height);
|
|
98
|
+
// The real HALs report the scale their backing store actually got, and
|
|
99
|
+
// every device-px rect a renderer builds comes from this — so a mock
|
|
100
|
+
// returning nothing is a renderer whose rects are all NaN. `getDpr()` is
|
|
101
|
+
// the unclamped answer, which is what a mock canvas of any size would get.
|
|
102
|
+
return { x: getDpr(), y: getDpr() };
|
|
103
|
+
}
|
|
104
|
+
// The overrides below log and then defer to the base's shells, so the buffer
|
|
105
|
+
// lifecycle a test observes (delete-before-count, prune) is the same code both
|
|
106
|
+
// real HALs run rather than a twin that can drift out of parity.
|
|
107
|
+
uploadBuffer(regionKey, passId, data, count) {
|
|
108
|
+
this.record('uploadBuffer', regionKey, passId, data.byteLength, count);
|
|
109
|
+
this.noteReplaced(regionKey, passId);
|
|
110
|
+
super.uploadBuffer(regionKey, passId, data, count);
|
|
111
|
+
}
|
|
112
|
+
deleteBuffer(regionKey, passId) {
|
|
113
|
+
this.record('deleteBuffer', regionKey, passId);
|
|
114
|
+
this.noteReplaced(regionKey, passId);
|
|
115
|
+
super.deleteBuffer(regionKey, passId);
|
|
116
|
+
}
|
|
117
|
+
deleteRegion(regionKey) {
|
|
118
|
+
this.record('deleteRegion', regionKey);
|
|
119
|
+
this.noteRegionReplaced(regionKey);
|
|
120
|
+
super.deleteRegion(regionKey);
|
|
121
|
+
}
|
|
122
|
+
pruneRegions(active) {
|
|
123
|
+
const activeSet = new Set(active);
|
|
124
|
+
this.record('pruneRegions', [...activeSet]);
|
|
125
|
+
for (const regionKey of this.drawnThisFrame.keys()) {
|
|
126
|
+
if (!activeSet.has(regionKey)) {
|
|
127
|
+
this.noteRegionReplaced(regionKey);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
super.pruneRegions(activeSet);
|
|
131
|
+
}
|
|
132
|
+
noteReplaced(regionKey, passId) {
|
|
133
|
+
if (this.frameOpen && this.drawnThisFrame.get(regionKey)?.has(passId)) {
|
|
134
|
+
this.replacedWhileDrawnLog.push(`${regionKey}:${passId}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
noteRegionReplaced(regionKey) {
|
|
138
|
+
const drawn = this.drawnThisFrame.get(regionKey);
|
|
139
|
+
if (drawn) {
|
|
140
|
+
for (const passId of drawn) {
|
|
141
|
+
this.noteReplaced(regionKey, passId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
writeUniforms(data) {
|
|
146
|
+
this.uniformWrites.push(data.slice(0));
|
|
147
|
+
this.record('writeUniforms', data.byteLength);
|
|
148
|
+
}
|
|
149
|
+
beginFrame(clearR, clearG, clearB, clearA) {
|
|
150
|
+
this.record('beginFrame', clearR, clearG, clearB, clearA);
|
|
151
|
+
// A fresh frame starts unclipped on both real HALs — WebGL2 disables
|
|
152
|
+
// SCISSOR_TEST and sets the full viewport, WebGPU opens a render pass whose
|
|
153
|
+
// initial state is the whole attachment.
|
|
154
|
+
this.scissor = null;
|
|
155
|
+
this.viewport = null;
|
|
156
|
+
this.frameOpen = true;
|
|
157
|
+
this.drawnThisFrame.clear();
|
|
158
|
+
}
|
|
159
|
+
// Throws on an id the display never registered, where both real HALs return
|
|
160
|
+
// early instead — the mock is louder than production on purpose. A `drawPass`
|
|
161
|
+
// naming an unregistered pass draws nothing on either GPU backend, silently,
|
|
162
|
+
// while Canvas2D keeps painting correctly; and because this mock recorded the
|
|
163
|
+
// call anyway, the unit test asserting `callsOf('drawPass')` went green over
|
|
164
|
+
// it. That is the same failure `assertUniquePassIds` above exists for, seen
|
|
165
|
+
// from the draw side, and a typo'd or renamed id has no legitimate use — so
|
|
166
|
+
// there is nothing to preserve by matching the silent skip.
|
|
167
|
+
//
|
|
168
|
+
// `bufferPassId` is checked too: `drawPass(a, key, b)` runs pass `a`'s
|
|
169
|
+
// pipeline over pass `b`'s buffer, and every such pair in tree names two
|
|
170
|
+
// registered passes (canvas's chevron over line, continuation over rect).
|
|
171
|
+
drawPass(passId, regionKey, bufferPassId) {
|
|
172
|
+
this.assertRegistered(passId, 'passId');
|
|
173
|
+
if (bufferPassId !== undefined) {
|
|
174
|
+
this.assertRegistered(bufferPassId, 'bufferPassId');
|
|
175
|
+
}
|
|
176
|
+
this.record('drawPass', passId, regionKey, bufferPassId);
|
|
177
|
+
// The buffer the draw reads, which `bufferPassId` renames — that is the
|
|
178
|
+
// resource a later upload in this frame would be replacing under it.
|
|
179
|
+
const bufferKey = bufferPassId ?? passId;
|
|
180
|
+
let drawn = this.drawnThisFrame.get(regionKey);
|
|
181
|
+
if (!drawn) {
|
|
182
|
+
drawn = new Set();
|
|
183
|
+
this.drawnThisFrame.set(regionKey, drawn);
|
|
184
|
+
}
|
|
185
|
+
drawn.add(bufferKey);
|
|
186
|
+
this.drawLog.push({
|
|
187
|
+
passId,
|
|
188
|
+
regionKey,
|
|
189
|
+
bufferPassId,
|
|
190
|
+
scissor: this.scissor && { ...this.scissor },
|
|
191
|
+
viewport: this.viewport && { ...this.viewport },
|
|
192
|
+
uniformWrite: this.uniformWrites.length - 1,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
assertRegistered(passId, role) {
|
|
196
|
+
if (!this.descriptors.has(passId)) {
|
|
197
|
+
throw new Error(`drawPass ${role} '${passId}' is not a registered pass — this HAL holds ` +
|
|
198
|
+
`${[...this.descriptors.keys()].map(id => `'${id}'`).join(', ') || '(none)'}. ` +
|
|
199
|
+
`Both real HALs drop such a draw without a word, so on the GPU ` +
|
|
200
|
+
`backends this paints nothing while Canvas2D still looks right.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
endFrame() {
|
|
204
|
+
this.record('endFrame');
|
|
205
|
+
this.frameOpen = false;
|
|
206
|
+
}
|
|
207
|
+
setScissor(x, y, w, h) {
|
|
208
|
+
this.record('setScissor', x, y, w, h);
|
|
209
|
+
this.scissor = { x, y, w, h };
|
|
210
|
+
}
|
|
211
|
+
clearScissor() {
|
|
212
|
+
this.record('clearScissor');
|
|
213
|
+
this.scissor = null;
|
|
214
|
+
}
|
|
215
|
+
setViewport(x, y, w, h) {
|
|
216
|
+
this.record('setViewport', x, y, w, h);
|
|
217
|
+
this.viewport = { x, y, w, h };
|
|
218
|
+
}
|
|
219
|
+
clearViewport() {
|
|
220
|
+
this.record('clearViewport');
|
|
221
|
+
this.viewport = null;
|
|
222
|
+
}
|
|
223
|
+
// Test helpers
|
|
224
|
+
get lastUniforms() {
|
|
225
|
+
return this.uniformWrites.at(-1) ?? null;
|
|
226
|
+
}
|
|
227
|
+
getLastUniformsF32() {
|
|
228
|
+
return this.lastUniforms ? new Float32Array(this.lastUniforms) : null;
|
|
229
|
+
}
|
|
230
|
+
getLastUniformsU32() {
|
|
231
|
+
return this.lastUniforms ? new Uint32Array(this.lastUniforms) : null;
|
|
232
|
+
}
|
|
233
|
+
getLastUniformsI32() {
|
|
234
|
+
return this.lastUniforms ? new Int32Array(this.lastUniforms) : null;
|
|
235
|
+
}
|
|
236
|
+
// Every write of the frame, in order. `getLastUniforms*` is the same list's
|
|
237
|
+
// tail and stays the right call for a renderer that writes once.
|
|
238
|
+
getUniformWritesF32() {
|
|
239
|
+
return this.uniformWrites.map(u => new Float32Array(u));
|
|
240
|
+
}
|
|
241
|
+
getUniformWritesU32() {
|
|
242
|
+
return this.uniformWrites.map(u => new Uint32Array(u));
|
|
243
|
+
}
|
|
244
|
+
getBuffer(regionKey, passId) {
|
|
245
|
+
return this.regions.get(regionKey, passId);
|
|
246
|
+
}
|
|
247
|
+
getTexture(passId) {
|
|
248
|
+
return this.textures.get(passId);
|
|
249
|
+
}
|
|
250
|
+
callsOf(method) {
|
|
251
|
+
return this.calls.filter(c => c.method === method);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Every draw of the frame with the state it went out under, in order — the
|
|
255
|
+
* answer to "which columns did this pass paint into" and "which uniforms did
|
|
256
|
+
* it read", neither of which `callsOf('drawPass')` can give, because clip and
|
|
257
|
+
* uniforms are state rather than arguments. `scissor: null` is unclipped;
|
|
258
|
+
* `uniformWrite` indexes `getUniformWrites*`. See {@link MockDraw} for what
|
|
259
|
+
* each is guarding.
|
|
260
|
+
*/
|
|
261
|
+
draws() {
|
|
262
|
+
return this.drawLog;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The uniform bytes a recorded draw reads, as f32 — `draws()` joined to
|
|
266
|
+
* `getUniformWritesF32()` for you, since doing it by hand at every call site
|
|
267
|
+
* is how the index and the list drift apart. `null` when the draw preceded
|
|
268
|
+
* any write, which is the case no renderer should rely on: WebGPU clamps it
|
|
269
|
+
* to ring slot 0 and WebGL2 leaves the previous frame's UBO bound, so the two
|
|
270
|
+
* backends disagree about what it even means.
|
|
271
|
+
*/
|
|
272
|
+
/**
|
|
273
|
+
* `${regionKey}:${passId}` for each buffer this run replaced or deleted while
|
|
274
|
+
* the open frame had already drawn from it, in order — empty when a renderer
|
|
275
|
+
* never does that. See the field for why the sequence is worth asserting on
|
|
276
|
+
* either way.
|
|
277
|
+
*/
|
|
278
|
+
replacedWhileDrawn() {
|
|
279
|
+
return [...this.replacedWhileDrawnLog];
|
|
280
|
+
}
|
|
281
|
+
uniformsOf(draw) {
|
|
282
|
+
const buf = this.uniformWrites[draw.uniformWrite];
|
|
283
|
+
return buf ? new Float32Array(buf) : null;
|
|
284
|
+
}
|
|
285
|
+
reset() {
|
|
286
|
+
this.disposed = false;
|
|
287
|
+
this.calls = [];
|
|
288
|
+
this.regions.deleteAll();
|
|
289
|
+
this.uniformWrites = [];
|
|
290
|
+
this.drawLog = [];
|
|
291
|
+
this.scissor = null;
|
|
292
|
+
this.viewport = null;
|
|
293
|
+
this.frameOpen = false;
|
|
294
|
+
this.drawnThisFrame.clear();
|
|
295
|
+
this.replacedWhileDrawnLog = [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared out-of-memory / over-device-limit reporter for the GPU HALs. Both
|
|
3
|
+
* WebGPU and WebGL2 detect an over-limit allocation (a buffer past
|
|
4
|
+
* `maxBufferSize`, a texture past `maxTextureDimension2D`, or a frame that
|
|
5
|
+
* exhausts VRAM), then need to log it AND forward it to the display's
|
|
6
|
+
* renderError via the handler wired by `useRenderingBackend`. Centralized so
|
|
7
|
+
* the two HALs can't drift.
|
|
8
|
+
*/
|
|
9
|
+
export declare class OomReporter {
|
|
10
|
+
private hal;
|
|
11
|
+
private handler;
|
|
12
|
+
constructor(hal: string);
|
|
13
|
+
setHandler(handler: (error: Error) => void): void;
|
|
14
|
+
report(message: string): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared out-of-memory / over-device-limit reporter for the GPU HALs. Both
|
|
3
|
+
* WebGPU and WebGL2 detect an over-limit allocation (a buffer past
|
|
4
|
+
* `maxBufferSize`, a texture past `maxTextureDimension2D`, or a frame that
|
|
5
|
+
* exhausts VRAM), then need to log it AND forward it to the display's
|
|
6
|
+
* renderError via the handler wired by `useRenderingBackend`. Centralized so
|
|
7
|
+
* the two HALs can't drift.
|
|
8
|
+
*/
|
|
9
|
+
export class OomReporter {
|
|
10
|
+
hal;
|
|
11
|
+
handler = null;
|
|
12
|
+
constructor(hal) {
|
|
13
|
+
this.hal = hal;
|
|
14
|
+
}
|
|
15
|
+
setHandler(handler) {
|
|
16
|
+
this.handler = handler;
|
|
17
|
+
}
|
|
18
|
+
report(message) {
|
|
19
|
+
console.error(`[${this.hal}] ${message}`);
|
|
20
|
+
this.handler?.(new Error(message));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PipelineDescriptor } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Throw if two passes in one display's registry share an `id`.
|
|
4
|
+
*
|
|
5
|
+
* A pass id is two keys at once — the pipeline (`pipelines.set(desc.id, …)` in
|
|
6
|
+
* both HALs) and the instance buffer (`uploadBuffer(regionKey, passId, …)`).
|
|
7
|
+
* A duplicate therefore collides both: the second registration replaces the
|
|
8
|
+
* first's pipeline, both passes upload to one buffer so the last write wins,
|
|
9
|
+
* and whichever shader has the larger stride reads off the end of it. No
|
|
10
|
+
* validation error, no throw, no failing test — just wrong pixels on the GPU
|
|
11
|
+
* backends while Canvas2D keeps drawing correctly.
|
|
12
|
+
*
|
|
13
|
+
* Nothing structural stops it. A display's pass list is concatenated from
|
|
14
|
+
* several registries (`ALIGNMENTS_PASSES` merges three plus a standalone
|
|
15
|
+
* overlay pass), each `slangPass({ id })` sits in its own feature directory,
|
|
16
|
+
* and a new directory copied from a neighbour is exactly how the id would come
|
|
17
|
+
* along with the rest of the file.
|
|
18
|
+
*
|
|
19
|
+
* The id is reported rather than just counted, because the duplicate is a
|
|
20
|
+
* string the author typed and the fix is renaming one of the two.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertUniquePassIds(passes: readonly PipelineDescriptor[]): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw if two passes in one display's registry share an `id`.
|
|
3
|
+
*
|
|
4
|
+
* A pass id is two keys at once — the pipeline (`pipelines.set(desc.id, …)` in
|
|
5
|
+
* both HALs) and the instance buffer (`uploadBuffer(regionKey, passId, …)`).
|
|
6
|
+
* A duplicate therefore collides both: the second registration replaces the
|
|
7
|
+
* first's pipeline, both passes upload to one buffer so the last write wins,
|
|
8
|
+
* and whichever shader has the larger stride reads off the end of it. No
|
|
9
|
+
* validation error, no throw, no failing test — just wrong pixels on the GPU
|
|
10
|
+
* backends while Canvas2D keeps drawing correctly.
|
|
11
|
+
*
|
|
12
|
+
* Nothing structural stops it. A display's pass list is concatenated from
|
|
13
|
+
* several registries (`ALIGNMENTS_PASSES` merges three plus a standalone
|
|
14
|
+
* overlay pass), each `slangPass({ id })` sits in its own feature directory,
|
|
15
|
+
* and a new directory copied from a neighbour is exactly how the id would come
|
|
16
|
+
* along with the rest of the file.
|
|
17
|
+
*
|
|
18
|
+
* The id is reported rather than just counted, because the duplicate is a
|
|
19
|
+
* string the author typed and the fix is renaming one of the two.
|
|
20
|
+
*/
|
|
21
|
+
export function assertUniquePassIds(passes) {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const duplicates = new Set();
|
|
24
|
+
for (const { id } of passes) {
|
|
25
|
+
if (seen.has(id)) {
|
|
26
|
+
duplicates.add(id);
|
|
27
|
+
}
|
|
28
|
+
seen.add(id);
|
|
29
|
+
}
|
|
30
|
+
if (duplicates.size > 0) {
|
|
31
|
+
throw new Error(`duplicate pass id(s) ${[...duplicates].map(id => `'${id}'`).join(', ')}` +
|
|
32
|
+
` — a pass id keys both the pipeline and the instance buffer, so two` +
|
|
33
|
+
` passes sharing one draw each other's instances through their own` +
|
|
34
|
+
` shader. Rename one.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class RegionRegistry<Buf> {
|
|
2
|
+
private readonly destroy;
|
|
3
|
+
private regions;
|
|
4
|
+
constructor(destroy: (buf: Buf) => void);
|
|
5
|
+
get(regionKey: number, passId: string): Buf | undefined;
|
|
6
|
+
set(regionKey: number, passId: string, buf: Buf): void;
|
|
7
|
+
deleteBuffer(regionKey: number, passId: string): void;
|
|
8
|
+
deleteRegion(regionKey: number): void;
|
|
9
|
+
deleteAll(): void;
|
|
10
|
+
prune(active: Iterable<number>): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Per-(region, pass) buffer registry shared by WebGL2Hal and WebGPUHal.
|
|
2
|
+
// Owns the nested map and the destroy semantics so the two HALs only have to
|
|
3
|
+
// supply a leaf-buffer type and its destroy hook. Both HALs used to ship
|
|
4
|
+
// near-identical deleteBuffer/deleteRegion/deleteAll/prune/getOrCreate
|
|
5
|
+
// implementations; centralizing avoids drift when one side's lifecycle is
|
|
6
|
+
// tweaked and the other isn't. Contract pinned by regionRegistry.test.ts.
|
|
7
|
+
export class RegionRegistry {
|
|
8
|
+
destroy;
|
|
9
|
+
regions = new Map();
|
|
10
|
+
constructor(destroy) {
|
|
11
|
+
this.destroy = destroy;
|
|
12
|
+
}
|
|
13
|
+
get(regionKey, passId) {
|
|
14
|
+
return this.regions.get(regionKey)?.get(passId);
|
|
15
|
+
}
|
|
16
|
+
// Set the buffer for (regionKey, passId), auto-creating the region map on
|
|
17
|
+
// first write. Caller is responsible for destroying any prior entry first
|
|
18
|
+
// (typically via deleteBuffer); set itself does NOT call destroy.
|
|
19
|
+
set(regionKey, passId, buf) {
|
|
20
|
+
let region = this.regions.get(regionKey);
|
|
21
|
+
if (!region) {
|
|
22
|
+
region = new Map();
|
|
23
|
+
this.regions.set(regionKey, region);
|
|
24
|
+
}
|
|
25
|
+
region.set(passId, buf);
|
|
26
|
+
}
|
|
27
|
+
// Destroy and remove one (regionKey, passId) entry. No-op if absent.
|
|
28
|
+
deleteBuffer(regionKey, passId) {
|
|
29
|
+
const region = this.regions.get(regionKey);
|
|
30
|
+
if (region) {
|
|
31
|
+
const buf = region.get(passId);
|
|
32
|
+
if (buf) {
|
|
33
|
+
this.destroy(buf);
|
|
34
|
+
region.delete(passId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Destroy every buffer in `regionKey` and remove the region entry.
|
|
39
|
+
deleteRegion(regionKey) {
|
|
40
|
+
const region = this.regions.get(regionKey);
|
|
41
|
+
if (region) {
|
|
42
|
+
for (const buf of region.values()) {
|
|
43
|
+
this.destroy(buf);
|
|
44
|
+
}
|
|
45
|
+
this.regions.delete(regionKey);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Destroy every buffer everywhere and clear the registry.
|
|
49
|
+
deleteAll() {
|
|
50
|
+
for (const region of this.regions.values()) {
|
|
51
|
+
for (const buf of region.values()) {
|
|
52
|
+
this.destroy(buf);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.regions.clear();
|
|
56
|
+
}
|
|
57
|
+
// Delete every region whose key is NOT in `active`.
|
|
58
|
+
prune(active) {
|
|
59
|
+
const activeSet = new Set(active);
|
|
60
|
+
for (const regionKey of this.regions.keys()) {
|
|
61
|
+
if (!activeSet.has(regionKey)) {
|
|
62
|
+
this.deleteRegion(regionKey);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|