@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.
Files changed (164) hide show
  1. package/LICENSE +201 -0
  2. package/esm/OverlayCanvas.d.ts +29 -0
  3. package/esm/OverlayCanvas.js +43 -0
  4. package/esm/RenderCanvas.d.ts +76 -0
  5. package/esm/RenderCanvas.js +38 -0
  6. package/esm/RenderLifecycleMixin.d.ts +180 -0
  7. package/esm/RenderLifecycleMixin.js +279 -0
  8. package/esm/ScrollLockedOverlay.d.ts +28 -0
  9. package/esm/ScrollLockedOverlay.js +42 -0
  10. package/esm/blockClipUtils.d.ts +27 -0
  11. package/esm/blockClipUtils.js +101 -0
  12. package/esm/canvas2dUtils.d.ts +372 -0
  13. package/esm/canvas2dUtils.js +564 -0
  14. package/esm/canvasContext.d.ts +68 -0
  15. package/esm/canvasContext.js +74 -0
  16. package/esm/colorRampLut.d.ts +17 -0
  17. package/esm/colorRampLut.js +25 -0
  18. package/esm/coverageBand.d.ts +107 -0
  19. package/esm/coverageBand.js +109 -0
  20. package/esm/coverageBandBuffers.d.ts +30 -0
  21. package/esm/coverageBandBuffers.js +13 -0
  22. package/esm/createRenderingBackend.d.ts +52 -0
  23. package/esm/createRenderingBackend.js +58 -0
  24. package/esm/displayPhase.d.ts +155 -0
  25. package/esm/displayPhase.js +97 -0
  26. package/esm/globalRenderingBackend.d.ts +90 -0
  27. package/esm/globalRenderingBackend.js +53 -0
  28. package/esm/gpuDevice.d.ts +91 -0
  29. package/esm/gpuDevice.js +277 -0
  30. package/esm/graphicsCapabilities.d.ts +98 -0
  31. package/esm/graphicsCapabilities.js +165 -0
  32. package/esm/hal/createHal.d.ts +22 -0
  33. package/esm/hal/createHal.js +91 -0
  34. package/esm/hal/deviceGpuCache.d.ts +36 -0
  35. package/esm/hal/deviceGpuCache.js +120 -0
  36. package/esm/hal/gpuHalBase.d.ts +55 -0
  37. package/esm/hal/gpuHalBase.js +81 -0
  38. package/esm/hal/index.d.ts +7 -0
  39. package/esm/hal/index.js +5 -0
  40. package/esm/hal/mockHal.d.ts +153 -0
  41. package/esm/hal/mockHal.js +297 -0
  42. package/esm/hal/oomReporter.d.ts +15 -0
  43. package/esm/hal/oomReporter.js +22 -0
  44. package/esm/hal/passIds.d.ts +22 -0
  45. package/esm/hal/passIds.js +36 -0
  46. package/esm/hal/regionRegistry.d.ts +11 -0
  47. package/esm/hal/regionRegistry.js +66 -0
  48. package/esm/hal/types.d.ts +116 -0
  49. package/esm/hal/types.js +1 -0
  50. package/esm/hal/webgl2Hal.d.ts +47 -0
  51. package/esm/hal/webgl2Hal.js +490 -0
  52. package/esm/hal/webgpuHal.d.ts +169 -0
  53. package/esm/hal/webgpuHal.js +722 -0
  54. package/esm/installUpload.d.ts +91 -0
  55. package/esm/installUpload.js +94 -0
  56. package/esm/instanceCache.d.ts +43 -0
  57. package/esm/instanceCache.js +53 -0
  58. package/esm/instancePass.d.ts +50 -0
  59. package/esm/instancePass.js +34 -0
  60. package/esm/keyedRenderingBackend.d.ts +41 -0
  61. package/esm/keyedRenderingBackend.js +19 -0
  62. package/esm/mapUploadSync.d.ts +31 -0
  63. package/esm/mapUploadSync.js +45 -0
  64. package/esm/namedReactions.d.ts +29 -0
  65. package/esm/namedReactions.js +62 -0
  66. package/esm/perRegionRenderingBackend.d.ts +114 -0
  67. package/esm/perRegionRenderingBackend.js +93 -0
  68. package/esm/recoveryBudget.d.ts +40 -0
  69. package/esm/recoveryBudget.js +56 -0
  70. package/esm/regionDataMap.d.ts +60 -0
  71. package/esm/regionDataMap.js +98 -0
  72. package/esm/renderBlock.d.ts +25 -0
  73. package/esm/renderBlock.js +10 -0
  74. package/esm/renderingBackendBase.d.ts +71 -0
  75. package/esm/renderingBackendBase.js +50 -0
  76. package/esm/shaders/antialias.js.generated.d.ts +4 -0
  77. package/esm/shaders/antialias.js.generated.js +25 -0
  78. package/esm/shaders/capsule.generated.d.ts +1 -0
  79. package/esm/shaders/capsule.generated.js +3 -0
  80. package/esm/shaders/colorRampLut.generated.d.ts +1 -0
  81. package/esm/shaders/colorRampLut.generated.js +3 -0
  82. package/esm/shaders/coverageBand.generated.d.ts +2 -0
  83. package/esm/shaders/coverageBand.generated.js +4 -0
  84. package/esm/shaders/coverageBar.generated.d.ts +4 -0
  85. package/esm/shaders/coverageBar.generated.js +6 -0
  86. package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
  87. package/esm/shaders/coverageBar.iface.generated.js +114 -0
  88. package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
  89. package/esm/shaders/coverageIndicator.generated.js +6 -0
  90. package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
  91. package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
  92. package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
  93. package/esm/shaders/coverageInterbase.generated.js +6 -0
  94. package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
  95. package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
  96. package/esm/shaders/coverageMod.generated.d.ts +4 -0
  97. package/esm/shaders/coverageMod.generated.js +6 -0
  98. package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
  99. package/esm/shaders/coverageMod.iface.generated.js +144 -0
  100. package/esm/shaders/coverageSnp.generated.d.ts +4 -0
  101. package/esm/shaders/coverageSnp.generated.js +6 -0
  102. package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
  103. package/esm/shaders/coverageSnp.iface.generated.js +144 -0
  104. package/esm/shaders/hpmath.js.generated.d.ts +3 -0
  105. package/esm/shaders/hpmath.js.generated.js +48 -0
  106. package/esm/shaders/pointGlyph.generated.d.ts +1 -0
  107. package/esm/shaders/pointGlyph.generated.js +3 -0
  108. package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
  109. package/esm/shaders/pointGlyph.js.generated.js +7 -0
  110. package/esm/shaders/rowRect.generated.d.ts +3 -0
  111. package/esm/shaders/rowRect.generated.js +5 -0
  112. package/esm/shaders/rowRect.js.generated.d.ts +2 -0
  113. package/esm/shaders/rowRect.js.generated.js +13 -0
  114. package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
  115. package/esm/shaders/scoreScale.js.generated.js +46 -0
  116. package/esm/slangPass.d.ts +49 -0
  117. package/esm/slangPass.js +22 -0
  118. package/esm/useEventCallback.d.ts +1 -0
  119. package/esm/useEventCallback.js +19 -0
  120. package/esm/useGraphicsCapabilities.d.ts +17 -0
  121. package/esm/useGraphicsCapabilities.js +38 -0
  122. package/esm/useRenderingBackend.d.ts +83 -0
  123. package/esm/useRenderingBackend.js +422 -0
  124. package/esm/useTabVisibilityRerender.d.ts +13 -0
  125. package/esm/useTabVisibilityRerender.js +39 -0
  126. package/esm/webgpuUtils.d.ts +23 -0
  127. package/esm/webgpuUtils.js +76 -0
  128. package/package.json +91 -0
  129. package/src/shaders/antialias.js.generated.ts +33 -0
  130. package/src/shaders/antialias.slang +112 -0
  131. package/src/shaders/capsule.generated.ts +4 -0
  132. package/src/shaders/capsule.slang +62 -0
  133. package/src/shaders/colorPack.slang +26 -0
  134. package/src/shaders/colorRampLut.generated.ts +4 -0
  135. package/src/shaders/colorRampLut.slang +51 -0
  136. package/src/shaders/coverageBand.generated.ts +6 -0
  137. package/src/shaders/coverageBand.slang +276 -0
  138. package/src/shaders/coverageBar.generated.ts +10 -0
  139. package/src/shaders/coverageBar.iface.generated.ts +172 -0
  140. package/src/shaders/coverageBar.slang +37 -0
  141. package/src/shaders/coverageIndicator.generated.ts +10 -0
  142. package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
  143. package/src/shaders/coverageIndicator.slang +61 -0
  144. package/src/shaders/coverageInterbase.generated.ts +10 -0
  145. package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
  146. package/src/shaders/coverageInterbase.slang +63 -0
  147. package/src/shaders/coverageMod.generated.ts +10 -0
  148. package/src/shaders/coverageMod.iface.generated.ts +211 -0
  149. package/src/shaders/coverageMod.slang +36 -0
  150. package/src/shaders/coverageSnp.generated.ts +10 -0
  151. package/src/shaders/coverageSnp.iface.generated.ts +211 -0
  152. package/src/shaders/coverageSnp.slang +52 -0
  153. package/src/shaders/diagonalGrid.slang +25 -0
  154. package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
  155. package/src/shaders/hpmath.js.generated.ts +47 -0
  156. package/src/shaders/hpmath.slang +226 -0
  157. package/src/shaders/pointGlyph.generated.ts +4 -0
  158. package/src/shaders/pointGlyph.js.generated.ts +9 -0
  159. package/src/shaders/pointGlyph.slang +93 -0
  160. package/src/shaders/rowRect.generated.ts +8 -0
  161. package/src/shaders/rowRect.js.generated.ts +17 -0
  162. package/src/shaders/rowRect.slang +160 -0
  163. package/src/shaders/scoreScale.js.generated.ts +51 -0
  164. 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';
@@ -0,0 +1,5 @@
1
+ export { createGpuHal } from './createHal.js';
2
+ export { MockHal } from './mockHal.js';
3
+ export { assertUniquePassIds } from './passIds.js';
4
+ export { WebGL2Hal } from './webgl2Hal.js';
5
+ export { WebGPUHal } from './webgpuHal.js';
@@ -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
+ }