@motion-core/motion-gpu 0.4.1 → 0.5.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 (228) hide show
  1. package/README.md +99 -0
  2. package/dist/advanced.d.ts +1 -0
  3. package/dist/advanced.d.ts.map +1 -0
  4. package/dist/advanced.js +14 -6
  5. package/dist/core/advanced.d.ts +1 -0
  6. package/dist/core/advanced.d.ts.map +1 -0
  7. package/dist/core/advanced.js +14 -5
  8. package/dist/core/compute-shader.d.ts +87 -0
  9. package/dist/core/compute-shader.d.ts.map +1 -0
  10. package/dist/core/compute-shader.js +205 -0
  11. package/dist/core/compute-shader.js.map +1 -0
  12. package/dist/core/current-value.d.ts +1 -0
  13. package/dist/core/current-value.d.ts.map +1 -0
  14. package/dist/core/current-value.js +35 -34
  15. package/dist/core/current-value.js.map +1 -0
  16. package/dist/core/error-diagnostics.d.ts +1 -0
  17. package/dist/core/error-diagnostics.d.ts.map +1 -0
  18. package/dist/core/error-diagnostics.js +70 -137
  19. package/dist/core/error-diagnostics.js.map +1 -0
  20. package/dist/core/error-report.d.ts +2 -1
  21. package/dist/core/error-report.d.ts.map +1 -0
  22. package/dist/core/error-report.js +247 -233
  23. package/dist/core/error-report.js.map +1 -0
  24. package/dist/core/frame-registry.d.ts +1 -0
  25. package/dist/core/frame-registry.d.ts.map +1 -0
  26. package/dist/core/frame-registry.js +546 -662
  27. package/dist/core/frame-registry.js.map +1 -0
  28. package/dist/core/index.d.ts +6 -2
  29. package/dist/core/index.d.ts.map +1 -0
  30. package/dist/core/index.js +13 -12
  31. package/dist/core/material-preprocess.d.ts +1 -0
  32. package/dist/core/material-preprocess.d.ts.map +1 -0
  33. package/dist/core/material-preprocess.js +131 -152
  34. package/dist/core/material-preprocess.js.map +1 -0
  35. package/dist/core/material.d.ts +23 -6
  36. package/dist/core/material.d.ts.map +1 -0
  37. package/dist/core/material.js +290 -317
  38. package/dist/core/material.js.map +1 -0
  39. package/dist/core/recompile-policy.d.ts +1 -0
  40. package/dist/core/recompile-policy.d.ts.map +1 -0
  41. package/dist/core/recompile-policy.js +18 -13
  42. package/dist/core/recompile-policy.js.map +1 -0
  43. package/dist/core/render-graph.d.ts +8 -3
  44. package/dist/core/render-graph.d.ts.map +1 -0
  45. package/dist/core/render-graph.js +77 -68
  46. package/dist/core/render-graph.js.map +1 -0
  47. package/dist/core/render-targets.d.ts +1 -0
  48. package/dist/core/render-targets.d.ts.map +1 -0
  49. package/dist/core/render-targets.js +52 -53
  50. package/dist/core/render-targets.js.map +1 -0
  51. package/dist/core/renderer.d.ts +1 -0
  52. package/dist/core/renderer.d.ts.map +1 -0
  53. package/dist/core/renderer.js +1337 -1081
  54. package/dist/core/renderer.js.map +1 -0
  55. package/dist/core/runtime-loop.d.ts +3 -2
  56. package/dist/core/runtime-loop.d.ts.map +1 -0
  57. package/dist/core/runtime-loop.js +353 -362
  58. package/dist/core/runtime-loop.js.map +1 -0
  59. package/dist/core/scheduler-helpers.d.ts +1 -0
  60. package/dist/core/scheduler-helpers.d.ts.map +1 -0
  61. package/dist/core/scheduler-helpers.js +52 -51
  62. package/dist/core/scheduler-helpers.js.map +1 -0
  63. package/dist/core/shader.d.ts +10 -1
  64. package/dist/core/shader.d.ts.map +1 -0
  65. package/dist/core/shader.js +109 -115
  66. package/dist/core/shader.js.map +1 -0
  67. package/dist/core/storage-buffers.d.ts +37 -0
  68. package/dist/core/storage-buffers.d.ts.map +1 -0
  69. package/dist/core/storage-buffers.js +95 -0
  70. package/dist/core/storage-buffers.js.map +1 -0
  71. package/dist/core/texture-loader.d.ts +1 -0
  72. package/dist/core/texture-loader.d.ts.map +1 -0
  73. package/dist/core/texture-loader.js +209 -273
  74. package/dist/core/texture-loader.js.map +1 -0
  75. package/dist/core/textures.d.ts +13 -0
  76. package/dist/core/textures.d.ts.map +1 -0
  77. package/dist/core/textures.js +111 -116
  78. package/dist/core/textures.js.map +1 -0
  79. package/dist/core/types.d.ts +147 -4
  80. package/dist/core/types.d.ts.map +1 -0
  81. package/dist/core/types.js +0 -4
  82. package/dist/core/uniforms.d.ts +1 -0
  83. package/dist/core/uniforms.d.ts.map +1 -0
  84. package/dist/core/uniforms.js +170 -191
  85. package/dist/core/uniforms.js.map +1 -0
  86. package/dist/index.d.ts +1 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +13 -6
  89. package/dist/passes/BlitPass.d.ts +1 -0
  90. package/dist/passes/BlitPass.d.ts.map +1 -0
  91. package/dist/passes/BlitPass.js +23 -18
  92. package/dist/passes/BlitPass.js.map +1 -0
  93. package/dist/passes/ComputePass.d.ts +83 -0
  94. package/dist/passes/ComputePass.d.ts.map +1 -0
  95. package/dist/passes/ComputePass.js +92 -0
  96. package/dist/passes/ComputePass.js.map +1 -0
  97. package/dist/passes/CopyPass.d.ts +1 -0
  98. package/dist/passes/CopyPass.d.ts.map +1 -0
  99. package/dist/passes/CopyPass.js +58 -52
  100. package/dist/passes/CopyPass.js.map +1 -0
  101. package/dist/passes/FullscreenPass.d.ts +1 -0
  102. package/dist/passes/FullscreenPass.d.ts.map +1 -0
  103. package/dist/passes/FullscreenPass.js +127 -130
  104. package/dist/passes/FullscreenPass.js.map +1 -0
  105. package/dist/passes/PingPongComputePass.d.ts +104 -0
  106. package/dist/passes/PingPongComputePass.d.ts.map +1 -0
  107. package/dist/passes/PingPongComputePass.js +132 -0
  108. package/dist/passes/PingPongComputePass.js.map +1 -0
  109. package/dist/passes/ShaderPass.d.ts +1 -0
  110. package/dist/passes/ShaderPass.d.ts.map +1 -0
  111. package/dist/passes/ShaderPass.js +41 -37
  112. package/dist/passes/ShaderPass.js.map +1 -0
  113. package/dist/passes/index.d.ts +3 -0
  114. package/dist/passes/index.d.ts.map +1 -0
  115. package/dist/passes/index.js +6 -3
  116. package/dist/react/FragCanvas.d.ts +3 -2
  117. package/dist/react/FragCanvas.d.ts.map +1 -0
  118. package/dist/react/FragCanvas.js +234 -211
  119. package/dist/react/FragCanvas.js.map +1 -0
  120. package/dist/react/MotionGPUErrorOverlay.d.ts +1 -0
  121. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -0
  122. package/dist/react/MotionGPUErrorOverlay.js +200 -14
  123. package/dist/react/MotionGPUErrorOverlay.js.map +1 -0
  124. package/dist/react/Portal.d.ts +1 -0
  125. package/dist/react/Portal.d.ts.map +1 -0
  126. package/dist/react/Portal.js +18 -21
  127. package/dist/react/Portal.js.map +1 -0
  128. package/dist/react/advanced.d.ts +1 -0
  129. package/dist/react/advanced.d.ts.map +1 -0
  130. package/dist/react/advanced.js +14 -6
  131. package/dist/react/frame-context.d.ts +1 -0
  132. package/dist/react/frame-context.d.ts.map +1 -0
  133. package/dist/react/frame-context.js +88 -94
  134. package/dist/react/frame-context.js.map +1 -0
  135. package/dist/react/index.d.ts +6 -2
  136. package/dist/react/index.d.ts.map +1 -0
  137. package/dist/react/index.js +12 -9
  138. package/dist/react/motiongpu-context.d.ts +1 -0
  139. package/dist/react/motiongpu-context.d.ts.map +1 -0
  140. package/dist/react/motiongpu-context.js +18 -15
  141. package/dist/react/motiongpu-context.js.map +1 -0
  142. package/dist/react/use-motiongpu-user-context.d.ts +1 -0
  143. package/dist/react/use-motiongpu-user-context.d.ts.map +1 -0
  144. package/dist/react/use-motiongpu-user-context.js +83 -82
  145. package/dist/react/use-motiongpu-user-context.js.map +1 -0
  146. package/dist/react/use-texture.d.ts +1 -0
  147. package/dist/react/use-texture.d.ts.map +1 -0
  148. package/dist/react/use-texture.js +132 -152
  149. package/dist/react/use-texture.js.map +1 -0
  150. package/dist/svelte/FragCanvas.svelte +2 -2
  151. package/dist/svelte/FragCanvas.svelte.d.ts +3 -2
  152. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -0
  153. package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
  154. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts +1 -0
  155. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -0
  156. package/dist/svelte/Portal.svelte.d.ts +1 -0
  157. package/dist/svelte/Portal.svelte.d.ts.map +1 -0
  158. package/dist/svelte/advanced.d.ts +1 -0
  159. package/dist/svelte/advanced.d.ts.map +1 -0
  160. package/dist/svelte/advanced.js +13 -6
  161. package/dist/svelte/frame-context.d.ts +1 -0
  162. package/dist/svelte/frame-context.d.ts.map +1 -0
  163. package/dist/svelte/frame-context.js +27 -27
  164. package/dist/svelte/frame-context.js.map +1 -0
  165. package/dist/svelte/index.d.ts +6 -2
  166. package/dist/svelte/index.d.ts.map +1 -0
  167. package/dist/svelte/index.js +12 -9
  168. package/dist/svelte/motiongpu-context.d.ts +1 -0
  169. package/dist/svelte/motiongpu-context.d.ts.map +1 -0
  170. package/dist/svelte/motiongpu-context.js +24 -21
  171. package/dist/svelte/motiongpu-context.js.map +1 -0
  172. package/dist/svelte/use-motiongpu-user-context.d.ts +1 -0
  173. package/dist/svelte/use-motiongpu-user-context.d.ts.map +1 -0
  174. package/dist/svelte/use-motiongpu-user-context.js +69 -70
  175. package/dist/svelte/use-motiongpu-user-context.js.map +1 -0
  176. package/dist/svelte/use-texture.d.ts +1 -0
  177. package/dist/svelte/use-texture.d.ts.map +1 -0
  178. package/dist/svelte/use-texture.js +125 -147
  179. package/dist/svelte/use-texture.js.map +1 -0
  180. package/package.json +12 -7
  181. package/src/lib/advanced.ts +6 -0
  182. package/src/lib/core/advanced.ts +12 -0
  183. package/src/lib/core/compute-shader.ts +326 -0
  184. package/src/lib/core/current-value.ts +64 -0
  185. package/src/lib/core/error-diagnostics.ts +236 -0
  186. package/src/lib/core/error-report.ts +535 -0
  187. package/src/lib/core/frame-registry.ts +1190 -0
  188. package/src/lib/core/index.ts +94 -0
  189. package/src/lib/core/material-preprocess.ts +295 -0
  190. package/src/lib/core/material.ts +748 -0
  191. package/src/lib/core/recompile-policy.ts +31 -0
  192. package/src/lib/core/render-graph.ts +173 -0
  193. package/src/lib/core/render-targets.ts +107 -0
  194. package/src/lib/core/renderer.ts +2161 -0
  195. package/src/lib/core/runtime-loop.ts +537 -0
  196. package/src/lib/core/scheduler-helpers.ts +136 -0
  197. package/src/lib/core/shader.ts +301 -0
  198. package/src/lib/core/storage-buffers.ts +142 -0
  199. package/src/lib/core/texture-loader.ts +482 -0
  200. package/src/lib/core/textures.ts +257 -0
  201. package/src/lib/core/types.ts +743 -0
  202. package/src/lib/core/uniforms.ts +282 -0
  203. package/src/lib/index.ts +6 -0
  204. package/src/lib/passes/BlitPass.ts +54 -0
  205. package/src/lib/passes/ComputePass.ts +136 -0
  206. package/src/lib/passes/CopyPass.ts +80 -0
  207. package/src/lib/passes/FullscreenPass.ts +173 -0
  208. package/src/lib/passes/PingPongComputePass.ts +180 -0
  209. package/src/lib/passes/ShaderPass.ts +89 -0
  210. package/src/lib/passes/index.ts +9 -0
  211. package/src/lib/react/FragCanvas.tsx +345 -0
  212. package/src/lib/react/MotionGPUErrorOverlay.tsx +524 -0
  213. package/src/lib/react/Portal.tsx +34 -0
  214. package/src/lib/react/advanced.ts +36 -0
  215. package/src/lib/react/frame-context.ts +169 -0
  216. package/src/lib/react/index.ts +68 -0
  217. package/src/lib/react/motiongpu-context.ts +88 -0
  218. package/src/lib/react/use-motiongpu-user-context.ts +186 -0
  219. package/src/lib/react/use-texture.ts +233 -0
  220. package/src/lib/svelte/FragCanvas.svelte +249 -0
  221. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +512 -0
  222. package/src/lib/svelte/Portal.svelte +31 -0
  223. package/src/lib/svelte/advanced.ts +32 -0
  224. package/src/lib/svelte/frame-context.ts +87 -0
  225. package/src/lib/svelte/index.ts +68 -0
  226. package/src/lib/svelte/motiongpu-context.ts +97 -0
  227. package/src/lib/svelte/use-motiongpu-user-context.ts +145 -0
  228. package/src/lib/svelte/use-texture.ts +232 -0
@@ -0,0 +1,2161 @@
1
+ import { buildRenderTargetSignature, resolveRenderTargetDefinitions } from './render-targets.js';
2
+ import { planRenderGraph, type RenderGraphPlan } from './render-graph.js';
3
+ import {
4
+ buildShaderSourceWithMap,
5
+ formatShaderSourceLocation,
6
+ type ShaderLineMap
7
+ } from './shader.js';
8
+ import {
9
+ attachShaderCompilationDiagnostics,
10
+ type ShaderCompilationRuntimeContext
11
+ } from './error-diagnostics.js';
12
+ import {
13
+ getTextureMipLevelCount,
14
+ normalizeTextureDefinitions,
15
+ resolveTextureUpdateMode,
16
+ resolveTextureSize,
17
+ toTextureData
18
+ } from './textures.js';
19
+ import { packUniformsInto } from './uniforms.js';
20
+ import {
21
+ buildComputeShaderSource,
22
+ buildPingPongComputeShaderSource,
23
+ extractWorkgroupSize,
24
+ storageTextureSampleScalarType
25
+ } from './compute-shader.js';
26
+ import { normalizeStorageBufferDefinition } from './storage-buffers.js';
27
+ import type {
28
+ AnyPass,
29
+ RenderPass,
30
+ RenderPassInputSlot,
31
+ RenderPassOutputSlot,
32
+ RenderMode,
33
+ RenderTarget,
34
+ Renderer,
35
+ RendererOptions,
36
+ StorageBufferAccess,
37
+ StorageBufferType,
38
+ TextureSource,
39
+ TextureUpdateMode,
40
+ TextureValue
41
+ } from './types.js';
42
+
43
+ /**
44
+ * Binding index for frame uniforms (`time`, `delta`, `resolution`).
45
+ */
46
+ const FRAME_BINDING = 0;
47
+
48
+ /**
49
+ * Binding index for material uniform buffer.
50
+ */
51
+ const UNIFORM_BINDING = 1;
52
+
53
+ /**
54
+ * First binding index used for texture sampler/texture pairs.
55
+ */
56
+ const FIRST_TEXTURE_BINDING = 2;
57
+
58
+ /**
59
+ * Runtime texture binding state associated with a single texture key.
60
+ */
61
+ interface RuntimeTextureBinding {
62
+ key: string;
63
+ samplerBinding: number;
64
+ textureBinding: number;
65
+ sampler: GPUSampler;
66
+ fallbackTexture: GPUTexture;
67
+ fallbackView: GPUTextureView;
68
+ texture: GPUTexture | null;
69
+ view: GPUTextureView;
70
+ source: TextureSource | null;
71
+ width: number | undefined;
72
+ height: number | undefined;
73
+ mipLevelCount: number;
74
+ format: GPUTextureFormat;
75
+ colorSpace: 'srgb' | 'linear';
76
+ defaultColorSpace: 'srgb' | 'linear';
77
+ flipY: boolean;
78
+ defaultFlipY: boolean;
79
+ generateMipmaps: boolean;
80
+ defaultGenerateMipmaps: boolean;
81
+ premultipliedAlpha: boolean;
82
+ defaultPremultipliedAlpha: boolean;
83
+ update: TextureUpdateMode;
84
+ defaultUpdate?: TextureUpdateMode;
85
+ lastToken: TextureValue;
86
+ }
87
+
88
+ /**
89
+ * Runtime render target allocation metadata.
90
+ */
91
+ interface RuntimeRenderTarget {
92
+ texture: GPUTexture;
93
+ view: GPUTextureView;
94
+ width: number;
95
+ height: number;
96
+ format: GPUTextureFormat;
97
+ }
98
+
99
+ /**
100
+ * Runtime ping-pong storage textures for a single logical target key.
101
+ */
102
+ interface PingPongTexturePair {
103
+ target: string;
104
+ format: GPUTextureFormat;
105
+ width: number;
106
+ height: number;
107
+ textureA: GPUTexture;
108
+ viewA: GPUTextureView;
109
+ textureB: GPUTexture;
110
+ viewB: GPUTextureView;
111
+ bindGroupLayout: GPUBindGroupLayout;
112
+ }
113
+
114
+ /**
115
+ * Cached pass properties used to validate render-graph cache correctness.
116
+ */
117
+ interface RenderGraphPassSnapshot {
118
+ pass: AnyPass;
119
+ enabled: RenderPass['enabled'];
120
+ needsSwap: RenderPass['needsSwap'];
121
+ input: RenderPass['input'];
122
+ output: RenderPass['output'];
123
+ clear: RenderPass['clear'];
124
+ preserve: RenderPass['preserve'];
125
+ hasClearColor: boolean;
126
+ clearColor0: number;
127
+ clearColor1: number;
128
+ clearColor2: number;
129
+ clearColor3: number;
130
+ }
131
+
132
+ /**
133
+ * Returns sampler/texture binding slots for a texture index.
134
+ */
135
+ function getTextureBindings(index: number): {
136
+ samplerBinding: number;
137
+ textureBinding: number;
138
+ } {
139
+ const samplerBinding = FIRST_TEXTURE_BINDING + index * 2;
140
+ return {
141
+ samplerBinding,
142
+ textureBinding: samplerBinding + 1
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Maps WGSL scalar texture type to WebGPU sampled texture bind-group sample type.
148
+ */
149
+ function toGpuTextureSampleType(type: 'f32' | 'u32' | 'i32'): GPUTextureSampleType {
150
+ if (type === 'u32') {
151
+ return 'uint';
152
+ }
153
+ if (type === 'i32') {
154
+ return 'sint';
155
+ }
156
+ return 'float';
157
+ }
158
+
159
+ /**
160
+ * Resizes canvas backing store to match client size and DPR.
161
+ */
162
+ function resizeCanvas(
163
+ canvas: HTMLCanvasElement,
164
+ dprInput: number,
165
+ cssSize?: { width: number; height: number }
166
+ ): { width: number; height: number } {
167
+ const dpr = Number.isFinite(dprInput) && dprInput > 0 ? dprInput : 1;
168
+ const rect = cssSize ? null : canvas.getBoundingClientRect();
169
+ const cssWidth = Math.max(0, cssSize?.width ?? rect?.width ?? 0);
170
+ const cssHeight = Math.max(0, cssSize?.height ?? rect?.height ?? 0);
171
+ const width = Math.max(1, Math.floor((cssWidth || 1) * dpr));
172
+ const height = Math.max(1, Math.floor((cssHeight || 1) * dpr));
173
+
174
+ if (canvas.width !== width || canvas.height !== height) {
175
+ canvas.width = width;
176
+ canvas.height = height;
177
+ }
178
+
179
+ return { width, height };
180
+ }
181
+
182
+ /**
183
+ * Throws when a shader module contains WGSL compilation errors.
184
+ */
185
+ async function assertCompilation(
186
+ module: GPUShaderModule,
187
+ options?: {
188
+ lineMap?: ShaderLineMap;
189
+ fragmentSource?: string;
190
+ includeSources?: Record<string, string>;
191
+ defineBlockSource?: string;
192
+ materialSource?: {
193
+ component?: string;
194
+ file?: string;
195
+ line?: number;
196
+ column?: number;
197
+ functionName?: string;
198
+ } | null;
199
+ runtimeContext?: ShaderCompilationRuntimeContext;
200
+ }
201
+ ): Promise<void> {
202
+ const info = await module.getCompilationInfo();
203
+ const errors = info.messages.filter((message: GPUCompilationMessage) => message.type === 'error');
204
+
205
+ if (errors.length === 0) {
206
+ return;
207
+ }
208
+
209
+ const diagnostics = errors.map((message: GPUCompilationMessage) => ({
210
+ generatedLine: message.lineNum,
211
+ message: message.message,
212
+ linePos: message.linePos,
213
+ lineLength: message.length,
214
+ sourceLocation: options?.lineMap?.[message.lineNum] ?? null
215
+ }));
216
+
217
+ const summary = diagnostics
218
+ .map((diagnostic) => {
219
+ const sourceLabel = formatShaderSourceLocation(diagnostic.sourceLocation);
220
+ const generatedLineLabel =
221
+ diagnostic.generatedLine > 0 ? `generated WGSL line ${diagnostic.generatedLine}` : null;
222
+ const contextLabel = [sourceLabel, generatedLineLabel].filter((value) => Boolean(value));
223
+ if (contextLabel.length === 0) {
224
+ return diagnostic.message;
225
+ }
226
+
227
+ return `[${contextLabel.join(' | ')}] ${diagnostic.message}`;
228
+ })
229
+ .join('\n');
230
+ const error = new Error(`WGSL compilation failed:\n${summary}`);
231
+ throw attachShaderCompilationDiagnostics(error, {
232
+ kind: 'shader-compilation',
233
+ diagnostics,
234
+ fragmentSource: options?.fragmentSource ?? '',
235
+ includeSources: options?.includeSources ?? {},
236
+ ...(options?.defineBlockSource !== undefined
237
+ ? { defineBlockSource: options.defineBlockSource }
238
+ : {}),
239
+ materialSource: options?.materialSource ?? null,
240
+ ...(options?.runtimeContext !== undefined ? { runtimeContext: options.runtimeContext } : {})
241
+ });
242
+ }
243
+
244
+ function toSortedUniqueStrings(values: string[]): string[] {
245
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
246
+ }
247
+
248
+ function buildPassGraphSnapshot(
249
+ passes: AnyPass[] | undefined
250
+ ): NonNullable<ShaderCompilationRuntimeContext['passGraph']> {
251
+ const declaredPasses = passes ?? [];
252
+ let enabledPassCount = 0;
253
+ const inputs: string[] = [];
254
+ const outputs: string[] = [];
255
+
256
+ for (const pass of declaredPasses) {
257
+ if (pass.enabled === false) {
258
+ continue;
259
+ }
260
+
261
+ enabledPassCount += 1;
262
+ if ('isCompute' in pass && (pass as { isCompute?: boolean }).isCompute === true) {
263
+ continue;
264
+ }
265
+ const rp = pass as RenderPass;
266
+ const needsSwap = rp.needsSwap ?? true;
267
+ const input = rp.input ?? 'source';
268
+ const output = rp.output ?? (needsSwap ? 'target' : 'source');
269
+ inputs.push(input);
270
+ outputs.push(output);
271
+ }
272
+
273
+ return {
274
+ passCount: declaredPasses.length,
275
+ enabledPassCount,
276
+ inputs: toSortedUniqueStrings(inputs),
277
+ outputs: toSortedUniqueStrings(outputs)
278
+ };
279
+ }
280
+
281
+ function buildShaderCompilationRuntimeContext(
282
+ options: RendererOptions
283
+ ): ShaderCompilationRuntimeContext {
284
+ const passList = options.getPasses?.() ?? options.passes;
285
+ const renderTargetMap = options.getRenderTargets?.() ?? options.renderTargets;
286
+
287
+ return {
288
+ ...(options.materialSignature ? { materialSignature: options.materialSignature } : {}),
289
+ passGraph: buildPassGraphSnapshot(passList),
290
+ activeRenderTargets: Object.keys(renderTargetMap ?? {}).sort((a, b) => a.localeCompare(b))
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Creates a 1x1 white fallback texture used before user textures become available.
296
+ */
297
+ function createFallbackTexture(device: GPUDevice, format: GPUTextureFormat): GPUTexture {
298
+ const texture = device.createTexture({
299
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
300
+ format,
301
+ usage:
302
+ GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
303
+ });
304
+
305
+ const pixel = new Uint8Array([255, 255, 255, 255]);
306
+ device.queue.writeTexture(
307
+ { texture },
308
+ pixel,
309
+ { offset: 0, bytesPerRow: 4, rowsPerImage: 1 },
310
+ { width: 1, height: 1, depthOrArrayLayers: 1 }
311
+ );
312
+
313
+ return texture;
314
+ }
315
+
316
+ /**
317
+ * Creates an offscreen canvas used for CPU mipmap generation.
318
+ */
319
+ function createMipmapCanvas(width: number, height: number): OffscreenCanvas | HTMLCanvasElement {
320
+ if (typeof OffscreenCanvas !== 'undefined') {
321
+ return new OffscreenCanvas(width, height);
322
+ }
323
+
324
+ const canvas = document.createElement('canvas');
325
+ canvas.width = width;
326
+ canvas.height = height;
327
+ return canvas;
328
+ }
329
+
330
+ /**
331
+ * Creates typed descriptor for `copyExternalImageToTexture`.
332
+ */
333
+ function createExternalCopySource(
334
+ source: CanvasImageSource,
335
+ options: { flipY?: boolean; premultipliedAlpha?: boolean }
336
+ ): GPUCopyExternalImageSourceInfo {
337
+ const descriptor = {
338
+ source,
339
+ ...(options.flipY ? { flipY: true } : {}),
340
+ ...(options.premultipliedAlpha ? { premultipliedAlpha: true } : {})
341
+ };
342
+
343
+ return descriptor as GPUCopyExternalImageSourceInfo;
344
+ }
345
+
346
+ /**
347
+ * Uploads source content to a GPU texture and optionally generates mip chain on CPU.
348
+ */
349
+ function uploadTexture(
350
+ device: GPUDevice,
351
+ texture: GPUTexture,
352
+ binding: Pick<RuntimeTextureBinding, 'flipY' | 'premultipliedAlpha' | 'generateMipmaps'>,
353
+ source: TextureSource,
354
+ width: number,
355
+ height: number,
356
+ mipLevelCount: number
357
+ ): void {
358
+ device.queue.copyExternalImageToTexture(
359
+ createExternalCopySource(source, {
360
+ flipY: binding.flipY,
361
+ premultipliedAlpha: binding.premultipliedAlpha
362
+ }),
363
+ { texture, mipLevel: 0 },
364
+ { width, height, depthOrArrayLayers: 1 }
365
+ );
366
+
367
+ if (!binding.generateMipmaps || mipLevelCount <= 1) {
368
+ return;
369
+ }
370
+
371
+ let previousSource: CanvasImageSource = source;
372
+ let previousWidth = width;
373
+ let previousHeight = height;
374
+
375
+ for (let level = 1; level < mipLevelCount; level += 1) {
376
+ const nextWidth = Math.max(1, Math.floor(previousWidth / 2));
377
+ const nextHeight = Math.max(1, Math.floor(previousHeight / 2));
378
+ const canvas = createMipmapCanvas(nextWidth, nextHeight);
379
+ const context = canvas.getContext('2d');
380
+ if (!context) {
381
+ throw new Error('Unable to create 2D context for mipmap generation');
382
+ }
383
+
384
+ context.drawImage(
385
+ previousSource,
386
+ 0,
387
+ 0,
388
+ previousWidth,
389
+ previousHeight,
390
+ 0,
391
+ 0,
392
+ nextWidth,
393
+ nextHeight
394
+ );
395
+
396
+ device.queue.copyExternalImageToTexture(
397
+ createExternalCopySource(canvas, {
398
+ premultipliedAlpha: binding.premultipliedAlpha
399
+ }),
400
+ { texture, mipLevel: level },
401
+ { width: nextWidth, height: nextHeight, depthOrArrayLayers: 1 }
402
+ );
403
+
404
+ previousSource = canvas;
405
+ previousWidth = nextWidth;
406
+ previousHeight = nextHeight;
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Creates bind group layout entries for frame/uniform buffers plus texture bindings.
412
+ */
413
+ function createBindGroupLayoutEntries(
414
+ textureBindings: RuntimeTextureBinding[]
415
+ ): GPUBindGroupLayoutEntry[] {
416
+ const entries: GPUBindGroupLayoutEntry[] = [
417
+ {
418
+ binding: FRAME_BINDING,
419
+ visibility: GPUShaderStage.FRAGMENT,
420
+ buffer: { type: 'uniform', minBindingSize: 16 }
421
+ },
422
+ {
423
+ binding: UNIFORM_BINDING,
424
+ visibility: GPUShaderStage.FRAGMENT,
425
+ buffer: { type: 'uniform' }
426
+ }
427
+ ];
428
+
429
+ for (const binding of textureBindings) {
430
+ entries.push({
431
+ binding: binding.samplerBinding,
432
+ visibility: GPUShaderStage.FRAGMENT,
433
+ sampler: { type: 'filtering' }
434
+ });
435
+
436
+ entries.push({
437
+ binding: binding.textureBinding,
438
+ visibility: GPUShaderStage.FRAGMENT,
439
+ texture: {
440
+ sampleType: 'float',
441
+ viewDimension: '2d',
442
+ multisampled: false
443
+ }
444
+ });
445
+ }
446
+
447
+ return entries;
448
+ }
449
+
450
+ /**
451
+ * Maximum gap (in floats) between two dirty ranges that triggers merge.
452
+ *
453
+ * Set to 4 (16 bytes) which covers one vec4f alignment slot.
454
+ */
455
+ const DIRTY_RANGE_MERGE_GAP = 4;
456
+
457
+ /**
458
+ * Computes dirty float ranges between two uniform snapshots.
459
+ *
460
+ * Adjacent dirty ranges separated by a gap smaller than or equal to
461
+ * {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
462
+ */
463
+ export function findDirtyFloatRanges(
464
+ previous: Float32Array,
465
+ next: Float32Array,
466
+ mergeGapThreshold = DIRTY_RANGE_MERGE_GAP
467
+ ): Array<{ start: number; count: number }> {
468
+ const ranges: Array<{ start: number; count: number }> = [];
469
+ let start = -1;
470
+
471
+ for (let index = 0; index < next.length; index += 1) {
472
+ if (previous[index] !== next[index]) {
473
+ if (start === -1) {
474
+ start = index;
475
+ }
476
+ continue;
477
+ }
478
+
479
+ if (start !== -1) {
480
+ ranges.push({ start, count: index - start });
481
+ start = -1;
482
+ }
483
+ }
484
+
485
+ if (start !== -1) {
486
+ ranges.push({ start, count: next.length - start });
487
+ }
488
+
489
+ if (ranges.length <= 1) {
490
+ return ranges;
491
+ }
492
+
493
+ const merged: Array<{ start: number; count: number }> = [ranges[0]!];
494
+ for (let index = 1; index < ranges.length; index += 1) {
495
+ const prev = merged[merged.length - 1]!;
496
+ const curr = ranges[index]!;
497
+ const gap = curr.start - (prev.start + prev.count);
498
+
499
+ if (gap <= mergeGapThreshold) {
500
+ prev.count = curr.start + curr.count - prev.start;
501
+ } else {
502
+ merged.push(curr);
503
+ }
504
+ }
505
+
506
+ return merged;
507
+ }
508
+
509
+ /**
510
+ * Determines whether shader output should perform linear-to-sRGB conversion.
511
+ */
512
+ function shouldConvertLinearToSrgb(
513
+ outputColorSpace: 'srgb' | 'linear',
514
+ canvasFormat: GPUTextureFormat
515
+ ): boolean {
516
+ if (outputColorSpace !== 'srgb') {
517
+ return false;
518
+ }
519
+
520
+ return !canvasFormat.endsWith('-srgb');
521
+ }
522
+
523
+ /**
524
+ * WGSL shader used to blit an offscreen texture to the canvas.
525
+ */
526
+ function createFullscreenBlitShader(): string {
527
+ return `
528
+ struct MotionGPUVertexOut {
529
+ @builtin(position) position: vec4f,
530
+ @location(0) uv: vec2f,
531
+ };
532
+
533
+ @group(0) @binding(0) var motiongpuBlitSampler: sampler;
534
+ @group(0) @binding(1) var motiongpuBlitTexture: texture_2d<f32>;
535
+
536
+ @vertex
537
+ fn motiongpuBlitVertex(@builtin(vertex_index) index: u32) -> MotionGPUVertexOut {
538
+ var positions = array<vec2f, 3>(
539
+ vec2f(-1.0, -3.0),
540
+ vec2f(-1.0, 1.0),
541
+ vec2f(3.0, 1.0)
542
+ );
543
+
544
+ let position = positions[index];
545
+ var out: MotionGPUVertexOut;
546
+ out.position = vec4f(position, 0.0, 1.0);
547
+ out.uv = (position + vec2f(1.0, 1.0)) * 0.5;
548
+ return out;
549
+ }
550
+
551
+ @fragment
552
+ fn motiongpuBlitFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
553
+ return textureSample(motiongpuBlitTexture, motiongpuBlitSampler, in.uv);
554
+ }
555
+ `;
556
+ }
557
+
558
+ /**
559
+ * Allocates a render target texture with usage flags suitable for passes/blits.
560
+ */
561
+ function createRenderTexture(
562
+ device: GPUDevice,
563
+ width: number,
564
+ height: number,
565
+ format: GPUTextureFormat
566
+ ): RuntimeRenderTarget {
567
+ const texture = device.createTexture({
568
+ size: { width, height, depthOrArrayLayers: 1 },
569
+ format,
570
+ usage:
571
+ GPUTextureUsage.TEXTURE_BINDING |
572
+ GPUTextureUsage.RENDER_ATTACHMENT |
573
+ GPUTextureUsage.COPY_DST |
574
+ GPUTextureUsage.COPY_SRC
575
+ });
576
+
577
+ return {
578
+ texture,
579
+ view: texture.createView(),
580
+ width,
581
+ height,
582
+ format
583
+ };
584
+ }
585
+
586
+ /**
587
+ * Destroys a render target texture if present.
588
+ */
589
+ function destroyRenderTexture(target: RuntimeRenderTarget | null): void {
590
+ target?.texture.destroy();
591
+ }
592
+
593
+ /**
594
+ * Creates the WebGPU renderer used by `FragCanvas`.
595
+ *
596
+ * @param options - Renderer creation options resolved from material/context state.
597
+ * @returns Renderer instance with `render` and `destroy`.
598
+ * @throws {Error} On WebGPU unavailability, shader compilation issues, or runtime setup failures.
599
+ */
600
+ export async function createRenderer(options: RendererOptions): Promise<Renderer> {
601
+ if (!navigator.gpu) {
602
+ throw new Error('WebGPU is not available in this browser');
603
+ }
604
+
605
+ const context = options.canvas.getContext('webgpu') as GPUCanvasContext | null;
606
+ if (!context) {
607
+ throw new Error('Canvas does not support webgpu context');
608
+ }
609
+
610
+ const format = navigator.gpu.getPreferredCanvasFormat();
611
+ const adapter = await navigator.gpu.requestAdapter(options.adapterOptions);
612
+ if (!adapter) {
613
+ throw new Error('Unable to acquire WebGPU adapter');
614
+ }
615
+
616
+ const device = await adapter.requestDevice(options.deviceDescriptor);
617
+ let isDestroyed = false;
618
+ let deviceLostMessage: string | null = null;
619
+ let uncapturedErrorMessage: string | null = null;
620
+ const initializationCleanups: Array<() => void> = [];
621
+ let acceptInitializationCleanups = true;
622
+
623
+ const registerInitializationCleanup = (cleanup: () => void): void => {
624
+ if (!acceptInitializationCleanups) {
625
+ return;
626
+ }
627
+ options.__onInitializationCleanupRegistered?.();
628
+ initializationCleanups.push(cleanup);
629
+ };
630
+
631
+ const runInitializationCleanups = (): void => {
632
+ for (let index = initializationCleanups.length - 1; index >= 0; index -= 1) {
633
+ try {
634
+ initializationCleanups[index]?.();
635
+ } catch {
636
+ // Best-effort cleanup on failed renderer initialization.
637
+ }
638
+ }
639
+ initializationCleanups.length = 0;
640
+ };
641
+
642
+ void device.lost.then((info) => {
643
+ if (isDestroyed) {
644
+ return;
645
+ }
646
+
647
+ const reason = info.reason ? ` (${info.reason})` : '';
648
+ const details = info.message?.trim();
649
+ deviceLostMessage = details
650
+ ? `WebGPU device lost: ${details}${reason}`
651
+ : `WebGPU device lost${reason}`;
652
+ });
653
+
654
+ const handleUncapturedError = (event: GPUUncapturedErrorEvent): void => {
655
+ if (isDestroyed) {
656
+ return;
657
+ }
658
+
659
+ const message =
660
+ event.error instanceof Error
661
+ ? event.error.message
662
+ : String((event.error as { message?: string })?.message ?? event.error);
663
+ uncapturedErrorMessage = `WebGPU uncaptured error: ${message}`;
664
+ };
665
+
666
+ device.addEventListener('uncapturederror', handleUncapturedError);
667
+ try {
668
+ const runtimeContext = buildShaderCompilationRuntimeContext(options);
669
+ const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
670
+ const builtShader = buildShaderSourceWithMap(
671
+ options.fragmentWgsl,
672
+ options.uniformLayout,
673
+ options.textureKeys,
674
+ {
675
+ convertLinearToSrgb,
676
+ fragmentLineMap: options.fragmentLineMap,
677
+ ...(options.storageBufferKeys !== undefined
678
+ ? { storageBufferKeys: options.storageBufferKeys }
679
+ : {}),
680
+ ...(options.storageBufferDefinitions !== undefined
681
+ ? { storageBufferDefinitions: options.storageBufferDefinitions }
682
+ : {})
683
+ }
684
+ );
685
+ const shaderModule = device.createShaderModule({ code: builtShader.code });
686
+ await assertCompilation(shaderModule, {
687
+ lineMap: builtShader.lineMap,
688
+ fragmentSource: options.fragmentSource,
689
+ includeSources: options.includeSources,
690
+ ...(options.defineBlockSource !== undefined
691
+ ? { defineBlockSource: options.defineBlockSource }
692
+ : {}),
693
+ materialSource: options.materialSource ?? null,
694
+ runtimeContext
695
+ });
696
+
697
+ const normalizedTextureDefinitions = normalizeTextureDefinitions(
698
+ options.textureDefinitions,
699
+ options.textureKeys
700
+ );
701
+ const storageBufferKeys = options.storageBufferKeys ?? [];
702
+ const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
703
+ const storageTextureKeys = options.storageTextureKeys ?? [];
704
+ const storageTextureKeySet = new Set(storageTextureKeys);
705
+ const textureBindings = options.textureKeys.map((key, index): RuntimeTextureBinding => {
706
+ const config = normalizedTextureDefinitions[key];
707
+ if (!config) {
708
+ throw new Error(`Missing texture definition for "${key}"`);
709
+ }
710
+
711
+ const { samplerBinding, textureBinding } = getTextureBindings(index);
712
+ const sampler = device.createSampler({
713
+ magFilter: config.filter,
714
+ minFilter: config.filter,
715
+ mipmapFilter: config.generateMipmaps ? config.filter : 'nearest',
716
+ addressModeU: config.addressModeU,
717
+ addressModeV: config.addressModeV,
718
+ maxAnisotropy: config.filter === 'linear' ? config.anisotropy : 1
719
+ });
720
+ // Storage textures use a safe fallback format — the fallback is never
721
+ // sampled because storage textures are eagerly allocated with their
722
+ // real format/dimensions. Non-storage textures use their own format.
723
+ const fallbackFormat = config.storage ? 'rgba8unorm' : config.format;
724
+ const fallbackTexture = createFallbackTexture(device, fallbackFormat);
725
+ registerInitializationCleanup(() => {
726
+ fallbackTexture.destroy();
727
+ });
728
+ const fallbackView = fallbackTexture.createView();
729
+
730
+ const runtimeBinding: RuntimeTextureBinding = {
731
+ key,
732
+ samplerBinding,
733
+ textureBinding,
734
+ sampler,
735
+ fallbackTexture,
736
+ fallbackView,
737
+ texture: null,
738
+ view: fallbackView,
739
+ source: null,
740
+ width: undefined,
741
+ height: undefined,
742
+ mipLevelCount: 1,
743
+ format: config.format,
744
+ colorSpace: config.colorSpace,
745
+ defaultColorSpace: config.colorSpace,
746
+ flipY: config.flipY,
747
+ defaultFlipY: config.flipY,
748
+ generateMipmaps: config.generateMipmaps,
749
+ defaultGenerateMipmaps: config.generateMipmaps,
750
+ premultipliedAlpha: config.premultipliedAlpha,
751
+ defaultPremultipliedAlpha: config.premultipliedAlpha,
752
+ update: config.update ?? 'once',
753
+ lastToken: null
754
+ };
755
+
756
+ if (config.update !== undefined) {
757
+ runtimeBinding.defaultUpdate = config.update;
758
+ }
759
+
760
+ // Storage textures: eagerly create GPU texture with explicit dimensions
761
+ if (config.storage && config.width && config.height) {
762
+ const storageUsage =
763
+ GPUTextureUsage.TEXTURE_BINDING |
764
+ GPUTextureUsage.STORAGE_BINDING |
765
+ GPUTextureUsage.COPY_DST;
766
+ const storageTexture = device.createTexture({
767
+ size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
768
+ format: config.format,
769
+ usage: storageUsage
770
+ });
771
+ registerInitializationCleanup(() => {
772
+ storageTexture.destroy();
773
+ });
774
+ runtimeBinding.texture = storageTexture as unknown as GPUTexture;
775
+ runtimeBinding.view = storageTexture.createView();
776
+ runtimeBinding.width = config.width;
777
+ runtimeBinding.height = config.height;
778
+ }
779
+
780
+ return runtimeBinding;
781
+ });
782
+
783
+ const bindGroupLayout = device.createBindGroupLayout({
784
+ entries: createBindGroupLayoutEntries(textureBindings)
785
+ });
786
+ const fragmentStorageBindGroupLayout =
787
+ storageBufferKeys.length > 0
788
+ ? device.createBindGroupLayout({
789
+ entries: storageBufferKeys.map((_, index) => ({
790
+ binding: index,
791
+ visibility: GPUShaderStage.FRAGMENT,
792
+ buffer: { type: 'read-only-storage' as GPUBufferBindingType }
793
+ }))
794
+ })
795
+ : null;
796
+ const pipelineLayout = device.createPipelineLayout({
797
+ bindGroupLayouts: fragmentStorageBindGroupLayout
798
+ ? [bindGroupLayout, fragmentStorageBindGroupLayout]
799
+ : [bindGroupLayout]
800
+ });
801
+
802
+ const pipeline = device.createRenderPipeline({
803
+ layout: pipelineLayout,
804
+ vertex: {
805
+ module: shaderModule,
806
+ entryPoint: 'motiongpuVertex'
807
+ },
808
+ fragment: {
809
+ module: shaderModule,
810
+ entryPoint: 'motiongpuFragment',
811
+ targets: [{ format }]
812
+ },
813
+ primitive: {
814
+ topology: 'triangle-list'
815
+ }
816
+ });
817
+
818
+ const blitShaderModule = device.createShaderModule({
819
+ code: createFullscreenBlitShader()
820
+ });
821
+ await assertCompilation(blitShaderModule);
822
+
823
+ const blitBindGroupLayout = device.createBindGroupLayout({
824
+ entries: [
825
+ {
826
+ binding: 0,
827
+ visibility: GPUShaderStage.FRAGMENT,
828
+ sampler: { type: 'filtering' }
829
+ },
830
+ {
831
+ binding: 1,
832
+ visibility: GPUShaderStage.FRAGMENT,
833
+ texture: {
834
+ sampleType: 'float',
835
+ viewDimension: '2d',
836
+ multisampled: false
837
+ }
838
+ }
839
+ ]
840
+ });
841
+ const blitPipelineLayout = device.createPipelineLayout({
842
+ bindGroupLayouts: [blitBindGroupLayout]
843
+ });
844
+ const blitPipeline = device.createRenderPipeline({
845
+ layout: blitPipelineLayout,
846
+ vertex: { module: blitShaderModule, entryPoint: 'motiongpuBlitVertex' },
847
+ fragment: {
848
+ module: blitShaderModule,
849
+ entryPoint: 'motiongpuBlitFragment',
850
+ targets: [{ format }]
851
+ },
852
+ primitive: {
853
+ topology: 'triangle-list'
854
+ }
855
+ });
856
+ const blitSampler = device.createSampler({
857
+ magFilter: 'linear',
858
+ minFilter: 'linear',
859
+ addressModeU: 'clamp-to-edge',
860
+ addressModeV: 'clamp-to-edge'
861
+ });
862
+ let blitBindGroupByView = new WeakMap<GPUTextureView, GPUBindGroup>();
863
+
864
+ // ── Storage buffer allocation ────────────────────────────────────────
865
+ const storageBufferMap = new Map<string, GPUBuffer>();
866
+ const pingPongTexturePairs = new Map<string, PingPongTexturePair>();
867
+
868
+ for (const key of storageBufferKeys) {
869
+ const definition = storageBufferDefinitions[key];
870
+ if (!definition) {
871
+ continue;
872
+ }
873
+ const normalized = normalizeStorageBufferDefinition(definition);
874
+ const buffer = device.createBuffer({
875
+ size: normalized.size,
876
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
877
+ });
878
+ registerInitializationCleanup(() => {
879
+ buffer.destroy();
880
+ });
881
+ if (definition.initialData) {
882
+ const data = definition.initialData;
883
+ device.queue.writeBuffer(
884
+ buffer,
885
+ 0,
886
+ data.buffer as ArrayBuffer,
887
+ data.byteOffset,
888
+ data.byteLength
889
+ );
890
+ }
891
+ storageBufferMap.set(key, buffer);
892
+ }
893
+ const fragmentStorageBindGroup =
894
+ fragmentStorageBindGroupLayout && storageBufferKeys.length > 0
895
+ ? device.createBindGroup({
896
+ layout: fragmentStorageBindGroupLayout,
897
+ entries: storageBufferKeys.map((key, index) => {
898
+ const buffer = storageBufferMap.get(key);
899
+ if (!buffer) {
900
+ throw new Error(`Storage buffer "${key}" not allocated.`);
901
+ }
902
+ return { binding: index, resource: { buffer } };
903
+ })
904
+ })
905
+ : null;
906
+
907
+ const ensurePingPongTexturePair = (target: string): PingPongTexturePair => {
908
+ const existing = pingPongTexturePairs.get(target);
909
+ if (existing) {
910
+ return existing;
911
+ }
912
+
913
+ const config = normalizedTextureDefinitions[target];
914
+ if (!config || !config.storage) {
915
+ throw new Error(
916
+ `PingPongComputePass target "${target}" must reference a texture declared with storage:true.`
917
+ );
918
+ }
919
+ if (!config.width || !config.height) {
920
+ throw new Error(
921
+ `PingPongComputePass target "${target}" requires explicit texture width and height.`
922
+ );
923
+ }
924
+
925
+ const usage =
926
+ GPUTextureUsage.TEXTURE_BINDING |
927
+ GPUTextureUsage.STORAGE_BINDING |
928
+ GPUTextureUsage.COPY_DST;
929
+ const textureA = device.createTexture({
930
+ size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
931
+ format: config.format,
932
+ usage
933
+ });
934
+ const textureB = device.createTexture({
935
+ size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
936
+ format: config.format,
937
+ usage
938
+ });
939
+ registerInitializationCleanup(() => {
940
+ textureA.destroy();
941
+ });
942
+ registerInitializationCleanup(() => {
943
+ textureB.destroy();
944
+ });
945
+
946
+ const sampleScalarType = storageTextureSampleScalarType(config.format);
947
+ const sampleType = toGpuTextureSampleType(sampleScalarType);
948
+ const bindGroupLayout = device.createBindGroupLayout({
949
+ entries: [
950
+ {
951
+ binding: 0,
952
+ visibility: GPUShaderStage.COMPUTE,
953
+ texture: {
954
+ sampleType,
955
+ viewDimension: '2d',
956
+ multisampled: false
957
+ }
958
+ },
959
+ {
960
+ binding: 1,
961
+ visibility: GPUShaderStage.COMPUTE,
962
+ storageTexture: {
963
+ access: 'write-only' as GPUStorageTextureAccess,
964
+ format: config.format as GPUTextureFormat,
965
+ viewDimension: '2d'
966
+ }
967
+ }
968
+ ]
969
+ });
970
+
971
+ const pair: PingPongTexturePair = {
972
+ target,
973
+ format: config.format as GPUTextureFormat,
974
+ width: config.width,
975
+ height: config.height,
976
+ textureA,
977
+ viewA: textureA.createView(),
978
+ textureB,
979
+ viewB: textureB.createView(),
980
+ bindGroupLayout
981
+ };
982
+ pingPongTexturePairs.set(target, pair);
983
+ return pair;
984
+ };
985
+
986
+ // ── Compute pipeline setup ──────────────────────────────────────────
987
+ interface ComputePipelineEntry {
988
+ pipeline: GPUComputePipeline;
989
+ bindGroup: GPUBindGroup;
990
+ workgroupSize: [number, number, number];
991
+ computeSource: string;
992
+ }
993
+ const computePipelineCache = new Map<string, ComputePipelineEntry>();
994
+
995
+ const buildComputePipelineEntry = (buildOptions: {
996
+ computeSource: string;
997
+ pingPongTarget?: string;
998
+ pingPongFormat?: GPUTextureFormat;
999
+ }): ComputePipelineEntry => {
1000
+ const cacheKey =
1001
+ buildOptions.pingPongTarget && buildOptions.pingPongFormat
1002
+ ? `pingpong:${buildOptions.pingPongTarget}:${buildOptions.pingPongFormat}:${buildOptions.computeSource}`
1003
+ : `compute:${buildOptions.computeSource}`;
1004
+ const cached = computePipelineCache.get(cacheKey);
1005
+ if (cached) {
1006
+ return cached;
1007
+ }
1008
+
1009
+ const storageBufferDefs: Record<
1010
+ string,
1011
+ { type: StorageBufferType; access: StorageBufferAccess }
1012
+ > = {};
1013
+ for (const key of storageBufferKeys) {
1014
+ const def = storageBufferDefinitions[key];
1015
+ if (def) {
1016
+ const norm = normalizeStorageBufferDefinition(def);
1017
+ storageBufferDefs[key] = { type: norm.type, access: norm.access };
1018
+ }
1019
+ }
1020
+ const storageTextureDefs: Record<string, { format: GPUTextureFormat }> = {};
1021
+ for (const key of storageTextureKeys) {
1022
+ const texDef = options.textureDefinitions[key];
1023
+ if (texDef?.format) {
1024
+ storageTextureDefs[key] = { format: texDef.format };
1025
+ }
1026
+ }
1027
+
1028
+ const isPingPongPipeline = Boolean(
1029
+ buildOptions.pingPongTarget && buildOptions.pingPongFormat
1030
+ );
1031
+ const shaderCode = isPingPongPipeline
1032
+ ? buildPingPongComputeShaderSource({
1033
+ compute: buildOptions.computeSource,
1034
+ uniformLayout: options.uniformLayout,
1035
+ storageBufferKeys,
1036
+ storageBufferDefinitions: storageBufferDefs,
1037
+ target: buildOptions.pingPongTarget!,
1038
+ targetFormat: buildOptions.pingPongFormat!
1039
+ })
1040
+ : buildComputeShaderSource({
1041
+ compute: buildOptions.computeSource,
1042
+ uniformLayout: options.uniformLayout,
1043
+ storageBufferKeys,
1044
+ storageBufferDefinitions: storageBufferDefs,
1045
+ storageTextureKeys,
1046
+ storageTextureDefinitions: storageTextureDefs
1047
+ });
1048
+
1049
+ const computeShaderModule = device.createShaderModule({ code: shaderCode });
1050
+ const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
1051
+
1052
+ // Compute bind group layout: group(0)=uniforms, group(1)=storage buffers, group(2)=storage textures
1053
+ const computeUniformBGL = device.createBindGroupLayout({
1054
+ entries: [
1055
+ {
1056
+ binding: FRAME_BINDING,
1057
+ visibility: GPUShaderStage.COMPUTE,
1058
+ buffer: { type: 'uniform', minBindingSize: 16 }
1059
+ },
1060
+ {
1061
+ binding: UNIFORM_BINDING,
1062
+ visibility: GPUShaderStage.COMPUTE,
1063
+ buffer: { type: 'uniform' }
1064
+ }
1065
+ ]
1066
+ });
1067
+
1068
+ const storageBGLEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map((key, index) => {
1069
+ const def = storageBufferDefinitions[key];
1070
+ const access = def?.access ?? 'read-write';
1071
+ const bufferType: GPUBufferBindingType =
1072
+ access === 'read' ? 'read-only-storage' : 'storage';
1073
+ return {
1074
+ binding: index,
1075
+ visibility: GPUShaderStage.COMPUTE,
1076
+ buffer: { type: bufferType }
1077
+ };
1078
+ });
1079
+ const storageBGL =
1080
+ storageBGLEntries.length > 0
1081
+ ? device.createBindGroupLayout({ entries: storageBGLEntries })
1082
+ : null;
1083
+
1084
+ const storageTextureBGLEntries: GPUBindGroupLayoutEntry[] = isPingPongPipeline
1085
+ ? [
1086
+ {
1087
+ binding: 0,
1088
+ visibility: GPUShaderStage.COMPUTE,
1089
+ texture: {
1090
+ sampleType: toGpuTextureSampleType(
1091
+ storageTextureSampleScalarType(buildOptions.pingPongFormat!)
1092
+ ),
1093
+ viewDimension: '2d',
1094
+ multisampled: false
1095
+ }
1096
+ },
1097
+ {
1098
+ binding: 1,
1099
+ visibility: GPUShaderStage.COMPUTE,
1100
+ storageTexture: {
1101
+ access: 'write-only' as GPUStorageTextureAccess,
1102
+ format: buildOptions.pingPongFormat!,
1103
+ viewDimension: '2d'
1104
+ }
1105
+ }
1106
+ ]
1107
+ : storageTextureKeys.map((key, index) => {
1108
+ const texDef = options.textureDefinitions[key];
1109
+ return {
1110
+ binding: index,
1111
+ visibility: GPUShaderStage.COMPUTE,
1112
+ storageTexture: {
1113
+ access: 'write-only' as GPUStorageTextureAccess,
1114
+ format: (texDef?.format ?? 'rgba8unorm') as GPUTextureFormat,
1115
+ viewDimension: '2d'
1116
+ }
1117
+ };
1118
+ });
1119
+ const storageTextureBGL =
1120
+ storageTextureBGLEntries.length > 0
1121
+ ? device.createBindGroupLayout({ entries: storageTextureBGLEntries })
1122
+ : null;
1123
+
1124
+ // Bind group layout indices must match shader @group() indices:
1125
+ // group(0) = uniforms, group(1) = storage buffers, group(2) = storage textures.
1126
+ // When a group is unused, insert an empty placeholder to keep indices aligned.
1127
+ const bindGroupLayouts: GPUBindGroupLayout[] = [computeUniformBGL];
1128
+ if (storageBGL || storageTextureBGL) {
1129
+ bindGroupLayouts.push(storageBGL ?? device.createBindGroupLayout({ entries: [] }));
1130
+ }
1131
+ if (storageTextureBGL) {
1132
+ bindGroupLayouts.push(storageTextureBGL);
1133
+ }
1134
+
1135
+ const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
1136
+ const pipeline = device.createComputePipeline({
1137
+ layout: computePipelineLayout,
1138
+ compute: {
1139
+ module: computeShaderModule,
1140
+ entryPoint: 'compute'
1141
+ }
1142
+ });
1143
+
1144
+ // Build uniform bind group for compute (group 0)
1145
+ const computeUniformBindGroup = device.createBindGroup({
1146
+ layout: computeUniformBGL,
1147
+ entries: [
1148
+ { binding: FRAME_BINDING, resource: { buffer: frameBuffer } },
1149
+ { binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
1150
+ ]
1151
+ });
1152
+
1153
+ const entry: ComputePipelineEntry = {
1154
+ pipeline,
1155
+ bindGroup: computeUniformBindGroup,
1156
+ workgroupSize,
1157
+ computeSource: buildOptions.computeSource
1158
+ };
1159
+ computePipelineCache.set(cacheKey, entry);
1160
+ return entry;
1161
+ };
1162
+
1163
+ // Helper to get the storage bind group for dispatch
1164
+ const getComputeStorageBindGroup = (): GPUBindGroup | null => {
1165
+ if (storageBufferKeys.length === 0) {
1166
+ return null;
1167
+ }
1168
+ // Rebuild bind group with current storage buffers
1169
+ const storageBGLEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map((key, index) => {
1170
+ const def = storageBufferDefinitions[key];
1171
+ const access = def?.access ?? 'read-write';
1172
+ const bufferType: GPUBufferBindingType =
1173
+ access === 'read' ? 'read-only-storage' : 'storage';
1174
+ return {
1175
+ binding: index,
1176
+ visibility: GPUShaderStage.COMPUTE,
1177
+ buffer: { type: bufferType }
1178
+ };
1179
+ });
1180
+ const storageBGL = device.createBindGroupLayout({ entries: storageBGLEntries });
1181
+ const storageEntries: GPUBindGroupEntry[] = storageBufferKeys.map((key, index) => {
1182
+ const buffer = storageBufferMap.get(key);
1183
+ if (!buffer) {
1184
+ throw new Error(`Storage buffer "${key}" not allocated.`);
1185
+ }
1186
+ return { binding: index, resource: { buffer } };
1187
+ });
1188
+ return device.createBindGroup({
1189
+ layout: storageBGL,
1190
+ entries: storageEntries
1191
+ });
1192
+ };
1193
+
1194
+ // Helper to get the storage texture bind group for compute dispatch (group 2)
1195
+ const getComputeStorageTextureBindGroup = (): GPUBindGroup | null => {
1196
+ if (storageTextureKeys.length === 0) {
1197
+ return null;
1198
+ }
1199
+ const entries: GPUBindGroupLayoutEntry[] = storageTextureKeys.map((key, index) => {
1200
+ const texDef = options.textureDefinitions[key];
1201
+ return {
1202
+ binding: index,
1203
+ visibility: GPUShaderStage.COMPUTE,
1204
+ storageTexture: {
1205
+ access: 'write-only' as GPUStorageTextureAccess,
1206
+ format: (texDef?.format ?? 'rgba8unorm') as GPUTextureFormat,
1207
+ viewDimension: '2d' as GPUTextureViewDimension
1208
+ }
1209
+ };
1210
+ });
1211
+ const bgl = device.createBindGroupLayout({ entries });
1212
+
1213
+ const bgEntries: GPUBindGroupEntry[] = storageTextureKeys.map((key, index) => {
1214
+ const binding = textureBindings.find((b) => b.key === key);
1215
+ if (!binding || !binding.texture) {
1216
+ throw new Error(`Storage texture "${key}" not allocated.`);
1217
+ }
1218
+ return { binding: index, resource: binding.view };
1219
+ });
1220
+
1221
+ return device.createBindGroup({ layout: bgl, entries: bgEntries });
1222
+ };
1223
+
1224
+ // Helper to get ping-pong storage texture bind group for compute dispatch (group 2)
1225
+ const getPingPongStorageTextureBindGroup = (
1226
+ target: string,
1227
+ readFromA: boolean
1228
+ ): GPUBindGroup => {
1229
+ const pair = ensurePingPongTexturePair(target);
1230
+ const readView = readFromA ? pair.viewA : pair.viewB;
1231
+ const writeView = readFromA ? pair.viewB : pair.viewA;
1232
+ return device.createBindGroup({
1233
+ layout: pair.bindGroupLayout,
1234
+ entries: [
1235
+ { binding: 0, resource: readView },
1236
+ { binding: 1, resource: writeView }
1237
+ ]
1238
+ });
1239
+ };
1240
+
1241
+ const frameBuffer = device.createBuffer({
1242
+ size: 16,
1243
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1244
+ });
1245
+ registerInitializationCleanup(() => {
1246
+ frameBuffer.destroy();
1247
+ });
1248
+
1249
+ const uniformBuffer = device.createBuffer({
1250
+ size: options.uniformLayout.byteLength,
1251
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1252
+ });
1253
+ registerInitializationCleanup(() => {
1254
+ uniformBuffer.destroy();
1255
+ });
1256
+ const frameScratch = new Float32Array(4);
1257
+ const uniformScratch = new Float32Array(options.uniformLayout.byteLength / 4);
1258
+ const uniformPrevious = new Float32Array(options.uniformLayout.byteLength / 4);
1259
+ let hasUniformSnapshot = false;
1260
+
1261
+ /**
1262
+ * Rebuilds bind group using current texture views.
1263
+ */
1264
+ const createBindGroup = (): GPUBindGroup => {
1265
+ const entries: GPUBindGroupEntry[] = [
1266
+ { binding: FRAME_BINDING, resource: { buffer: frameBuffer } },
1267
+ { binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
1268
+ ];
1269
+
1270
+ for (const binding of textureBindings) {
1271
+ entries.push({
1272
+ binding: binding.samplerBinding,
1273
+ resource: binding.sampler
1274
+ });
1275
+ entries.push({
1276
+ binding: binding.textureBinding,
1277
+ resource: binding.view
1278
+ });
1279
+ }
1280
+
1281
+ return device.createBindGroup({
1282
+ layout: bindGroupLayout,
1283
+ entries
1284
+ });
1285
+ };
1286
+
1287
+ /**
1288
+ * Synchronizes one runtime texture binding with incoming texture value.
1289
+ *
1290
+ * @returns `true` when bind group must be rebuilt.
1291
+ */
1292
+ const updateTextureBinding = (
1293
+ binding: RuntimeTextureBinding,
1294
+ value: TextureValue,
1295
+ renderMode: RenderMode
1296
+ ): boolean => {
1297
+ const nextData = toTextureData(value);
1298
+
1299
+ if (!nextData) {
1300
+ if (binding.source === null && binding.texture === null) {
1301
+ return false;
1302
+ }
1303
+
1304
+ binding.texture?.destroy();
1305
+ binding.texture = null;
1306
+ binding.view = binding.fallbackView;
1307
+ binding.source = null;
1308
+ binding.width = undefined;
1309
+ binding.height = undefined;
1310
+ binding.lastToken = null;
1311
+ return true;
1312
+ }
1313
+
1314
+ const source = nextData.source;
1315
+ const colorSpace = nextData.colorSpace ?? binding.defaultColorSpace;
1316
+ const format = colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb';
1317
+ const flipY = nextData.flipY ?? binding.defaultFlipY;
1318
+ const premultipliedAlpha = nextData.premultipliedAlpha ?? binding.defaultPremultipliedAlpha;
1319
+ const generateMipmaps = nextData.generateMipmaps ?? binding.defaultGenerateMipmaps;
1320
+ const update = resolveTextureUpdateMode({
1321
+ source,
1322
+ ...(nextData.update !== undefined ? { override: nextData.update } : {}),
1323
+ ...(binding.defaultUpdate !== undefined ? { defaultMode: binding.defaultUpdate } : {})
1324
+ });
1325
+ const { width, height } = resolveTextureSize(nextData);
1326
+ const mipLevelCount = generateMipmaps ? getTextureMipLevelCount(width, height) : 1;
1327
+ const sourceChanged = binding.source !== source;
1328
+ const tokenChanged = binding.lastToken !== value;
1329
+ const requiresReallocation =
1330
+ binding.texture === null ||
1331
+ binding.width !== width ||
1332
+ binding.height !== height ||
1333
+ binding.mipLevelCount !== mipLevelCount ||
1334
+ binding.format !== format;
1335
+
1336
+ if (!requiresReallocation) {
1337
+ const shouldUpload =
1338
+ sourceChanged ||
1339
+ update === 'perFrame' ||
1340
+ (update === 'onInvalidate' && (renderMode !== 'always' || tokenChanged));
1341
+
1342
+ if (shouldUpload && binding.texture) {
1343
+ binding.flipY = flipY;
1344
+ binding.generateMipmaps = generateMipmaps;
1345
+ binding.premultipliedAlpha = premultipliedAlpha;
1346
+ binding.colorSpace = colorSpace;
1347
+ uploadTexture(device, binding.texture, binding, source, width, height, mipLevelCount);
1348
+ }
1349
+
1350
+ binding.source = source;
1351
+ binding.width = width;
1352
+ binding.height = height;
1353
+ binding.mipLevelCount = mipLevelCount;
1354
+ binding.update = update;
1355
+ binding.lastToken = value;
1356
+ return false;
1357
+ }
1358
+
1359
+ let textureUsage =
1360
+ GPUTextureUsage.TEXTURE_BINDING |
1361
+ GPUTextureUsage.COPY_DST |
1362
+ GPUTextureUsage.RENDER_ATTACHMENT;
1363
+ if (storageTextureKeySet.has(binding.key)) {
1364
+ textureUsage |= GPUTextureUsage.STORAGE_BINDING;
1365
+ }
1366
+ const texture = device.createTexture({
1367
+ size: { width, height, depthOrArrayLayers: 1 },
1368
+ format,
1369
+ mipLevelCount,
1370
+ usage: textureUsage
1371
+ });
1372
+ registerInitializationCleanup(() => {
1373
+ texture.destroy();
1374
+ });
1375
+
1376
+ binding.flipY = flipY;
1377
+ binding.generateMipmaps = generateMipmaps;
1378
+ binding.premultipliedAlpha = premultipliedAlpha;
1379
+ binding.colorSpace = colorSpace;
1380
+ binding.format = format;
1381
+ uploadTexture(device, texture, binding, source, width, height, mipLevelCount);
1382
+
1383
+ binding.texture?.destroy();
1384
+ binding.texture = texture;
1385
+ binding.view = texture.createView();
1386
+ binding.source = source;
1387
+ binding.width = width;
1388
+ binding.height = height;
1389
+ binding.mipLevelCount = mipLevelCount;
1390
+ binding.update = update;
1391
+ binding.lastToken = value;
1392
+ return true;
1393
+ };
1394
+
1395
+ for (const binding of textureBindings) {
1396
+ // Skip storage textures — they are eagerly allocated and not source-driven
1397
+ if (storageTextureKeySet.has(binding.key)) continue;
1398
+ const defaultSource = normalizedTextureDefinitions[binding.key]?.source ?? null;
1399
+ updateTextureBinding(binding, defaultSource, 'always');
1400
+ }
1401
+
1402
+ let bindGroup = createBindGroup();
1403
+ let sourceSlotTarget: RuntimeRenderTarget | null = null;
1404
+ let targetSlotTarget: RuntimeRenderTarget | null = null;
1405
+ let renderTargetSignature = '';
1406
+ let renderTargetSnapshot: Readonly<Record<string, RenderTarget>> = {};
1407
+ let renderTargetKeys: string[] = [];
1408
+ let cachedGraphPlan: RenderGraphPlan | null = null;
1409
+ let cachedGraphRenderTargetSignature = '';
1410
+ const cachedGraphClearColor: [number, number, number, number] = [NaN, NaN, NaN, NaN];
1411
+ const cachedGraphPasses: RenderGraphPassSnapshot[] = [];
1412
+ let contextConfigured = false;
1413
+ let configuredWidth = 0;
1414
+ let configuredHeight = 0;
1415
+ const runtimeRenderTargets = new Map<string, RuntimeRenderTarget>();
1416
+ const activePasses: AnyPass[] = [];
1417
+ const lifecyclePreviousSet = new Set<AnyPass>();
1418
+ const lifecycleNextSet = new Set<AnyPass>();
1419
+ const lifecycleUniquePasses: AnyPass[] = [];
1420
+ let lifecyclePassesRef: AnyPass[] | null = null;
1421
+ let passWidth = 0;
1422
+ let passHeight = 0;
1423
+
1424
+ /**
1425
+ * Resolves active render pass list for current frame.
1426
+ */
1427
+ const resolvePasses = (): AnyPass[] => {
1428
+ return options.getPasses?.() ?? options.passes ?? [];
1429
+ };
1430
+
1431
+ /**
1432
+ * Resolves active render target declarations for current frame.
1433
+ */
1434
+ const resolveRenderTargets = () => {
1435
+ return options.getRenderTargets?.() ?? options.renderTargets;
1436
+ };
1437
+
1438
+ /**
1439
+ * Checks whether cached render-graph plan can be reused for this frame.
1440
+ */
1441
+ const isGraphPlanCacheValid = (
1442
+ passes: AnyPass[],
1443
+ clearColor: [number, number, number, number]
1444
+ ): boolean => {
1445
+ if (!cachedGraphPlan) {
1446
+ return false;
1447
+ }
1448
+
1449
+ if (cachedGraphRenderTargetSignature !== renderTargetSignature) {
1450
+ return false;
1451
+ }
1452
+
1453
+ if (
1454
+ cachedGraphClearColor[0] !== clearColor[0] ||
1455
+ cachedGraphClearColor[1] !== clearColor[1] ||
1456
+ cachedGraphClearColor[2] !== clearColor[2] ||
1457
+ cachedGraphClearColor[3] !== clearColor[3]
1458
+ ) {
1459
+ return false;
1460
+ }
1461
+
1462
+ if (cachedGraphPasses.length !== passes.length) {
1463
+ return false;
1464
+ }
1465
+
1466
+ for (let index = 0; index < passes.length; index += 1) {
1467
+ const pass = passes[index];
1468
+ const rp = pass as Partial<RenderPass>;
1469
+ const snapshot = cachedGraphPasses[index];
1470
+ if (!pass || !snapshot || snapshot.pass !== pass) {
1471
+ return false;
1472
+ }
1473
+
1474
+ if (
1475
+ snapshot.enabled !== pass.enabled ||
1476
+ snapshot.needsSwap !== rp.needsSwap ||
1477
+ snapshot.input !== rp.input ||
1478
+ snapshot.output !== rp.output ||
1479
+ snapshot.clear !== rp.clear ||
1480
+ snapshot.preserve !== rp.preserve
1481
+ ) {
1482
+ return false;
1483
+ }
1484
+
1485
+ const passClearColor = rp.clearColor;
1486
+ const hasPassClearColor = passClearColor !== undefined;
1487
+ if (snapshot.hasClearColor !== hasPassClearColor) {
1488
+ return false;
1489
+ }
1490
+
1491
+ if (passClearColor) {
1492
+ if (
1493
+ snapshot.clearColor0 !== passClearColor[0] ||
1494
+ snapshot.clearColor1 !== passClearColor[1] ||
1495
+ snapshot.clearColor2 !== passClearColor[2] ||
1496
+ snapshot.clearColor3 !== passClearColor[3]
1497
+ ) {
1498
+ return false;
1499
+ }
1500
+ }
1501
+ }
1502
+
1503
+ return true;
1504
+ };
1505
+
1506
+ /**
1507
+ * Updates render-graph cache with current pass set.
1508
+ */
1509
+ const updateGraphPlanCache = (
1510
+ passes: AnyPass[],
1511
+ clearColor: [number, number, number, number],
1512
+ graphPlan: RenderGraphPlan
1513
+ ): void => {
1514
+ cachedGraphPlan = graphPlan;
1515
+ cachedGraphRenderTargetSignature = renderTargetSignature;
1516
+ cachedGraphClearColor[0] = clearColor[0];
1517
+ cachedGraphClearColor[1] = clearColor[1];
1518
+ cachedGraphClearColor[2] = clearColor[2];
1519
+ cachedGraphClearColor[3] = clearColor[3];
1520
+ cachedGraphPasses.length = passes.length;
1521
+
1522
+ let index = 0;
1523
+ for (const pass of passes) {
1524
+ const rp = pass as Partial<RenderPass>;
1525
+ const passClearColor = rp.clearColor;
1526
+ const hasPassClearColor = passClearColor !== undefined;
1527
+ const snapshot = cachedGraphPasses[index];
1528
+ if (!snapshot) {
1529
+ cachedGraphPasses[index] = {
1530
+ pass,
1531
+ enabled: pass.enabled,
1532
+ needsSwap: rp.needsSwap,
1533
+ input: rp.input,
1534
+ output: rp.output,
1535
+ clear: rp.clear,
1536
+ preserve: rp.preserve,
1537
+ hasClearColor: hasPassClearColor,
1538
+ clearColor0: passClearColor?.[0] ?? 0,
1539
+ clearColor1: passClearColor?.[1] ?? 0,
1540
+ clearColor2: passClearColor?.[2] ?? 0,
1541
+ clearColor3: passClearColor?.[3] ?? 0
1542
+ };
1543
+ index += 1;
1544
+ continue;
1545
+ }
1546
+
1547
+ snapshot.pass = pass;
1548
+ snapshot.enabled = pass.enabled;
1549
+ snapshot.needsSwap = rp.needsSwap;
1550
+ snapshot.input = rp.input;
1551
+ snapshot.output = rp.output;
1552
+ snapshot.clear = rp.clear;
1553
+ snapshot.preserve = rp.preserve;
1554
+ snapshot.hasClearColor = hasPassClearColor;
1555
+ snapshot.clearColor0 = passClearColor?.[0] ?? 0;
1556
+ snapshot.clearColor1 = passClearColor?.[1] ?? 0;
1557
+ snapshot.clearColor2 = passClearColor?.[2] ?? 0;
1558
+ snapshot.clearColor3 = passClearColor?.[3] ?? 0;
1559
+ index += 1;
1560
+ }
1561
+ };
1562
+
1563
+ /**
1564
+ * Synchronizes pass lifecycle callbacks and resize notifications.
1565
+ */
1566
+ const syncPassLifecycle = (passes: AnyPass[], width: number, height: number): void => {
1567
+ const resized = passWidth !== width || passHeight !== height;
1568
+ if (!resized && lifecyclePassesRef === passes && passes.length === activePasses.length) {
1569
+ let isSameOrder = true;
1570
+ for (let index = 0; index < passes.length; index += 1) {
1571
+ if (activePasses[index] !== passes[index]) {
1572
+ isSameOrder = false;
1573
+ break;
1574
+ }
1575
+ }
1576
+
1577
+ if (isSameOrder) {
1578
+ return;
1579
+ }
1580
+ }
1581
+
1582
+ lifecycleNextSet.clear();
1583
+ lifecycleUniquePasses.length = 0;
1584
+ for (const pass of passes) {
1585
+ if (lifecycleNextSet.has(pass)) {
1586
+ continue;
1587
+ }
1588
+
1589
+ lifecycleNextSet.add(pass);
1590
+ lifecycleUniquePasses.push(pass);
1591
+ }
1592
+ lifecyclePreviousSet.clear();
1593
+ for (const pass of activePasses) {
1594
+ lifecyclePreviousSet.add(pass);
1595
+ }
1596
+
1597
+ for (const pass of activePasses) {
1598
+ if (!lifecycleNextSet.has(pass)) {
1599
+ pass.dispose?.();
1600
+ }
1601
+ }
1602
+
1603
+ for (const pass of lifecycleUniquePasses) {
1604
+ if (resized || !lifecyclePreviousSet.has(pass)) {
1605
+ pass.setSize?.(width, height);
1606
+ }
1607
+ }
1608
+
1609
+ activePasses.length = 0;
1610
+ for (const pass of lifecycleUniquePasses) {
1611
+ activePasses.push(pass);
1612
+ }
1613
+ lifecyclePassesRef = passes;
1614
+ passWidth = width;
1615
+ passHeight = height;
1616
+ };
1617
+
1618
+ /**
1619
+ * Ensures internal ping-pong slot texture matches current canvas size/format.
1620
+ */
1621
+ const ensureSlotTarget = (
1622
+ slot: RenderPassInputSlot,
1623
+ width: number,
1624
+ height: number
1625
+ ): RuntimeRenderTarget => {
1626
+ const current = slot === 'source' ? sourceSlotTarget : targetSlotTarget;
1627
+ if (
1628
+ current &&
1629
+ current.width === width &&
1630
+ current.height === height &&
1631
+ current.format === format
1632
+ ) {
1633
+ return current;
1634
+ }
1635
+
1636
+ destroyRenderTexture(current);
1637
+ const next = createRenderTexture(device, width, height, format);
1638
+ if (slot === 'source') {
1639
+ sourceSlotTarget = next;
1640
+ } else {
1641
+ targetSlotTarget = next;
1642
+ }
1643
+
1644
+ return next;
1645
+ };
1646
+
1647
+ /**
1648
+ * Creates/updates runtime render targets and returns immutable pass snapshot.
1649
+ */
1650
+ const syncRenderTargets = (
1651
+ canvasWidth: number,
1652
+ canvasHeight: number
1653
+ ): Readonly<Record<string, RenderTarget>> => {
1654
+ const resolvedDefinitions = resolveRenderTargetDefinitions(
1655
+ resolveRenderTargets(),
1656
+ canvasWidth,
1657
+ canvasHeight,
1658
+ format
1659
+ );
1660
+ const nextSignature = buildRenderTargetSignature(resolvedDefinitions);
1661
+
1662
+ if (nextSignature !== renderTargetSignature) {
1663
+ const activeKeys = new Set(resolvedDefinitions.map((definition) => definition.key));
1664
+
1665
+ for (const [key, target] of runtimeRenderTargets.entries()) {
1666
+ if (!activeKeys.has(key)) {
1667
+ target.texture.destroy();
1668
+ runtimeRenderTargets.delete(key);
1669
+ }
1670
+ }
1671
+
1672
+ for (const definition of resolvedDefinitions) {
1673
+ const current = runtimeRenderTargets.get(definition.key);
1674
+ if (
1675
+ current &&
1676
+ current.width === definition.width &&
1677
+ current.height === definition.height &&
1678
+ current.format === definition.format
1679
+ ) {
1680
+ continue;
1681
+ }
1682
+
1683
+ current?.texture.destroy();
1684
+ runtimeRenderTargets.set(
1685
+ definition.key,
1686
+ createRenderTexture(device, definition.width, definition.height, definition.format)
1687
+ );
1688
+ }
1689
+
1690
+ renderTargetSignature = nextSignature;
1691
+ const nextSnapshot: Record<string, RenderTarget> = {};
1692
+ const nextKeys: string[] = [];
1693
+ for (const definition of resolvedDefinitions) {
1694
+ const target = runtimeRenderTargets.get(definition.key);
1695
+ if (!target) {
1696
+ continue;
1697
+ }
1698
+
1699
+ nextKeys.push(definition.key);
1700
+ nextSnapshot[definition.key] = {
1701
+ texture: target.texture,
1702
+ view: target.view,
1703
+ width: target.width,
1704
+ height: target.height,
1705
+ format: target.format
1706
+ };
1707
+ }
1708
+
1709
+ renderTargetSnapshot = nextSnapshot;
1710
+ renderTargetKeys = nextKeys;
1711
+ }
1712
+
1713
+ return renderTargetSnapshot;
1714
+ };
1715
+
1716
+ /**
1717
+ * Blits a texture view to the current canvas texture.
1718
+ */
1719
+ const blitToCanvas = (
1720
+ commandEncoder: GPUCommandEncoder,
1721
+ sourceView: GPUTextureView,
1722
+ canvasView: GPUTextureView,
1723
+ clearColor: [number, number, number, number]
1724
+ ): void => {
1725
+ let bindGroup = blitBindGroupByView.get(sourceView);
1726
+ if (!bindGroup) {
1727
+ bindGroup = device.createBindGroup({
1728
+ layout: blitBindGroupLayout,
1729
+ entries: [
1730
+ { binding: 0, resource: blitSampler },
1731
+ { binding: 1, resource: sourceView }
1732
+ ]
1733
+ });
1734
+ blitBindGroupByView.set(sourceView, bindGroup);
1735
+ }
1736
+
1737
+ const pass = commandEncoder.beginRenderPass({
1738
+ colorAttachments: [
1739
+ {
1740
+ view: canvasView,
1741
+ clearValue: {
1742
+ r: clearColor[0],
1743
+ g: clearColor[1],
1744
+ b: clearColor[2],
1745
+ a: clearColor[3]
1746
+ },
1747
+ loadOp: 'clear',
1748
+ storeOp: 'store'
1749
+ }
1750
+ ]
1751
+ });
1752
+
1753
+ pass.setPipeline(blitPipeline);
1754
+ pass.setBindGroup(0, bindGroup);
1755
+ pass.draw(3);
1756
+ pass.end();
1757
+ };
1758
+
1759
+ /**
1760
+ * Executes a full frame render.
1761
+ */
1762
+ const render: Renderer['render'] = ({
1763
+ time,
1764
+ delta,
1765
+ renderMode,
1766
+ uniforms,
1767
+ textures,
1768
+ canvasSize,
1769
+ pendingStorageWrites
1770
+ }) => {
1771
+ if (deviceLostMessage) {
1772
+ throw new Error(deviceLostMessage);
1773
+ }
1774
+
1775
+ if (uncapturedErrorMessage) {
1776
+ const message = uncapturedErrorMessage;
1777
+ uncapturedErrorMessage = null;
1778
+ throw new Error(message);
1779
+ }
1780
+
1781
+ const { width, height } = resizeCanvas(options.canvas, options.getDpr(), canvasSize);
1782
+
1783
+ if (!contextConfigured || configuredWidth !== width || configuredHeight !== height) {
1784
+ context.configure({
1785
+ device,
1786
+ format,
1787
+ alphaMode: 'premultiplied'
1788
+ });
1789
+ contextConfigured = true;
1790
+ configuredWidth = width;
1791
+ configuredHeight = height;
1792
+ }
1793
+
1794
+ frameScratch[0] = time;
1795
+ frameScratch[1] = delta;
1796
+ frameScratch[2] = width;
1797
+ frameScratch[3] = height;
1798
+ device.queue.writeBuffer(
1799
+ frameBuffer,
1800
+ 0,
1801
+ frameScratch.buffer as ArrayBuffer,
1802
+ frameScratch.byteOffset,
1803
+ frameScratch.byteLength
1804
+ );
1805
+
1806
+ packUniformsInto(uniforms, options.uniformLayout, uniformScratch);
1807
+ if (!hasUniformSnapshot) {
1808
+ device.queue.writeBuffer(
1809
+ uniformBuffer,
1810
+ 0,
1811
+ uniformScratch.buffer as ArrayBuffer,
1812
+ uniformScratch.byteOffset,
1813
+ uniformScratch.byteLength
1814
+ );
1815
+ uniformPrevious.set(uniformScratch);
1816
+ hasUniformSnapshot = true;
1817
+ } else {
1818
+ const dirtyRanges = findDirtyFloatRanges(uniformPrevious, uniformScratch);
1819
+ for (const range of dirtyRanges) {
1820
+ const byteOffset = range.start * 4;
1821
+ const byteLength = range.count * 4;
1822
+ device.queue.writeBuffer(
1823
+ uniformBuffer,
1824
+ byteOffset,
1825
+ uniformScratch.buffer as ArrayBuffer,
1826
+ uniformScratch.byteOffset + byteOffset,
1827
+ byteLength
1828
+ );
1829
+ }
1830
+
1831
+ if (dirtyRanges.length > 0) {
1832
+ uniformPrevious.set(uniformScratch);
1833
+ }
1834
+ }
1835
+
1836
+ let bindGroupDirty = false;
1837
+ for (const binding of textureBindings) {
1838
+ // Storage textures are managed by compute passes, skip source-driven updates
1839
+ if (storageTextureKeySet.has(binding.key)) continue;
1840
+ const nextTexture =
1841
+ textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
1842
+ if (updateTextureBinding(binding, nextTexture, renderMode)) {
1843
+ bindGroupDirty = true;
1844
+ }
1845
+ }
1846
+
1847
+ if (bindGroupDirty) {
1848
+ bindGroup = createBindGroup();
1849
+ }
1850
+
1851
+ // Apply pending storage buffer writes
1852
+ if (pendingStorageWrites) {
1853
+ for (const write of pendingStorageWrites) {
1854
+ const buffer = storageBufferMap.get(write.name);
1855
+ if (buffer) {
1856
+ const data = write.data;
1857
+ device.queue.writeBuffer(
1858
+ buffer,
1859
+ write.offset,
1860
+ data.buffer as ArrayBuffer,
1861
+ data.byteOffset,
1862
+ data.byteLength
1863
+ );
1864
+ }
1865
+ }
1866
+ }
1867
+
1868
+ const commandEncoder = device.createCommandEncoder();
1869
+ const passes = resolvePasses();
1870
+ const clearColor = options.getClearColor();
1871
+ syncPassLifecycle(passes, width, height);
1872
+ const runtimeTargets = syncRenderTargets(width, height);
1873
+ const graphPlan = isGraphPlanCacheValid(passes, clearColor)
1874
+ ? cachedGraphPlan!
1875
+ : (() => {
1876
+ const nextPlan = planRenderGraph(passes, clearColor, renderTargetKeys);
1877
+ updateGraphPlanCache(passes, clearColor, nextPlan);
1878
+ return nextPlan;
1879
+ })();
1880
+ const canvasTexture = context.getCurrentTexture();
1881
+ const canvasSurface: RenderTarget = {
1882
+ texture: canvasTexture,
1883
+ view: canvasTexture.createView(),
1884
+ width,
1885
+ height,
1886
+ format
1887
+ };
1888
+ const slots =
1889
+ graphPlan.steps.length > 0
1890
+ ? {
1891
+ source: ensureSlotTarget('source', width, height),
1892
+ target: ensureSlotTarget('target', width, height),
1893
+ canvas: canvasSurface
1894
+ }
1895
+ : null;
1896
+ const sceneOutput = slots ? slots.source : canvasSurface;
1897
+
1898
+ // Dispatch compute passes BEFORE scene render so storage textures
1899
+ // and buffers are up-to-date when the fragment shader samples them.
1900
+ if (slots) {
1901
+ for (const step of graphPlan.steps) {
1902
+ if (step.kind !== 'compute') {
1903
+ continue;
1904
+ }
1905
+ const computePass = step.pass as {
1906
+ isCompute?: boolean;
1907
+ getCompute?: () => string;
1908
+ resolveDispatch?: (ctx: {
1909
+ width: number;
1910
+ height: number;
1911
+ time: number;
1912
+ delta: number;
1913
+ workgroupSize: [number, number, number];
1914
+ }) => [number, number, number];
1915
+ getWorkgroupSize?: () => [number, number, number];
1916
+ isPingPong?: boolean;
1917
+ getTarget?: () => string;
1918
+ getCurrentOutput?: () => string;
1919
+ getIterations?: () => number;
1920
+ advanceFrame?: () => void;
1921
+ };
1922
+ if (
1923
+ computePass.getCompute &&
1924
+ computePass.resolveDispatch &&
1925
+ computePass.getWorkgroupSize
1926
+ ) {
1927
+ const computeSource = computePass.getCompute();
1928
+ const pingPongTarget =
1929
+ computePass.isPingPong && computePass.getTarget ? computePass.getTarget() : undefined;
1930
+ if (computePass.isPingPong && !pingPongTarget) {
1931
+ throw new Error('PingPongComputePass must provide a target texture key.');
1932
+ }
1933
+ const pingPongPair = pingPongTarget ? ensurePingPongTexturePair(pingPongTarget) : null;
1934
+ const pipelineEntry = buildComputePipelineEntry({
1935
+ computeSource,
1936
+ ...(pingPongPair
1937
+ ? {
1938
+ pingPongTarget: pingPongPair.target,
1939
+ pingPongFormat: pingPongPair.format
1940
+ }
1941
+ : {})
1942
+ });
1943
+ const workgroupSize = computePass.getWorkgroupSize();
1944
+ const storageBindGroup = getComputeStorageBindGroup();
1945
+ const storageTextureBindGroup = getComputeStorageTextureBindGroup();
1946
+ const iterations =
1947
+ computePass.isPingPong && computePass.getIterations ? computePass.getIterations() : 1;
1948
+ const currentOutput =
1949
+ computePass.isPingPong && computePass.getCurrentOutput
1950
+ ? computePass.getCurrentOutput()
1951
+ : null;
1952
+ const readFromAAtIterationZero =
1953
+ pingPongPair && currentOutput ? currentOutput !== `${pingPongPair.target}B` : true;
1954
+
1955
+ for (let iter = 0; iter < iterations; iter += 1) {
1956
+ const dispatch = computePass.resolveDispatch({
1957
+ width,
1958
+ height,
1959
+ time,
1960
+ delta,
1961
+ workgroupSize
1962
+ });
1963
+ const cPass = commandEncoder.beginComputePass();
1964
+ cPass.setPipeline(pipelineEntry.pipeline);
1965
+ cPass.setBindGroup(0, pipelineEntry.bindGroup);
1966
+ if (storageBindGroup) {
1967
+ cPass.setBindGroup(1, storageBindGroup);
1968
+ }
1969
+ if (pingPongPair) {
1970
+ const readFromA =
1971
+ iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
1972
+ cPass.setBindGroup(
1973
+ 2,
1974
+ getPingPongStorageTextureBindGroup(pingPongPair.target, readFromA)
1975
+ );
1976
+ } else if (storageTextureBindGroup) {
1977
+ cPass.setBindGroup(2, storageTextureBindGroup);
1978
+ }
1979
+ cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
1980
+ cPass.end();
1981
+ }
1982
+
1983
+ if (computePass.isPingPong && computePass.advanceFrame) {
1984
+ computePass.advanceFrame();
1985
+ }
1986
+ }
1987
+ }
1988
+ }
1989
+
1990
+ const scenePass = commandEncoder.beginRenderPass({
1991
+ colorAttachments: [
1992
+ {
1993
+ view: sceneOutput.view,
1994
+ clearValue: {
1995
+ r: clearColor[0],
1996
+ g: clearColor[1],
1997
+ b: clearColor[2],
1998
+ a: clearColor[3]
1999
+ },
2000
+ loadOp: 'clear',
2001
+ storeOp: 'store'
2002
+ }
2003
+ ]
2004
+ });
2005
+
2006
+ scenePass.setPipeline(pipeline);
2007
+ scenePass.setBindGroup(0, bindGroup);
2008
+ if (fragmentStorageBindGroup) {
2009
+ scenePass.setBindGroup(1, fragmentStorageBindGroup);
2010
+ }
2011
+ scenePass.draw(3);
2012
+ scenePass.end();
2013
+
2014
+ if (slots) {
2015
+ const resolveStepSurface = (
2016
+ slot: RenderPassInputSlot | RenderPassOutputSlot
2017
+ ): RenderTarget => {
2018
+ if (slot === 'source') {
2019
+ return slots.source;
2020
+ }
2021
+
2022
+ if (slot === 'target') {
2023
+ return slots.target;
2024
+ }
2025
+
2026
+ if (slot === 'canvas') {
2027
+ return slots.canvas;
2028
+ }
2029
+
2030
+ const named = runtimeTargets[slot];
2031
+ if (!named) {
2032
+ throw new Error(`Render graph references unknown runtime target "${slot}".`);
2033
+ }
2034
+
2035
+ return named;
2036
+ };
2037
+
2038
+ for (const step of graphPlan.steps) {
2039
+ // Compute passes already dispatched above
2040
+ if (step.kind === 'compute') {
2041
+ continue;
2042
+ }
2043
+
2044
+ const input = resolveStepSurface(step.input);
2045
+ const output = resolveStepSurface(step.output);
2046
+
2047
+ (step.pass as RenderPass).render({
2048
+ device,
2049
+ commandEncoder,
2050
+ source: slots.source,
2051
+ target: slots.target,
2052
+ canvas: slots.canvas,
2053
+ input,
2054
+ output,
2055
+ targets: runtimeTargets,
2056
+ time,
2057
+ delta,
2058
+ width,
2059
+ height,
2060
+ clear: step.clear,
2061
+ clearColor: step.clearColor,
2062
+ preserve: step.preserve,
2063
+ beginRenderPass: (passOptions?: {
2064
+ clear?: boolean;
2065
+ clearColor?: [number, number, number, number];
2066
+ preserve?: boolean;
2067
+ view?: GPUTextureView;
2068
+ }) => {
2069
+ const clear = passOptions?.clear ?? step.clear;
2070
+ const clearColor = passOptions?.clearColor ?? step.clearColor;
2071
+ const preserve = passOptions?.preserve ?? step.preserve;
2072
+
2073
+ return commandEncoder.beginRenderPass({
2074
+ colorAttachments: [
2075
+ {
2076
+ view: passOptions?.view ?? output.view,
2077
+ clearValue: {
2078
+ r: clearColor[0],
2079
+ g: clearColor[1],
2080
+ b: clearColor[2],
2081
+ a: clearColor[3]
2082
+ },
2083
+ loadOp: clear ? 'clear' : 'load',
2084
+ storeOp: preserve ? 'store' : 'discard'
2085
+ }
2086
+ ]
2087
+ });
2088
+ }
2089
+ });
2090
+
2091
+ if (step.needsSwap) {
2092
+ const previousSource = slots.source;
2093
+ slots.source = slots.target;
2094
+ slots.target = previousSource;
2095
+ }
2096
+ }
2097
+
2098
+ if (graphPlan.finalOutput !== 'canvas') {
2099
+ const finalSurface = resolveStepSurface(graphPlan.finalOutput);
2100
+ blitToCanvas(commandEncoder, finalSurface.view, slots.canvas.view, clearColor);
2101
+ }
2102
+ }
2103
+
2104
+ device.queue.submit([commandEncoder.finish()]);
2105
+ };
2106
+
2107
+ acceptInitializationCleanups = false;
2108
+ initializationCleanups.length = 0;
2109
+ return {
2110
+ render,
2111
+ getStorageBuffer: (name: string): GPUBuffer | undefined => {
2112
+ return storageBufferMap.get(name);
2113
+ },
2114
+ getDevice: (): GPUDevice => {
2115
+ return device;
2116
+ },
2117
+ destroy: () => {
2118
+ isDestroyed = true;
2119
+ device.removeEventListener('uncapturederror', handleUncapturedError);
2120
+ frameBuffer.destroy();
2121
+ uniformBuffer.destroy();
2122
+ for (const buffer of storageBufferMap.values()) {
2123
+ buffer.destroy();
2124
+ }
2125
+ storageBufferMap.clear();
2126
+ for (const pair of pingPongTexturePairs.values()) {
2127
+ pair.textureA.destroy();
2128
+ pair.textureB.destroy();
2129
+ }
2130
+ pingPongTexturePairs.clear();
2131
+ computePipelineCache.clear();
2132
+ destroyRenderTexture(sourceSlotTarget);
2133
+ destroyRenderTexture(targetSlotTarget);
2134
+ for (const target of runtimeRenderTargets.values()) {
2135
+ target.texture.destroy();
2136
+ }
2137
+ runtimeRenderTargets.clear();
2138
+ for (const pass of activePasses) {
2139
+ pass.dispose?.();
2140
+ }
2141
+ activePasses.length = 0;
2142
+ lifecyclePassesRef = null;
2143
+ for (const binding of textureBindings) {
2144
+ binding.texture?.destroy();
2145
+ binding.fallbackTexture.destroy();
2146
+ }
2147
+ blitBindGroupByView = new WeakMap();
2148
+ cachedGraphPlan = null;
2149
+ cachedGraphPasses.length = 0;
2150
+ renderTargetSnapshot = {};
2151
+ renderTargetKeys = [];
2152
+ }
2153
+ };
2154
+ } catch (error) {
2155
+ isDestroyed = true;
2156
+ acceptInitializationCleanups = false;
2157
+ device.removeEventListener('uncapturederror', handleUncapturedError);
2158
+ runInitializationCleanups();
2159
+ throw error;
2160
+ }
2161
+ }