@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
@@ -1,284 +1,301 @@
1
- import { buildRenderTargetSignature, resolveRenderTargetDefinitions } from './render-targets.js';
2
- import { planRenderGraph } from './render-graph.js';
3
- import { buildShaderSourceWithMap, formatShaderSourceLocation } from './shader.js';
4
- import { attachShaderCompilationDiagnostics } from './error-diagnostics.js';
5
- import { getTextureMipLevelCount, normalizeTextureDefinitions, resolveTextureUpdateMode, resolveTextureSize, toTextureData } from './textures.js';
6
- import { packUniformsInto } from './uniforms.js';
1
+ import { packUniformsInto } from "./uniforms.js";
2
+ import { getTextureMipLevelCount, normalizeTextureDefinitions, resolveTextureSize, resolveTextureUpdateMode, toTextureData } from "./textures.js";
3
+ import { normalizeStorageBufferDefinition } from "./storage-buffers.js";
4
+ import { attachShaderCompilationDiagnostics } from "./error-diagnostics.js";
5
+ import { buildShaderSourceWithMap, formatShaderSourceLocation } from "./shader.js";
6
+ import { buildRenderTargetSignature, resolveRenderTargetDefinitions } from "./render-targets.js";
7
+ import { planRenderGraph } from "./render-graph.js";
8
+ import { buildComputeShaderSource, buildPingPongComputeShaderSource, extractWorkgroupSize, storageTextureSampleScalarType } from "./compute-shader.js";
9
+ //#region src/lib/core/renderer.ts
7
10
  /**
8
- * Binding index for frame uniforms (`time`, `delta`, `resolution`).
9
- */
10
- const FRAME_BINDING = 0;
11
+ * Binding index for frame uniforms (`time`, `delta`, `resolution`).
12
+ */
13
+ var FRAME_BINDING = 0;
11
14
  /**
12
- * Binding index for material uniform buffer.
13
- */
14
- const UNIFORM_BINDING = 1;
15
+ * Binding index for material uniform buffer.
16
+ */
17
+ var UNIFORM_BINDING = 1;
15
18
  /**
16
- * First binding index used for texture sampler/texture pairs.
17
- */
18
- const FIRST_TEXTURE_BINDING = 2;
19
+ * First binding index used for texture sampler/texture pairs.
20
+ */
21
+ var FIRST_TEXTURE_BINDING = 2;
19
22
  /**
20
- * Returns sampler/texture binding slots for a texture index.
21
- */
23
+ * Returns sampler/texture binding slots for a texture index.
24
+ */
22
25
  function getTextureBindings(index) {
23
- const samplerBinding = FIRST_TEXTURE_BINDING + index * 2;
24
- return {
25
- samplerBinding,
26
- textureBinding: samplerBinding + 1
27
- };
26
+ const samplerBinding = FIRST_TEXTURE_BINDING + index * 2;
27
+ return {
28
+ samplerBinding,
29
+ textureBinding: samplerBinding + 1
30
+ };
28
31
  }
29
32
  /**
30
- * Resizes canvas backing store to match client size and DPR.
31
- */
33
+ * Maps WGSL scalar texture type to WebGPU sampled texture bind-group sample type.
34
+ */
35
+ function toGpuTextureSampleType(type) {
36
+ if (type === "u32") return "uint";
37
+ if (type === "i32") return "sint";
38
+ return "float";
39
+ }
40
+ /**
41
+ * Resizes canvas backing store to match client size and DPR.
42
+ */
32
43
  function resizeCanvas(canvas, dprInput, cssSize) {
33
- const dpr = Number.isFinite(dprInput) && dprInput > 0 ? dprInput : 1;
34
- const rect = cssSize ? null : canvas.getBoundingClientRect();
35
- const cssWidth = Math.max(0, cssSize?.width ?? rect?.width ?? 0);
36
- const cssHeight = Math.max(0, cssSize?.height ?? rect?.height ?? 0);
37
- const width = Math.max(1, Math.floor((cssWidth || 1) * dpr));
38
- const height = Math.max(1, Math.floor((cssHeight || 1) * dpr));
39
- if (canvas.width !== width || canvas.height !== height) {
40
- canvas.width = width;
41
- canvas.height = height;
42
- }
43
- return { width, height };
44
+ const dpr = Number.isFinite(dprInput) && dprInput > 0 ? dprInput : 1;
45
+ const rect = cssSize ? null : canvas.getBoundingClientRect();
46
+ const cssWidth = Math.max(0, cssSize?.width ?? rect?.width ?? 0);
47
+ const cssHeight = Math.max(0, cssSize?.height ?? rect?.height ?? 0);
48
+ const width = Math.max(1, Math.floor((cssWidth || 1) * dpr));
49
+ const height = Math.max(1, Math.floor((cssHeight || 1) * dpr));
50
+ if (canvas.width !== width || canvas.height !== height) {
51
+ canvas.width = width;
52
+ canvas.height = height;
53
+ }
54
+ return {
55
+ width,
56
+ height
57
+ };
44
58
  }
45
59
  /**
46
- * Throws when a shader module contains WGSL compilation errors.
47
- */
60
+ * Throws when a shader module contains WGSL compilation errors.
61
+ */
48
62
  async function assertCompilation(module, options) {
49
- const info = await module.getCompilationInfo();
50
- const errors = info.messages.filter((message) => message.type === 'error');
51
- if (errors.length === 0) {
52
- return;
53
- }
54
- const diagnostics = errors.map((message) => ({
55
- generatedLine: message.lineNum,
56
- message: message.message,
57
- linePos: message.linePos,
58
- lineLength: message.length,
59
- sourceLocation: options?.lineMap?.[message.lineNum] ?? null
60
- }));
61
- const summary = diagnostics
62
- .map((diagnostic) => {
63
- const sourceLabel = formatShaderSourceLocation(diagnostic.sourceLocation);
64
- const generatedLineLabel = diagnostic.generatedLine > 0 ? `generated WGSL line ${diagnostic.generatedLine}` : null;
65
- const contextLabel = [sourceLabel, generatedLineLabel].filter((value) => Boolean(value));
66
- if (contextLabel.length === 0) {
67
- return diagnostic.message;
68
- }
69
- return `[${contextLabel.join(' | ')}] ${diagnostic.message}`;
70
- })
71
- .join('\n');
72
- const error = new Error(`WGSL compilation failed:\n${summary}`);
73
- throw attachShaderCompilationDiagnostics(error, {
74
- kind: 'shader-compilation',
75
- diagnostics,
76
- fragmentSource: options?.fragmentSource ?? '',
77
- includeSources: options?.includeSources ?? {},
78
- ...(options?.defineBlockSource !== undefined
79
- ? { defineBlockSource: options.defineBlockSource }
80
- : {}),
81
- materialSource: options?.materialSource ?? null,
82
- ...(options?.runtimeContext !== undefined ? { runtimeContext: options.runtimeContext } : {})
83
- });
63
+ const errors = (await module.getCompilationInfo()).messages.filter((message) => message.type === "error");
64
+ if (errors.length === 0) return;
65
+ const diagnostics = errors.map((message) => ({
66
+ generatedLine: message.lineNum,
67
+ message: message.message,
68
+ linePos: message.linePos,
69
+ lineLength: message.length,
70
+ sourceLocation: options?.lineMap?.[message.lineNum] ?? null
71
+ }));
72
+ const summary = diagnostics.map((diagnostic) => {
73
+ const contextLabel = [formatShaderSourceLocation(diagnostic.sourceLocation), diagnostic.generatedLine > 0 ? `generated WGSL line ${diagnostic.generatedLine}` : null].filter((value) => Boolean(value));
74
+ if (contextLabel.length === 0) return diagnostic.message;
75
+ return `[${contextLabel.join(" | ")}] ${diagnostic.message}`;
76
+ }).join("\n");
77
+ throw attachShaderCompilationDiagnostics(/* @__PURE__ */ new Error(`WGSL compilation failed:\n${summary}`), {
78
+ kind: "shader-compilation",
79
+ diagnostics,
80
+ fragmentSource: options?.fragmentSource ?? "",
81
+ includeSources: options?.includeSources ?? {},
82
+ ...options?.defineBlockSource !== void 0 ? { defineBlockSource: options.defineBlockSource } : {},
83
+ materialSource: options?.materialSource ?? null,
84
+ ...options?.runtimeContext !== void 0 ? { runtimeContext: options.runtimeContext } : {}
85
+ });
84
86
  }
85
87
  function toSortedUniqueStrings(values) {
86
- return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
88
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
87
89
  }
88
90
  function buildPassGraphSnapshot(passes) {
89
- const declaredPasses = passes ?? [];
90
- let enabledPassCount = 0;
91
- const inputs = [];
92
- const outputs = [];
93
- for (const pass of declaredPasses) {
94
- if (pass.enabled === false) {
95
- continue;
96
- }
97
- enabledPassCount += 1;
98
- const needsSwap = pass.needsSwap ?? true;
99
- const input = pass.input ?? 'source';
100
- const output = pass.output ?? (needsSwap ? 'target' : 'source');
101
- inputs.push(input);
102
- outputs.push(output);
103
- }
104
- return {
105
- passCount: declaredPasses.length,
106
- enabledPassCount,
107
- inputs: toSortedUniqueStrings(inputs),
108
- outputs: toSortedUniqueStrings(outputs)
109
- };
91
+ const declaredPasses = passes ?? [];
92
+ let enabledPassCount = 0;
93
+ const inputs = [];
94
+ const outputs = [];
95
+ for (const pass of declaredPasses) {
96
+ if (pass.enabled === false) continue;
97
+ enabledPassCount += 1;
98
+ if ("isCompute" in pass && pass.isCompute === true) continue;
99
+ const rp = pass;
100
+ const needsSwap = rp.needsSwap ?? true;
101
+ const input = rp.input ?? "source";
102
+ const output = rp.output ?? (needsSwap ? "target" : "source");
103
+ inputs.push(input);
104
+ outputs.push(output);
105
+ }
106
+ return {
107
+ passCount: declaredPasses.length,
108
+ enabledPassCount,
109
+ inputs: toSortedUniqueStrings(inputs),
110
+ outputs: toSortedUniqueStrings(outputs)
111
+ };
110
112
  }
111
113
  function buildShaderCompilationRuntimeContext(options) {
112
- const passList = options.getPasses?.() ?? options.passes;
113
- const renderTargetMap = options.getRenderTargets?.() ?? options.renderTargets;
114
- return {
115
- ...(options.materialSignature ? { materialSignature: options.materialSignature } : {}),
116
- passGraph: buildPassGraphSnapshot(passList),
117
- activeRenderTargets: Object.keys(renderTargetMap ?? {}).sort((a, b) => a.localeCompare(b))
118
- };
114
+ const passList = options.getPasses?.() ?? options.passes;
115
+ const renderTargetMap = options.getRenderTargets?.() ?? options.renderTargets;
116
+ return {
117
+ ...options.materialSignature ? { materialSignature: options.materialSignature } : {},
118
+ passGraph: buildPassGraphSnapshot(passList),
119
+ activeRenderTargets: Object.keys(renderTargetMap ?? {}).sort((a, b) => a.localeCompare(b))
120
+ };
119
121
  }
120
122
  /**
121
- * Creates a 1x1 white fallback texture used before user textures become available.
122
- */
123
+ * Creates a 1x1 white fallback texture used before user textures become available.
124
+ */
123
125
  function createFallbackTexture(device, format) {
124
- const texture = device.createTexture({
125
- size: { width: 1, height: 1, depthOrArrayLayers: 1 },
126
- format,
127
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
128
- });
129
- const pixel = new Uint8Array([255, 255, 255, 255]);
130
- device.queue.writeTexture({ texture }, pixel, { offset: 0, bytesPerRow: 4, rowsPerImage: 1 }, { width: 1, height: 1, depthOrArrayLayers: 1 });
131
- return texture;
126
+ const texture = device.createTexture({
127
+ size: {
128
+ width: 1,
129
+ height: 1,
130
+ depthOrArrayLayers: 1
131
+ },
132
+ format,
133
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
134
+ });
135
+ const pixel = new Uint8Array([
136
+ 255,
137
+ 255,
138
+ 255,
139
+ 255
140
+ ]);
141
+ device.queue.writeTexture({ texture }, pixel, {
142
+ offset: 0,
143
+ bytesPerRow: 4,
144
+ rowsPerImage: 1
145
+ }, {
146
+ width: 1,
147
+ height: 1,
148
+ depthOrArrayLayers: 1
149
+ });
150
+ return texture;
132
151
  }
133
152
  /**
134
- * Creates an offscreen canvas used for CPU mipmap generation.
135
- */
153
+ * Creates an offscreen canvas used for CPU mipmap generation.
154
+ */
136
155
  function createMipmapCanvas(width, height) {
137
- if (typeof OffscreenCanvas !== 'undefined') {
138
- return new OffscreenCanvas(width, height);
139
- }
140
- const canvas = document.createElement('canvas');
141
- canvas.width = width;
142
- canvas.height = height;
143
- return canvas;
156
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(width, height);
157
+ const canvas = document.createElement("canvas");
158
+ canvas.width = width;
159
+ canvas.height = height;
160
+ return canvas;
144
161
  }
145
162
  /**
146
- * Creates typed descriptor for `copyExternalImageToTexture`.
147
- */
163
+ * Creates typed descriptor for `copyExternalImageToTexture`.
164
+ */
148
165
  function createExternalCopySource(source, options) {
149
- const descriptor = {
150
- source,
151
- ...(options.flipY ? { flipY: true } : {}),
152
- ...(options.premultipliedAlpha ? { premultipliedAlpha: true } : {})
153
- };
154
- return descriptor;
166
+ return {
167
+ source,
168
+ ...options.flipY ? { flipY: true } : {},
169
+ ...options.premultipliedAlpha ? { premultipliedAlpha: true } : {}
170
+ };
155
171
  }
156
172
  /**
157
- * Uploads source content to a GPU texture and optionally generates mip chain on CPU.
158
- */
173
+ * Uploads source content to a GPU texture and optionally generates mip chain on CPU.
174
+ */
159
175
  function uploadTexture(device, texture, binding, source, width, height, mipLevelCount) {
160
- device.queue.copyExternalImageToTexture(createExternalCopySource(source, {
161
- flipY: binding.flipY,
162
- premultipliedAlpha: binding.premultipliedAlpha
163
- }), { texture, mipLevel: 0 }, { width, height, depthOrArrayLayers: 1 });
164
- if (!binding.generateMipmaps || mipLevelCount <= 1) {
165
- return;
166
- }
167
- let previousSource = source;
168
- let previousWidth = width;
169
- let previousHeight = height;
170
- for (let level = 1; level < mipLevelCount; level += 1) {
171
- const nextWidth = Math.max(1, Math.floor(previousWidth / 2));
172
- const nextHeight = Math.max(1, Math.floor(previousHeight / 2));
173
- const canvas = createMipmapCanvas(nextWidth, nextHeight);
174
- const context = canvas.getContext('2d');
175
- if (!context) {
176
- throw new Error('Unable to create 2D context for mipmap generation');
177
- }
178
- context.drawImage(previousSource, 0, 0, previousWidth, previousHeight, 0, 0, nextWidth, nextHeight);
179
- device.queue.copyExternalImageToTexture(createExternalCopySource(canvas, {
180
- premultipliedAlpha: binding.premultipliedAlpha
181
- }), { texture, mipLevel: level }, { width: nextWidth, height: nextHeight, depthOrArrayLayers: 1 });
182
- previousSource = canvas;
183
- previousWidth = nextWidth;
184
- previousHeight = nextHeight;
185
- }
176
+ device.queue.copyExternalImageToTexture(createExternalCopySource(source, {
177
+ flipY: binding.flipY,
178
+ premultipliedAlpha: binding.premultipliedAlpha
179
+ }), {
180
+ texture,
181
+ mipLevel: 0
182
+ }, {
183
+ width,
184
+ height,
185
+ depthOrArrayLayers: 1
186
+ });
187
+ if (!binding.generateMipmaps || mipLevelCount <= 1) return;
188
+ let previousSource = source;
189
+ let previousWidth = width;
190
+ let previousHeight = height;
191
+ for (let level = 1; level < mipLevelCount; level += 1) {
192
+ const nextWidth = Math.max(1, Math.floor(previousWidth / 2));
193
+ const nextHeight = Math.max(1, Math.floor(previousHeight / 2));
194
+ const canvas = createMipmapCanvas(nextWidth, nextHeight);
195
+ const context = canvas.getContext("2d");
196
+ if (!context) throw new Error("Unable to create 2D context for mipmap generation");
197
+ context.drawImage(previousSource, 0, 0, previousWidth, previousHeight, 0, 0, nextWidth, nextHeight);
198
+ device.queue.copyExternalImageToTexture(createExternalCopySource(canvas, { premultipliedAlpha: binding.premultipliedAlpha }), {
199
+ texture,
200
+ mipLevel: level
201
+ }, {
202
+ width: nextWidth,
203
+ height: nextHeight,
204
+ depthOrArrayLayers: 1
205
+ });
206
+ previousSource = canvas;
207
+ previousWidth = nextWidth;
208
+ previousHeight = nextHeight;
209
+ }
186
210
  }
187
211
  /**
188
- * Creates bind group layout entries for frame/uniform buffers plus texture bindings.
189
- */
212
+ * Creates bind group layout entries for frame/uniform buffers plus texture bindings.
213
+ */
190
214
  function createBindGroupLayoutEntries(textureBindings) {
191
- const entries = [
192
- {
193
- binding: FRAME_BINDING,
194
- visibility: GPUShaderStage.FRAGMENT,
195
- buffer: { type: 'uniform', minBindingSize: 16 }
196
- },
197
- {
198
- binding: UNIFORM_BINDING,
199
- visibility: GPUShaderStage.FRAGMENT,
200
- buffer: { type: 'uniform' }
201
- }
202
- ];
203
- for (const binding of textureBindings) {
204
- entries.push({
205
- binding: binding.samplerBinding,
206
- visibility: GPUShaderStage.FRAGMENT,
207
- sampler: { type: 'filtering' }
208
- });
209
- entries.push({
210
- binding: binding.textureBinding,
211
- visibility: GPUShaderStage.FRAGMENT,
212
- texture: {
213
- sampleType: 'float',
214
- viewDimension: '2d',
215
- multisampled: false
216
- }
217
- });
218
- }
219
- return entries;
215
+ const entries = [{
216
+ binding: FRAME_BINDING,
217
+ visibility: GPUShaderStage.FRAGMENT,
218
+ buffer: {
219
+ type: "uniform",
220
+ minBindingSize: 16
221
+ }
222
+ }, {
223
+ binding: UNIFORM_BINDING,
224
+ visibility: GPUShaderStage.FRAGMENT,
225
+ buffer: { type: "uniform" }
226
+ }];
227
+ for (const binding of textureBindings) {
228
+ entries.push({
229
+ binding: binding.samplerBinding,
230
+ visibility: GPUShaderStage.FRAGMENT,
231
+ sampler: { type: "filtering" }
232
+ });
233
+ entries.push({
234
+ binding: binding.textureBinding,
235
+ visibility: GPUShaderStage.FRAGMENT,
236
+ texture: {
237
+ sampleType: "float",
238
+ viewDimension: "2d",
239
+ multisampled: false
240
+ }
241
+ });
242
+ }
243
+ return entries;
220
244
  }
221
245
  /**
222
- * Maximum gap (in floats) between two dirty ranges that triggers merge.
223
- *
224
- * Set to 4 (16 bytes) which covers one vec4f alignment slot.
225
- */
226
- const DIRTY_RANGE_MERGE_GAP = 4;
246
+ * Maximum gap (in floats) between two dirty ranges that triggers merge.
247
+ *
248
+ * Set to 4 (16 bytes) which covers one vec4f alignment slot.
249
+ */
250
+ var DIRTY_RANGE_MERGE_GAP = 4;
227
251
  /**
228
- * Computes dirty float ranges between two uniform snapshots.
229
- *
230
- * Adjacent dirty ranges separated by a gap smaller than or equal to
231
- * {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
232
- */
233
- export function findDirtyFloatRanges(previous, next, mergeGapThreshold = DIRTY_RANGE_MERGE_GAP) {
234
- const ranges = [];
235
- let start = -1;
236
- for (let index = 0; index < next.length; index += 1) {
237
- if (previous[index] !== next[index]) {
238
- if (start === -1) {
239
- start = index;
240
- }
241
- continue;
242
- }
243
- if (start !== -1) {
244
- ranges.push({ start, count: index - start });
245
- start = -1;
246
- }
247
- }
248
- if (start !== -1) {
249
- ranges.push({ start, count: next.length - start });
250
- }
251
- if (ranges.length <= 1) {
252
- return ranges;
253
- }
254
- const merged = [ranges[0]];
255
- for (let index = 1; index < ranges.length; index += 1) {
256
- const prev = merged[merged.length - 1];
257
- const curr = ranges[index];
258
- const gap = curr.start - (prev.start + prev.count);
259
- if (gap <= mergeGapThreshold) {
260
- prev.count = curr.start + curr.count - prev.start;
261
- }
262
- else {
263
- merged.push(curr);
264
- }
265
- }
266
- return merged;
252
+ * Computes dirty float ranges between two uniform snapshots.
253
+ *
254
+ * Adjacent dirty ranges separated by a gap smaller than or equal to
255
+ * {@link DIRTY_RANGE_MERGE_GAP} are merged to reduce `writeBuffer` calls.
256
+ */
257
+ function findDirtyFloatRanges(previous, next, mergeGapThreshold = DIRTY_RANGE_MERGE_GAP) {
258
+ const ranges = [];
259
+ let start = -1;
260
+ for (let index = 0; index < next.length; index += 1) {
261
+ if (previous[index] !== next[index]) {
262
+ if (start === -1) start = index;
263
+ continue;
264
+ }
265
+ if (start !== -1) {
266
+ ranges.push({
267
+ start,
268
+ count: index - start
269
+ });
270
+ start = -1;
271
+ }
272
+ }
273
+ if (start !== -1) ranges.push({
274
+ start,
275
+ count: next.length - start
276
+ });
277
+ if (ranges.length <= 1) return ranges;
278
+ const merged = [ranges[0]];
279
+ for (let index = 1; index < ranges.length; index += 1) {
280
+ const prev = merged[merged.length - 1];
281
+ const curr = ranges[index];
282
+ if (curr.start - (prev.start + prev.count) <= mergeGapThreshold) prev.count = curr.start + curr.count - prev.start;
283
+ else merged.push(curr);
284
+ }
285
+ return merged;
267
286
  }
268
287
  /**
269
- * Determines whether shader output should perform linear-to-sRGB conversion.
270
- */
288
+ * Determines whether shader output should perform linear-to-sRGB conversion.
289
+ */
271
290
  function shouldConvertLinearToSrgb(outputColorSpace, canvasFormat) {
272
- if (outputColorSpace !== 'srgb') {
273
- return false;
274
- }
275
- return !canvasFormat.endsWith('-srgb');
291
+ if (outputColorSpace !== "srgb") return false;
292
+ return !canvasFormat.endsWith("-srgb");
276
293
  }
277
294
  /**
278
- * WGSL shader used to blit an offscreen texture to the canvas.
279
- */
295
+ * WGSL shader used to blit an offscreen texture to the canvas.
296
+ */
280
297
  function createFullscreenBlitShader() {
281
- return `
298
+ return `
282
299
  struct MotionGPUVertexOut {
283
300
  @builtin(position) position: vec4f,
284
301
  @location(0) uv: vec2f,
@@ -309,851 +326,1090 @@ fn motiongpuBlitFragment(in: MotionGPUVertexOut) -> @location(0) vec4f {
309
326
  `;
310
327
  }
311
328
  /**
312
- * Allocates a render target texture with usage flags suitable for passes/blits.
313
- */
329
+ * Allocates a render target texture with usage flags suitable for passes/blits.
330
+ */
314
331
  function createRenderTexture(device, width, height, format) {
315
- const texture = device.createTexture({
316
- size: { width, height, depthOrArrayLayers: 1 },
317
- format,
318
- usage: GPUTextureUsage.TEXTURE_BINDING |
319
- GPUTextureUsage.RENDER_ATTACHMENT |
320
- GPUTextureUsage.COPY_DST |
321
- GPUTextureUsage.COPY_SRC
322
- });
323
- return {
324
- texture,
325
- view: texture.createView(),
326
- width,
327
- height,
328
- format
329
- };
332
+ const texture = device.createTexture({
333
+ size: {
334
+ width,
335
+ height,
336
+ depthOrArrayLayers: 1
337
+ },
338
+ format,
339
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC
340
+ });
341
+ return {
342
+ texture,
343
+ view: texture.createView(),
344
+ width,
345
+ height,
346
+ format
347
+ };
330
348
  }
331
349
  /**
332
- * Destroys a render target texture if present.
333
- */
350
+ * Destroys a render target texture if present.
351
+ */
334
352
  function destroyRenderTexture(target) {
335
- target?.texture.destroy();
353
+ target?.texture.destroy();
336
354
  }
337
355
  /**
338
- * Creates the WebGPU renderer used by `FragCanvas`.
339
- *
340
- * @param options - Renderer creation options resolved from material/context state.
341
- * @returns Renderer instance with `render` and `destroy`.
342
- * @throws {Error} On WebGPU unavailability, shader compilation issues, or runtime setup failures.
343
- */
344
- export async function createRenderer(options) {
345
- if (!navigator.gpu) {
346
- throw new Error('WebGPU is not available in this browser');
347
- }
348
- const context = options.canvas.getContext('webgpu');
349
- if (!context) {
350
- throw new Error('Canvas does not support webgpu context');
351
- }
352
- const format = navigator.gpu.getPreferredCanvasFormat();
353
- const adapter = await navigator.gpu.requestAdapter(options.adapterOptions);
354
- if (!adapter) {
355
- throw new Error('Unable to acquire WebGPU adapter');
356
- }
357
- const device = await adapter.requestDevice(options.deviceDescriptor);
358
- let isDestroyed = false;
359
- let deviceLostMessage = null;
360
- let uncapturedErrorMessage = null;
361
- const initializationCleanups = [];
362
- let acceptInitializationCleanups = true;
363
- const registerInitializationCleanup = (cleanup) => {
364
- if (!acceptInitializationCleanups) {
365
- return;
366
- }
367
- options.__onInitializationCleanupRegistered?.();
368
- initializationCleanups.push(cleanup);
369
- };
370
- const runInitializationCleanups = () => {
371
- for (let index = initializationCleanups.length - 1; index >= 0; index -= 1) {
372
- try {
373
- initializationCleanups[index]?.();
374
- }
375
- catch {
376
- // Best-effort cleanup on failed renderer initialization.
377
- }
378
- }
379
- initializationCleanups.length = 0;
380
- };
381
- void device.lost.then((info) => {
382
- if (isDestroyed) {
383
- return;
384
- }
385
- const reason = info.reason ? ` (${info.reason})` : '';
386
- const details = info.message?.trim();
387
- deviceLostMessage = details
388
- ? `WebGPU device lost: ${details}${reason}`
389
- : `WebGPU device lost${reason}`;
390
- });
391
- const handleUncapturedError = (event) => {
392
- if (isDestroyed) {
393
- return;
394
- }
395
- const message = event.error instanceof Error
396
- ? event.error.message
397
- : String(event.error?.message ?? event.error);
398
- uncapturedErrorMessage = `WebGPU uncaptured error: ${message}`;
399
- };
400
- device.addEventListener('uncapturederror', handleUncapturedError);
401
- try {
402
- const runtimeContext = buildShaderCompilationRuntimeContext(options);
403
- const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
404
- const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, options.textureKeys, {
405
- convertLinearToSrgb,
406
- fragmentLineMap: options.fragmentLineMap
407
- });
408
- const shaderModule = device.createShaderModule({ code: builtShader.code });
409
- await assertCompilation(shaderModule, {
410
- lineMap: builtShader.lineMap,
411
- fragmentSource: options.fragmentSource,
412
- includeSources: options.includeSources,
413
- ...(options.defineBlockSource !== undefined
414
- ? { defineBlockSource: options.defineBlockSource }
415
- : {}),
416
- materialSource: options.materialSource ?? null,
417
- runtimeContext
418
- });
419
- const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
420
- const textureBindings = options.textureKeys.map((key, index) => {
421
- const config = normalizedTextureDefinitions[key];
422
- if (!config) {
423
- throw new Error(`Missing texture definition for "${key}"`);
424
- }
425
- const { samplerBinding, textureBinding } = getTextureBindings(index);
426
- const sampler = device.createSampler({
427
- magFilter: config.filter,
428
- minFilter: config.filter,
429
- mipmapFilter: config.generateMipmaps ? config.filter : 'nearest',
430
- addressModeU: config.addressModeU,
431
- addressModeV: config.addressModeV,
432
- maxAnisotropy: config.filter === 'linear' ? config.anisotropy : 1
433
- });
434
- const fallbackTexture = createFallbackTexture(device, config.format);
435
- registerInitializationCleanup(() => {
436
- fallbackTexture.destroy();
437
- });
438
- const fallbackView = fallbackTexture.createView();
439
- const runtimeBinding = {
440
- key,
441
- samplerBinding,
442
- textureBinding,
443
- sampler,
444
- fallbackTexture,
445
- fallbackView,
446
- texture: null,
447
- view: fallbackView,
448
- source: null,
449
- width: undefined,
450
- height: undefined,
451
- mipLevelCount: 1,
452
- format: config.format,
453
- colorSpace: config.colorSpace,
454
- defaultColorSpace: config.colorSpace,
455
- flipY: config.flipY,
456
- defaultFlipY: config.flipY,
457
- generateMipmaps: config.generateMipmaps,
458
- defaultGenerateMipmaps: config.generateMipmaps,
459
- premultipliedAlpha: config.premultipliedAlpha,
460
- defaultPremultipliedAlpha: config.premultipliedAlpha,
461
- update: config.update ?? 'once',
462
- lastToken: null
463
- };
464
- if (config.update !== undefined) {
465
- runtimeBinding.defaultUpdate = config.update;
466
- }
467
- return runtimeBinding;
468
- });
469
- const bindGroupLayout = device.createBindGroupLayout({
470
- entries: createBindGroupLayoutEntries(textureBindings)
471
- });
472
- const pipelineLayout = device.createPipelineLayout({
473
- bindGroupLayouts: [bindGroupLayout]
474
- });
475
- const pipeline = device.createRenderPipeline({
476
- layout: pipelineLayout,
477
- vertex: {
478
- module: shaderModule,
479
- entryPoint: 'motiongpuVertex'
480
- },
481
- fragment: {
482
- module: shaderModule,
483
- entryPoint: 'motiongpuFragment',
484
- targets: [{ format }]
485
- },
486
- primitive: {
487
- topology: 'triangle-list'
488
- }
489
- });
490
- const blitShaderModule = device.createShaderModule({
491
- code: createFullscreenBlitShader()
492
- });
493
- await assertCompilation(blitShaderModule);
494
- const blitBindGroupLayout = device.createBindGroupLayout({
495
- entries: [
496
- {
497
- binding: 0,
498
- visibility: GPUShaderStage.FRAGMENT,
499
- sampler: { type: 'filtering' }
500
- },
501
- {
502
- binding: 1,
503
- visibility: GPUShaderStage.FRAGMENT,
504
- texture: {
505
- sampleType: 'float',
506
- viewDimension: '2d',
507
- multisampled: false
508
- }
509
- }
510
- ]
511
- });
512
- const blitPipelineLayout = device.createPipelineLayout({
513
- bindGroupLayouts: [blitBindGroupLayout]
514
- });
515
- const blitPipeline = device.createRenderPipeline({
516
- layout: blitPipelineLayout,
517
- vertex: { module: blitShaderModule, entryPoint: 'motiongpuBlitVertex' },
518
- fragment: {
519
- module: blitShaderModule,
520
- entryPoint: 'motiongpuBlitFragment',
521
- targets: [{ format }]
522
- },
523
- primitive: {
524
- topology: 'triangle-list'
525
- }
526
- });
527
- const blitSampler = device.createSampler({
528
- magFilter: 'linear',
529
- minFilter: 'linear',
530
- addressModeU: 'clamp-to-edge',
531
- addressModeV: 'clamp-to-edge'
532
- });
533
- let blitBindGroupByView = new WeakMap();
534
- const frameBuffer = device.createBuffer({
535
- size: 16,
536
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
537
- });
538
- registerInitializationCleanup(() => {
539
- frameBuffer.destroy();
540
- });
541
- const uniformBuffer = device.createBuffer({
542
- size: options.uniformLayout.byteLength,
543
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
544
- });
545
- registerInitializationCleanup(() => {
546
- uniformBuffer.destroy();
547
- });
548
- const frameScratch = new Float32Array(4);
549
- const uniformScratch = new Float32Array(options.uniformLayout.byteLength / 4);
550
- const uniformPrevious = new Float32Array(options.uniformLayout.byteLength / 4);
551
- let hasUniformSnapshot = false;
552
- /**
553
- * Rebuilds bind group using current texture views.
554
- */
555
- const createBindGroup = () => {
556
- const entries = [
557
- { binding: FRAME_BINDING, resource: { buffer: frameBuffer } },
558
- { binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
559
- ];
560
- for (const binding of textureBindings) {
561
- entries.push({
562
- binding: binding.samplerBinding,
563
- resource: binding.sampler
564
- });
565
- entries.push({
566
- binding: binding.textureBinding,
567
- resource: binding.view
568
- });
569
- }
570
- return device.createBindGroup({
571
- layout: bindGroupLayout,
572
- entries
573
- });
574
- };
575
- /**
576
- * Synchronizes one runtime texture binding with incoming texture value.
577
- *
578
- * @returns `true` when bind group must be rebuilt.
579
- */
580
- const updateTextureBinding = (binding, value, renderMode) => {
581
- const nextData = toTextureData(value);
582
- if (!nextData) {
583
- if (binding.source === null && binding.texture === null) {
584
- return false;
585
- }
586
- binding.texture?.destroy();
587
- binding.texture = null;
588
- binding.view = binding.fallbackView;
589
- binding.source = null;
590
- binding.width = undefined;
591
- binding.height = undefined;
592
- binding.lastToken = null;
593
- return true;
594
- }
595
- const source = nextData.source;
596
- const colorSpace = nextData.colorSpace ?? binding.defaultColorSpace;
597
- const format = colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb';
598
- const flipY = nextData.flipY ?? binding.defaultFlipY;
599
- const premultipliedAlpha = nextData.premultipliedAlpha ?? binding.defaultPremultipliedAlpha;
600
- const generateMipmaps = nextData.generateMipmaps ?? binding.defaultGenerateMipmaps;
601
- const update = resolveTextureUpdateMode({
602
- source,
603
- ...(nextData.update !== undefined ? { override: nextData.update } : {}),
604
- ...(binding.defaultUpdate !== undefined ? { defaultMode: binding.defaultUpdate } : {})
605
- });
606
- const { width, height } = resolveTextureSize(nextData);
607
- const mipLevelCount = generateMipmaps ? getTextureMipLevelCount(width, height) : 1;
608
- const sourceChanged = binding.source !== source;
609
- const tokenChanged = binding.lastToken !== value;
610
- const requiresReallocation = binding.texture === null ||
611
- binding.width !== width ||
612
- binding.height !== height ||
613
- binding.mipLevelCount !== mipLevelCount ||
614
- binding.format !== format;
615
- if (!requiresReallocation) {
616
- const shouldUpload = sourceChanged ||
617
- update === 'perFrame' ||
618
- (update === 'onInvalidate' && (renderMode !== 'always' || tokenChanged));
619
- if (shouldUpload && binding.texture) {
620
- binding.flipY = flipY;
621
- binding.generateMipmaps = generateMipmaps;
622
- binding.premultipliedAlpha = premultipliedAlpha;
623
- binding.colorSpace = colorSpace;
624
- uploadTexture(device, binding.texture, binding, source, width, height, mipLevelCount);
625
- }
626
- binding.source = source;
627
- binding.width = width;
628
- binding.height = height;
629
- binding.mipLevelCount = mipLevelCount;
630
- binding.update = update;
631
- binding.lastToken = value;
632
- return false;
633
- }
634
- const texture = device.createTexture({
635
- size: { width, height, depthOrArrayLayers: 1 },
636
- format,
637
- mipLevelCount,
638
- usage: GPUTextureUsage.TEXTURE_BINDING |
639
- GPUTextureUsage.COPY_DST |
640
- GPUTextureUsage.RENDER_ATTACHMENT
641
- });
642
- registerInitializationCleanup(() => {
643
- texture.destroy();
644
- });
645
- binding.flipY = flipY;
646
- binding.generateMipmaps = generateMipmaps;
647
- binding.premultipliedAlpha = premultipliedAlpha;
648
- binding.colorSpace = colorSpace;
649
- binding.format = format;
650
- uploadTexture(device, texture, binding, source, width, height, mipLevelCount);
651
- binding.texture?.destroy();
652
- binding.texture = texture;
653
- binding.view = texture.createView();
654
- binding.source = source;
655
- binding.width = width;
656
- binding.height = height;
657
- binding.mipLevelCount = mipLevelCount;
658
- binding.update = update;
659
- binding.lastToken = value;
660
- return true;
661
- };
662
- for (const binding of textureBindings) {
663
- const defaultSource = normalizedTextureDefinitions[binding.key]?.source ?? null;
664
- updateTextureBinding(binding, defaultSource, 'always');
665
- }
666
- let bindGroup = createBindGroup();
667
- let sourceSlotTarget = null;
668
- let targetSlotTarget = null;
669
- let renderTargetSignature = '';
670
- let renderTargetSnapshot = {};
671
- let renderTargetKeys = [];
672
- let cachedGraphPlan = null;
673
- let cachedGraphRenderTargetSignature = '';
674
- const cachedGraphClearColor = [NaN, NaN, NaN, NaN];
675
- const cachedGraphPasses = [];
676
- let contextConfigured = false;
677
- let configuredWidth = 0;
678
- let configuredHeight = 0;
679
- const runtimeRenderTargets = new Map();
680
- const activePasses = [];
681
- const lifecyclePreviousSet = new Set();
682
- const lifecycleNextSet = new Set();
683
- const lifecycleUniquePasses = [];
684
- let lifecyclePassesRef = null;
685
- let passWidth = 0;
686
- let passHeight = 0;
687
- /**
688
- * Resolves active render pass list for current frame.
689
- */
690
- const resolvePasses = () => {
691
- return options.getPasses?.() ?? options.passes ?? [];
692
- };
693
- /**
694
- * Resolves active render target declarations for current frame.
695
- */
696
- const resolveRenderTargets = () => {
697
- return options.getRenderTargets?.() ?? options.renderTargets;
698
- };
699
- /**
700
- * Checks whether cached render-graph plan can be reused for this frame.
701
- */
702
- const isGraphPlanCacheValid = (passes, clearColor) => {
703
- if (!cachedGraphPlan) {
704
- return false;
705
- }
706
- if (cachedGraphRenderTargetSignature !== renderTargetSignature) {
707
- return false;
708
- }
709
- if (cachedGraphClearColor[0] !== clearColor[0] ||
710
- cachedGraphClearColor[1] !== clearColor[1] ||
711
- cachedGraphClearColor[2] !== clearColor[2] ||
712
- cachedGraphClearColor[3] !== clearColor[3]) {
713
- return false;
714
- }
715
- if (cachedGraphPasses.length !== passes.length) {
716
- return false;
717
- }
718
- for (let index = 0; index < passes.length; index += 1) {
719
- const pass = passes[index];
720
- const snapshot = cachedGraphPasses[index];
721
- if (!pass || !snapshot || snapshot.pass !== pass) {
722
- return false;
723
- }
724
- if (snapshot.enabled !== pass.enabled ||
725
- snapshot.needsSwap !== pass.needsSwap ||
726
- snapshot.input !== pass.input ||
727
- snapshot.output !== pass.output ||
728
- snapshot.clear !== pass.clear ||
729
- snapshot.preserve !== pass.preserve) {
730
- return false;
731
- }
732
- const passClearColor = pass.clearColor;
733
- const hasPassClearColor = passClearColor !== undefined;
734
- if (snapshot.hasClearColor !== hasPassClearColor) {
735
- return false;
736
- }
737
- if (passClearColor) {
738
- if (snapshot.clearColor0 !== passClearColor[0] ||
739
- snapshot.clearColor1 !== passClearColor[1] ||
740
- snapshot.clearColor2 !== passClearColor[2] ||
741
- snapshot.clearColor3 !== passClearColor[3]) {
742
- return false;
743
- }
744
- }
745
- }
746
- return true;
747
- };
748
- /**
749
- * Updates render-graph cache with current pass set.
750
- */
751
- const updateGraphPlanCache = (passes, clearColor, graphPlan) => {
752
- cachedGraphPlan = graphPlan;
753
- cachedGraphRenderTargetSignature = renderTargetSignature;
754
- cachedGraphClearColor[0] = clearColor[0];
755
- cachedGraphClearColor[1] = clearColor[1];
756
- cachedGraphClearColor[2] = clearColor[2];
757
- cachedGraphClearColor[3] = clearColor[3];
758
- cachedGraphPasses.length = passes.length;
759
- let index = 0;
760
- for (const pass of passes) {
761
- const passClearColor = pass.clearColor;
762
- const hasPassClearColor = passClearColor !== undefined;
763
- const snapshot = cachedGraphPasses[index];
764
- if (!snapshot) {
765
- cachedGraphPasses[index] = {
766
- pass,
767
- enabled: pass.enabled,
768
- needsSwap: pass.needsSwap,
769
- input: pass.input,
770
- output: pass.output,
771
- clear: pass.clear,
772
- preserve: pass.preserve,
773
- hasClearColor: hasPassClearColor,
774
- clearColor0: passClearColor?.[0] ?? 0,
775
- clearColor1: passClearColor?.[1] ?? 0,
776
- clearColor2: passClearColor?.[2] ?? 0,
777
- clearColor3: passClearColor?.[3] ?? 0
778
- };
779
- index += 1;
780
- continue;
781
- }
782
- snapshot.pass = pass;
783
- snapshot.enabled = pass.enabled;
784
- snapshot.needsSwap = pass.needsSwap;
785
- snapshot.input = pass.input;
786
- snapshot.output = pass.output;
787
- snapshot.clear = pass.clear;
788
- snapshot.preserve = pass.preserve;
789
- snapshot.hasClearColor = hasPassClearColor;
790
- snapshot.clearColor0 = passClearColor?.[0] ?? 0;
791
- snapshot.clearColor1 = passClearColor?.[1] ?? 0;
792
- snapshot.clearColor2 = passClearColor?.[2] ?? 0;
793
- snapshot.clearColor3 = passClearColor?.[3] ?? 0;
794
- index += 1;
795
- }
796
- };
797
- /**
798
- * Synchronizes pass lifecycle callbacks and resize notifications.
799
- */
800
- const syncPassLifecycle = (passes, width, height) => {
801
- const resized = passWidth !== width || passHeight !== height;
802
- if (!resized && lifecyclePassesRef === passes && passes.length === activePasses.length) {
803
- let isSameOrder = true;
804
- for (let index = 0; index < passes.length; index += 1) {
805
- if (activePasses[index] !== passes[index]) {
806
- isSameOrder = false;
807
- break;
808
- }
809
- }
810
- if (isSameOrder) {
811
- return;
812
- }
813
- }
814
- lifecycleNextSet.clear();
815
- lifecycleUniquePasses.length = 0;
816
- for (const pass of passes) {
817
- if (lifecycleNextSet.has(pass)) {
818
- continue;
819
- }
820
- lifecycleNextSet.add(pass);
821
- lifecycleUniquePasses.push(pass);
822
- }
823
- lifecyclePreviousSet.clear();
824
- for (const pass of activePasses) {
825
- lifecyclePreviousSet.add(pass);
826
- }
827
- for (const pass of activePasses) {
828
- if (!lifecycleNextSet.has(pass)) {
829
- pass.dispose?.();
830
- }
831
- }
832
- for (const pass of lifecycleUniquePasses) {
833
- if (resized || !lifecyclePreviousSet.has(pass)) {
834
- pass.setSize?.(width, height);
835
- }
836
- }
837
- activePasses.length = 0;
838
- for (const pass of lifecycleUniquePasses) {
839
- activePasses.push(pass);
840
- }
841
- lifecyclePassesRef = passes;
842
- passWidth = width;
843
- passHeight = height;
844
- };
845
- /**
846
- * Ensures internal ping-pong slot texture matches current canvas size/format.
847
- */
848
- const ensureSlotTarget = (slot, width, height) => {
849
- const current = slot === 'source' ? sourceSlotTarget : targetSlotTarget;
850
- if (current &&
851
- current.width === width &&
852
- current.height === height &&
853
- current.format === format) {
854
- return current;
855
- }
856
- destroyRenderTexture(current);
857
- const next = createRenderTexture(device, width, height, format);
858
- if (slot === 'source') {
859
- sourceSlotTarget = next;
860
- }
861
- else {
862
- targetSlotTarget = next;
863
- }
864
- return next;
865
- };
866
- /**
867
- * Creates/updates runtime render targets and returns immutable pass snapshot.
868
- */
869
- const syncRenderTargets = (canvasWidth, canvasHeight) => {
870
- const resolvedDefinitions = resolveRenderTargetDefinitions(resolveRenderTargets(), canvasWidth, canvasHeight, format);
871
- const nextSignature = buildRenderTargetSignature(resolvedDefinitions);
872
- if (nextSignature !== renderTargetSignature) {
873
- const activeKeys = new Set(resolvedDefinitions.map((definition) => definition.key));
874
- for (const [key, target] of runtimeRenderTargets.entries()) {
875
- if (!activeKeys.has(key)) {
876
- target.texture.destroy();
877
- runtimeRenderTargets.delete(key);
878
- }
879
- }
880
- for (const definition of resolvedDefinitions) {
881
- const current = runtimeRenderTargets.get(definition.key);
882
- if (current &&
883
- current.width === definition.width &&
884
- current.height === definition.height &&
885
- current.format === definition.format) {
886
- continue;
887
- }
888
- current?.texture.destroy();
889
- runtimeRenderTargets.set(definition.key, createRenderTexture(device, definition.width, definition.height, definition.format));
890
- }
891
- renderTargetSignature = nextSignature;
892
- const nextSnapshot = {};
893
- const nextKeys = [];
894
- for (const definition of resolvedDefinitions) {
895
- const target = runtimeRenderTargets.get(definition.key);
896
- if (!target) {
897
- continue;
898
- }
899
- nextKeys.push(definition.key);
900
- nextSnapshot[definition.key] = {
901
- texture: target.texture,
902
- view: target.view,
903
- width: target.width,
904
- height: target.height,
905
- format: target.format
906
- };
907
- }
908
- renderTargetSnapshot = nextSnapshot;
909
- renderTargetKeys = nextKeys;
910
- }
911
- return renderTargetSnapshot;
912
- };
913
- /**
914
- * Blits a texture view to the current canvas texture.
915
- */
916
- const blitToCanvas = (commandEncoder, sourceView, canvasView, clearColor) => {
917
- let bindGroup = blitBindGroupByView.get(sourceView);
918
- if (!bindGroup) {
919
- bindGroup = device.createBindGroup({
920
- layout: blitBindGroupLayout,
921
- entries: [
922
- { binding: 0, resource: blitSampler },
923
- { binding: 1, resource: sourceView }
924
- ]
925
- });
926
- blitBindGroupByView.set(sourceView, bindGroup);
927
- }
928
- const pass = commandEncoder.beginRenderPass({
929
- colorAttachments: [
930
- {
931
- view: canvasView,
932
- clearValue: {
933
- r: clearColor[0],
934
- g: clearColor[1],
935
- b: clearColor[2],
936
- a: clearColor[3]
937
- },
938
- loadOp: 'clear',
939
- storeOp: 'store'
940
- }
941
- ]
942
- });
943
- pass.setPipeline(blitPipeline);
944
- pass.setBindGroup(0, bindGroup);
945
- pass.draw(3);
946
- pass.end();
947
- };
948
- /**
949
- * Executes a full frame render.
950
- */
951
- const render = ({ time, delta, renderMode, uniforms, textures, canvasSize }) => {
952
- if (deviceLostMessage) {
953
- throw new Error(deviceLostMessage);
954
- }
955
- if (uncapturedErrorMessage) {
956
- const message = uncapturedErrorMessage;
957
- uncapturedErrorMessage = null;
958
- throw new Error(message);
959
- }
960
- const { width, height } = resizeCanvas(options.canvas, options.getDpr(), canvasSize);
961
- if (!contextConfigured || configuredWidth !== width || configuredHeight !== height) {
962
- context.configure({
963
- device,
964
- format,
965
- alphaMode: 'premultiplied'
966
- });
967
- contextConfigured = true;
968
- configuredWidth = width;
969
- configuredHeight = height;
970
- }
971
- frameScratch[0] = time;
972
- frameScratch[1] = delta;
973
- frameScratch[2] = width;
974
- frameScratch[3] = height;
975
- device.queue.writeBuffer(frameBuffer, 0, frameScratch.buffer, frameScratch.byteOffset, frameScratch.byteLength);
976
- packUniformsInto(uniforms, options.uniformLayout, uniformScratch);
977
- if (!hasUniformSnapshot) {
978
- device.queue.writeBuffer(uniformBuffer, 0, uniformScratch.buffer, uniformScratch.byteOffset, uniformScratch.byteLength);
979
- uniformPrevious.set(uniformScratch);
980
- hasUniformSnapshot = true;
981
- }
982
- else {
983
- const dirtyRanges = findDirtyFloatRanges(uniformPrevious, uniformScratch);
984
- for (const range of dirtyRanges) {
985
- const byteOffset = range.start * 4;
986
- const byteLength = range.count * 4;
987
- device.queue.writeBuffer(uniformBuffer, byteOffset, uniformScratch.buffer, uniformScratch.byteOffset + byteOffset, byteLength);
988
- }
989
- if (dirtyRanges.length > 0) {
990
- uniformPrevious.set(uniformScratch);
991
- }
992
- }
993
- let bindGroupDirty = false;
994
- for (const binding of textureBindings) {
995
- const nextTexture = textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
996
- if (updateTextureBinding(binding, nextTexture, renderMode)) {
997
- bindGroupDirty = true;
998
- }
999
- }
1000
- if (bindGroupDirty) {
1001
- bindGroup = createBindGroup();
1002
- }
1003
- const commandEncoder = device.createCommandEncoder();
1004
- const passes = resolvePasses();
1005
- const clearColor = options.getClearColor();
1006
- syncPassLifecycle(passes, width, height);
1007
- const runtimeTargets = syncRenderTargets(width, height);
1008
- const graphPlan = isGraphPlanCacheValid(passes, clearColor)
1009
- ? cachedGraphPlan
1010
- : (() => {
1011
- const nextPlan = planRenderGraph(passes, clearColor, renderTargetKeys);
1012
- updateGraphPlanCache(passes, clearColor, nextPlan);
1013
- return nextPlan;
1014
- })();
1015
- const canvasTexture = context.getCurrentTexture();
1016
- const canvasSurface = {
1017
- texture: canvasTexture,
1018
- view: canvasTexture.createView(),
1019
- width,
1020
- height,
1021
- format
1022
- };
1023
- const slots = graphPlan.steps.length > 0
1024
- ? {
1025
- source: ensureSlotTarget('source', width, height),
1026
- target: ensureSlotTarget('target', width, height),
1027
- canvas: canvasSurface
1028
- }
1029
- : null;
1030
- const sceneOutput = slots ? slots.source : canvasSurface;
1031
- const scenePass = commandEncoder.beginRenderPass({
1032
- colorAttachments: [
1033
- {
1034
- view: sceneOutput.view,
1035
- clearValue: {
1036
- r: clearColor[0],
1037
- g: clearColor[1],
1038
- b: clearColor[2],
1039
- a: clearColor[3]
1040
- },
1041
- loadOp: 'clear',
1042
- storeOp: 'store'
1043
- }
1044
- ]
1045
- });
1046
- scenePass.setPipeline(pipeline);
1047
- scenePass.setBindGroup(0, bindGroup);
1048
- scenePass.draw(3);
1049
- scenePass.end();
1050
- if (slots) {
1051
- const resolveStepSurface = (slot) => {
1052
- if (slot === 'source') {
1053
- return slots.source;
1054
- }
1055
- if (slot === 'target') {
1056
- return slots.target;
1057
- }
1058
- if (slot === 'canvas') {
1059
- return slots.canvas;
1060
- }
1061
- const named = runtimeTargets[slot];
1062
- if (!named) {
1063
- throw new Error(`Render graph references unknown runtime target "${slot}".`);
1064
- }
1065
- return named;
1066
- };
1067
- for (const step of graphPlan.steps) {
1068
- const input = resolveStepSurface(step.input);
1069
- const output = resolveStepSurface(step.output);
1070
- step.pass.render({
1071
- device,
1072
- commandEncoder,
1073
- source: slots.source,
1074
- target: slots.target,
1075
- canvas: slots.canvas,
1076
- input,
1077
- output,
1078
- targets: runtimeTargets,
1079
- time,
1080
- delta,
1081
- width,
1082
- height,
1083
- clear: step.clear,
1084
- clearColor: step.clearColor,
1085
- preserve: step.preserve,
1086
- beginRenderPass: (passOptions) => {
1087
- const clear = passOptions?.clear ?? step.clear;
1088
- const clearColor = passOptions?.clearColor ?? step.clearColor;
1089
- const preserve = passOptions?.preserve ?? step.preserve;
1090
- return commandEncoder.beginRenderPass({
1091
- colorAttachments: [
1092
- {
1093
- view: passOptions?.view ?? output.view,
1094
- clearValue: {
1095
- r: clearColor[0],
1096
- g: clearColor[1],
1097
- b: clearColor[2],
1098
- a: clearColor[3]
1099
- },
1100
- loadOp: clear ? 'clear' : 'load',
1101
- storeOp: preserve ? 'store' : 'discard'
1102
- }
1103
- ]
1104
- });
1105
- }
1106
- });
1107
- if (step.needsSwap) {
1108
- const previousSource = slots.source;
1109
- slots.source = slots.target;
1110
- slots.target = previousSource;
1111
- }
1112
- }
1113
- if (graphPlan.finalOutput !== 'canvas') {
1114
- const finalSurface = resolveStepSurface(graphPlan.finalOutput);
1115
- blitToCanvas(commandEncoder, finalSurface.view, slots.canvas.view, clearColor);
1116
- }
1117
- }
1118
- device.queue.submit([commandEncoder.finish()]);
1119
- };
1120
- acceptInitializationCleanups = false;
1121
- initializationCleanups.length = 0;
1122
- return {
1123
- render,
1124
- destroy: () => {
1125
- isDestroyed = true;
1126
- device.removeEventListener('uncapturederror', handleUncapturedError);
1127
- frameBuffer.destroy();
1128
- uniformBuffer.destroy();
1129
- destroyRenderTexture(sourceSlotTarget);
1130
- destroyRenderTexture(targetSlotTarget);
1131
- for (const target of runtimeRenderTargets.values()) {
1132
- target.texture.destroy();
1133
- }
1134
- runtimeRenderTargets.clear();
1135
- for (const pass of activePasses) {
1136
- pass.dispose?.();
1137
- }
1138
- activePasses.length = 0;
1139
- lifecyclePassesRef = null;
1140
- for (const binding of textureBindings) {
1141
- binding.texture?.destroy();
1142
- binding.fallbackTexture.destroy();
1143
- }
1144
- blitBindGroupByView = new WeakMap();
1145
- cachedGraphPlan = null;
1146
- cachedGraphPasses.length = 0;
1147
- renderTargetSnapshot = {};
1148
- renderTargetKeys = [];
1149
- }
1150
- };
1151
- }
1152
- catch (error) {
1153
- isDestroyed = true;
1154
- acceptInitializationCleanups = false;
1155
- device.removeEventListener('uncapturederror', handleUncapturedError);
1156
- runInitializationCleanups();
1157
- throw error;
1158
- }
356
+ * Creates the WebGPU renderer used by `FragCanvas`.
357
+ *
358
+ * @param options - Renderer creation options resolved from material/context state.
359
+ * @returns Renderer instance with `render` and `destroy`.
360
+ * @throws {Error} On WebGPU unavailability, shader compilation issues, or runtime setup failures.
361
+ */
362
+ async function createRenderer(options) {
363
+ if (!navigator.gpu) throw new Error("WebGPU is not available in this browser");
364
+ const context = options.canvas.getContext("webgpu");
365
+ if (!context) throw new Error("Canvas does not support webgpu context");
366
+ const format = navigator.gpu.getPreferredCanvasFormat();
367
+ const adapter = await navigator.gpu.requestAdapter(options.adapterOptions);
368
+ if (!adapter) throw new Error("Unable to acquire WebGPU adapter");
369
+ const device = await adapter.requestDevice(options.deviceDescriptor);
370
+ let isDestroyed = false;
371
+ let deviceLostMessage = null;
372
+ let uncapturedErrorMessage = null;
373
+ const initializationCleanups = [];
374
+ let acceptInitializationCleanups = true;
375
+ const registerInitializationCleanup = (cleanup) => {
376
+ if (!acceptInitializationCleanups) return;
377
+ options.__onInitializationCleanupRegistered?.();
378
+ initializationCleanups.push(cleanup);
379
+ };
380
+ const runInitializationCleanups = () => {
381
+ for (let index = initializationCleanups.length - 1; index >= 0; index -= 1) try {
382
+ initializationCleanups[index]?.();
383
+ } catch {}
384
+ initializationCleanups.length = 0;
385
+ };
386
+ device.lost.then((info) => {
387
+ if (isDestroyed) return;
388
+ const reason = info.reason ? ` (${info.reason})` : "";
389
+ const details = info.message?.trim();
390
+ deviceLostMessage = details ? `WebGPU device lost: ${details}${reason}` : `WebGPU device lost${reason}`;
391
+ });
392
+ const handleUncapturedError = (event) => {
393
+ if (isDestroyed) return;
394
+ uncapturedErrorMessage = `WebGPU uncaptured error: ${event.error instanceof Error ? event.error.message : String(event.error?.message ?? event.error)}`;
395
+ };
396
+ device.addEventListener("uncapturederror", handleUncapturedError);
397
+ try {
398
+ const runtimeContext = buildShaderCompilationRuntimeContext(options);
399
+ const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
400
+ const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, options.textureKeys, {
401
+ convertLinearToSrgb,
402
+ fragmentLineMap: options.fragmentLineMap,
403
+ ...options.storageBufferKeys !== void 0 ? { storageBufferKeys: options.storageBufferKeys } : {},
404
+ ...options.storageBufferDefinitions !== void 0 ? { storageBufferDefinitions: options.storageBufferDefinitions } : {}
405
+ });
406
+ const shaderModule = device.createShaderModule({ code: builtShader.code });
407
+ await assertCompilation(shaderModule, {
408
+ lineMap: builtShader.lineMap,
409
+ fragmentSource: options.fragmentSource,
410
+ includeSources: options.includeSources,
411
+ ...options.defineBlockSource !== void 0 ? { defineBlockSource: options.defineBlockSource } : {},
412
+ materialSource: options.materialSource ?? null,
413
+ runtimeContext
414
+ });
415
+ const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
416
+ const storageBufferKeys = options.storageBufferKeys ?? [];
417
+ const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
418
+ const storageTextureKeys = options.storageTextureKeys ?? [];
419
+ const storageTextureKeySet = new Set(storageTextureKeys);
420
+ const textureBindings = options.textureKeys.map((key, index) => {
421
+ const config = normalizedTextureDefinitions[key];
422
+ if (!config) throw new Error(`Missing texture definition for "${key}"`);
423
+ const { samplerBinding, textureBinding } = getTextureBindings(index);
424
+ const sampler = device.createSampler({
425
+ magFilter: config.filter,
426
+ minFilter: config.filter,
427
+ mipmapFilter: config.generateMipmaps ? config.filter : "nearest",
428
+ addressModeU: config.addressModeU,
429
+ addressModeV: config.addressModeV,
430
+ maxAnisotropy: config.filter === "linear" ? config.anisotropy : 1
431
+ });
432
+ const fallbackTexture = createFallbackTexture(device, config.storage ? "rgba8unorm" : config.format);
433
+ registerInitializationCleanup(() => {
434
+ fallbackTexture.destroy();
435
+ });
436
+ const fallbackView = fallbackTexture.createView();
437
+ const runtimeBinding = {
438
+ key,
439
+ samplerBinding,
440
+ textureBinding,
441
+ sampler,
442
+ fallbackTexture,
443
+ fallbackView,
444
+ texture: null,
445
+ view: fallbackView,
446
+ source: null,
447
+ width: void 0,
448
+ height: void 0,
449
+ mipLevelCount: 1,
450
+ format: config.format,
451
+ colorSpace: config.colorSpace,
452
+ defaultColorSpace: config.colorSpace,
453
+ flipY: config.flipY,
454
+ defaultFlipY: config.flipY,
455
+ generateMipmaps: config.generateMipmaps,
456
+ defaultGenerateMipmaps: config.generateMipmaps,
457
+ premultipliedAlpha: config.premultipliedAlpha,
458
+ defaultPremultipliedAlpha: config.premultipliedAlpha,
459
+ update: config.update ?? "once",
460
+ lastToken: null
461
+ };
462
+ if (config.update !== void 0) runtimeBinding.defaultUpdate = config.update;
463
+ if (config.storage && config.width && config.height) {
464
+ const storageUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
465
+ const storageTexture = device.createTexture({
466
+ size: {
467
+ width: config.width,
468
+ height: config.height,
469
+ depthOrArrayLayers: 1
470
+ },
471
+ format: config.format,
472
+ usage: storageUsage
473
+ });
474
+ registerInitializationCleanup(() => {
475
+ storageTexture.destroy();
476
+ });
477
+ runtimeBinding.texture = storageTexture;
478
+ runtimeBinding.view = storageTexture.createView();
479
+ runtimeBinding.width = config.width;
480
+ runtimeBinding.height = config.height;
481
+ }
482
+ return runtimeBinding;
483
+ });
484
+ const bindGroupLayout = device.createBindGroupLayout({ entries: createBindGroupLayoutEntries(textureBindings) });
485
+ const fragmentStorageBindGroupLayout = storageBufferKeys.length > 0 ? device.createBindGroupLayout({ entries: storageBufferKeys.map((_, index) => ({
486
+ binding: index,
487
+ visibility: GPUShaderStage.FRAGMENT,
488
+ buffer: { type: "read-only-storage" }
489
+ })) }) : null;
490
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: fragmentStorageBindGroupLayout ? [bindGroupLayout, fragmentStorageBindGroupLayout] : [bindGroupLayout] });
491
+ const pipeline = device.createRenderPipeline({
492
+ layout: pipelineLayout,
493
+ vertex: {
494
+ module: shaderModule,
495
+ entryPoint: "motiongpuVertex"
496
+ },
497
+ fragment: {
498
+ module: shaderModule,
499
+ entryPoint: "motiongpuFragment",
500
+ targets: [{ format }]
501
+ },
502
+ primitive: { topology: "triangle-list" }
503
+ });
504
+ const blitShaderModule = device.createShaderModule({ code: createFullscreenBlitShader() });
505
+ await assertCompilation(blitShaderModule);
506
+ const blitBindGroupLayout = device.createBindGroupLayout({ entries: [{
507
+ binding: 0,
508
+ visibility: GPUShaderStage.FRAGMENT,
509
+ sampler: { type: "filtering" }
510
+ }, {
511
+ binding: 1,
512
+ visibility: GPUShaderStage.FRAGMENT,
513
+ texture: {
514
+ sampleType: "float",
515
+ viewDimension: "2d",
516
+ multisampled: false
517
+ }
518
+ }] });
519
+ const blitPipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [blitBindGroupLayout] });
520
+ const blitPipeline = device.createRenderPipeline({
521
+ layout: blitPipelineLayout,
522
+ vertex: {
523
+ module: blitShaderModule,
524
+ entryPoint: "motiongpuBlitVertex"
525
+ },
526
+ fragment: {
527
+ module: blitShaderModule,
528
+ entryPoint: "motiongpuBlitFragment",
529
+ targets: [{ format }]
530
+ },
531
+ primitive: { topology: "triangle-list" }
532
+ });
533
+ const blitSampler = device.createSampler({
534
+ magFilter: "linear",
535
+ minFilter: "linear",
536
+ addressModeU: "clamp-to-edge",
537
+ addressModeV: "clamp-to-edge"
538
+ });
539
+ let blitBindGroupByView = /* @__PURE__ */ new WeakMap();
540
+ const storageBufferMap = /* @__PURE__ */ new Map();
541
+ const pingPongTexturePairs = /* @__PURE__ */ new Map();
542
+ for (const key of storageBufferKeys) {
543
+ const definition = storageBufferDefinitions[key];
544
+ if (!definition) continue;
545
+ const normalized = normalizeStorageBufferDefinition(definition);
546
+ const buffer = device.createBuffer({
547
+ size: normalized.size,
548
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
549
+ });
550
+ registerInitializationCleanup(() => {
551
+ buffer.destroy();
552
+ });
553
+ if (definition.initialData) {
554
+ const data = definition.initialData;
555
+ device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
556
+ }
557
+ storageBufferMap.set(key, buffer);
558
+ }
559
+ const fragmentStorageBindGroup = fragmentStorageBindGroupLayout && storageBufferKeys.length > 0 ? device.createBindGroup({
560
+ layout: fragmentStorageBindGroupLayout,
561
+ entries: storageBufferKeys.map((key, index) => {
562
+ const buffer = storageBufferMap.get(key);
563
+ if (!buffer) throw new Error(`Storage buffer "${key}" not allocated.`);
564
+ return {
565
+ binding: index,
566
+ resource: { buffer }
567
+ };
568
+ })
569
+ }) : null;
570
+ const ensurePingPongTexturePair = (target) => {
571
+ const existing = pingPongTexturePairs.get(target);
572
+ if (existing) return existing;
573
+ const config = normalizedTextureDefinitions[target];
574
+ if (!config || !config.storage) throw new Error(`PingPongComputePass target "${target}" must reference a texture declared with storage:true.`);
575
+ if (!config.width || !config.height) throw new Error(`PingPongComputePass target "${target}" requires explicit texture width and height.`);
576
+ const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
577
+ const textureA = device.createTexture({
578
+ size: {
579
+ width: config.width,
580
+ height: config.height,
581
+ depthOrArrayLayers: 1
582
+ },
583
+ format: config.format,
584
+ usage
585
+ });
586
+ const textureB = device.createTexture({
587
+ size: {
588
+ width: config.width,
589
+ height: config.height,
590
+ depthOrArrayLayers: 1
591
+ },
592
+ format: config.format,
593
+ usage
594
+ });
595
+ registerInitializationCleanup(() => {
596
+ textureA.destroy();
597
+ });
598
+ registerInitializationCleanup(() => {
599
+ textureB.destroy();
600
+ });
601
+ const sampleType = toGpuTextureSampleType(storageTextureSampleScalarType(config.format));
602
+ const bindGroupLayout = device.createBindGroupLayout({ entries: [{
603
+ binding: 0,
604
+ visibility: GPUShaderStage.COMPUTE,
605
+ texture: {
606
+ sampleType,
607
+ viewDimension: "2d",
608
+ multisampled: false
609
+ }
610
+ }, {
611
+ binding: 1,
612
+ visibility: GPUShaderStage.COMPUTE,
613
+ storageTexture: {
614
+ access: "write-only",
615
+ format: config.format,
616
+ viewDimension: "2d"
617
+ }
618
+ }] });
619
+ const pair = {
620
+ target,
621
+ format: config.format,
622
+ width: config.width,
623
+ height: config.height,
624
+ textureA,
625
+ viewA: textureA.createView(),
626
+ textureB,
627
+ viewB: textureB.createView(),
628
+ bindGroupLayout
629
+ };
630
+ pingPongTexturePairs.set(target, pair);
631
+ return pair;
632
+ };
633
+ const computePipelineCache = /* @__PURE__ */ new Map();
634
+ const buildComputePipelineEntry = (buildOptions) => {
635
+ const cacheKey = buildOptions.pingPongTarget && buildOptions.pingPongFormat ? `pingpong:${buildOptions.pingPongTarget}:${buildOptions.pingPongFormat}:${buildOptions.computeSource}` : `compute:${buildOptions.computeSource}`;
636
+ const cached = computePipelineCache.get(cacheKey);
637
+ if (cached) return cached;
638
+ const storageBufferDefs = {};
639
+ for (const key of storageBufferKeys) {
640
+ const def = storageBufferDefinitions[key];
641
+ if (def) {
642
+ const norm = normalizeStorageBufferDefinition(def);
643
+ storageBufferDefs[key] = {
644
+ type: norm.type,
645
+ access: norm.access
646
+ };
647
+ }
648
+ }
649
+ const storageTextureDefs = {};
650
+ for (const key of storageTextureKeys) {
651
+ const texDef = options.textureDefinitions[key];
652
+ if (texDef?.format) storageTextureDefs[key] = { format: texDef.format };
653
+ }
654
+ const isPingPongPipeline = Boolean(buildOptions.pingPongTarget && buildOptions.pingPongFormat);
655
+ const shaderCode = isPingPongPipeline ? buildPingPongComputeShaderSource({
656
+ compute: buildOptions.computeSource,
657
+ uniformLayout: options.uniformLayout,
658
+ storageBufferKeys,
659
+ storageBufferDefinitions: storageBufferDefs,
660
+ target: buildOptions.pingPongTarget,
661
+ targetFormat: buildOptions.pingPongFormat
662
+ }) : buildComputeShaderSource({
663
+ compute: buildOptions.computeSource,
664
+ uniformLayout: options.uniformLayout,
665
+ storageBufferKeys,
666
+ storageBufferDefinitions: storageBufferDefs,
667
+ storageTextureKeys,
668
+ storageTextureDefinitions: storageTextureDefs
669
+ });
670
+ const computeShaderModule = device.createShaderModule({ code: shaderCode });
671
+ const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
672
+ const computeUniformBGL = device.createBindGroupLayout({ entries: [{
673
+ binding: FRAME_BINDING,
674
+ visibility: GPUShaderStage.COMPUTE,
675
+ buffer: {
676
+ type: "uniform",
677
+ minBindingSize: 16
678
+ }
679
+ }, {
680
+ binding: UNIFORM_BINDING,
681
+ visibility: GPUShaderStage.COMPUTE,
682
+ buffer: { type: "uniform" }
683
+ }] });
684
+ const storageBGLEntries = storageBufferKeys.map((key, index) => {
685
+ const bufferType = (storageBufferDefinitions[key]?.access ?? "read-write") === "read" ? "read-only-storage" : "storage";
686
+ return {
687
+ binding: index,
688
+ visibility: GPUShaderStage.COMPUTE,
689
+ buffer: { type: bufferType }
690
+ };
691
+ });
692
+ const storageBGL = storageBGLEntries.length > 0 ? device.createBindGroupLayout({ entries: storageBGLEntries }) : null;
693
+ const storageTextureBGLEntries = isPingPongPipeline ? [{
694
+ binding: 0,
695
+ visibility: GPUShaderStage.COMPUTE,
696
+ texture: {
697
+ sampleType: toGpuTextureSampleType(storageTextureSampleScalarType(buildOptions.pingPongFormat)),
698
+ viewDimension: "2d",
699
+ multisampled: false
700
+ }
701
+ }, {
702
+ binding: 1,
703
+ visibility: GPUShaderStage.COMPUTE,
704
+ storageTexture: {
705
+ access: "write-only",
706
+ format: buildOptions.pingPongFormat,
707
+ viewDimension: "2d"
708
+ }
709
+ }] : storageTextureKeys.map((key, index) => {
710
+ const texDef = options.textureDefinitions[key];
711
+ return {
712
+ binding: index,
713
+ visibility: GPUShaderStage.COMPUTE,
714
+ storageTexture: {
715
+ access: "write-only",
716
+ format: texDef?.format ?? "rgba8unorm",
717
+ viewDimension: "2d"
718
+ }
719
+ };
720
+ });
721
+ const storageTextureBGL = storageTextureBGLEntries.length > 0 ? device.createBindGroupLayout({ entries: storageTextureBGLEntries }) : null;
722
+ const bindGroupLayouts = [computeUniformBGL];
723
+ if (storageBGL || storageTextureBGL) bindGroupLayouts.push(storageBGL ?? device.createBindGroupLayout({ entries: [] }));
724
+ if (storageTextureBGL) bindGroupLayouts.push(storageTextureBGL);
725
+ const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
726
+ const entry = {
727
+ pipeline: device.createComputePipeline({
728
+ layout: computePipelineLayout,
729
+ compute: {
730
+ module: computeShaderModule,
731
+ entryPoint: "compute"
732
+ }
733
+ }),
734
+ bindGroup: device.createBindGroup({
735
+ layout: computeUniformBGL,
736
+ entries: [{
737
+ binding: FRAME_BINDING,
738
+ resource: { buffer: frameBuffer }
739
+ }, {
740
+ binding: UNIFORM_BINDING,
741
+ resource: { buffer: uniformBuffer }
742
+ }]
743
+ }),
744
+ workgroupSize,
745
+ computeSource: buildOptions.computeSource
746
+ };
747
+ computePipelineCache.set(cacheKey, entry);
748
+ return entry;
749
+ };
750
+ const getComputeStorageBindGroup = () => {
751
+ if (storageBufferKeys.length === 0) return null;
752
+ const storageBGLEntries = storageBufferKeys.map((key, index) => {
753
+ const bufferType = (storageBufferDefinitions[key]?.access ?? "read-write") === "read" ? "read-only-storage" : "storage";
754
+ return {
755
+ binding: index,
756
+ visibility: GPUShaderStage.COMPUTE,
757
+ buffer: { type: bufferType }
758
+ };
759
+ });
760
+ const storageBGL = device.createBindGroupLayout({ entries: storageBGLEntries });
761
+ const storageEntries = storageBufferKeys.map((key, index) => {
762
+ const buffer = storageBufferMap.get(key);
763
+ if (!buffer) throw new Error(`Storage buffer "${key}" not allocated.`);
764
+ return {
765
+ binding: index,
766
+ resource: { buffer }
767
+ };
768
+ });
769
+ return device.createBindGroup({
770
+ layout: storageBGL,
771
+ entries: storageEntries
772
+ });
773
+ };
774
+ const getComputeStorageTextureBindGroup = () => {
775
+ if (storageTextureKeys.length === 0) return null;
776
+ const entries = storageTextureKeys.map((key, index) => {
777
+ const texDef = options.textureDefinitions[key];
778
+ return {
779
+ binding: index,
780
+ visibility: GPUShaderStage.COMPUTE,
781
+ storageTexture: {
782
+ access: "write-only",
783
+ format: texDef?.format ?? "rgba8unorm",
784
+ viewDimension: "2d"
785
+ }
786
+ };
787
+ });
788
+ const bgl = device.createBindGroupLayout({ entries });
789
+ const bgEntries = storageTextureKeys.map((key, index) => {
790
+ const binding = textureBindings.find((b) => b.key === key);
791
+ if (!binding || !binding.texture) throw new Error(`Storage texture "${key}" not allocated.`);
792
+ return {
793
+ binding: index,
794
+ resource: binding.view
795
+ };
796
+ });
797
+ return device.createBindGroup({
798
+ layout: bgl,
799
+ entries: bgEntries
800
+ });
801
+ };
802
+ const getPingPongStorageTextureBindGroup = (target, readFromA) => {
803
+ const pair = ensurePingPongTexturePair(target);
804
+ const readView = readFromA ? pair.viewA : pair.viewB;
805
+ const writeView = readFromA ? pair.viewB : pair.viewA;
806
+ return device.createBindGroup({
807
+ layout: pair.bindGroupLayout,
808
+ entries: [{
809
+ binding: 0,
810
+ resource: readView
811
+ }, {
812
+ binding: 1,
813
+ resource: writeView
814
+ }]
815
+ });
816
+ };
817
+ const frameBuffer = device.createBuffer({
818
+ size: 16,
819
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
820
+ });
821
+ registerInitializationCleanup(() => {
822
+ frameBuffer.destroy();
823
+ });
824
+ const uniformBuffer = device.createBuffer({
825
+ size: options.uniformLayout.byteLength,
826
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
827
+ });
828
+ registerInitializationCleanup(() => {
829
+ uniformBuffer.destroy();
830
+ });
831
+ const frameScratch = new Float32Array(4);
832
+ const uniformScratch = new Float32Array(options.uniformLayout.byteLength / 4);
833
+ const uniformPrevious = new Float32Array(options.uniformLayout.byteLength / 4);
834
+ let hasUniformSnapshot = false;
835
+ /**
836
+ * Rebuilds bind group using current texture views.
837
+ */
838
+ const createBindGroup = () => {
839
+ const entries = [{
840
+ binding: FRAME_BINDING,
841
+ resource: { buffer: frameBuffer }
842
+ }, {
843
+ binding: UNIFORM_BINDING,
844
+ resource: { buffer: uniformBuffer }
845
+ }];
846
+ for (const binding of textureBindings) {
847
+ entries.push({
848
+ binding: binding.samplerBinding,
849
+ resource: binding.sampler
850
+ });
851
+ entries.push({
852
+ binding: binding.textureBinding,
853
+ resource: binding.view
854
+ });
855
+ }
856
+ return device.createBindGroup({
857
+ layout: bindGroupLayout,
858
+ entries
859
+ });
860
+ };
861
+ /**
862
+ * Synchronizes one runtime texture binding with incoming texture value.
863
+ *
864
+ * @returns `true` when bind group must be rebuilt.
865
+ */
866
+ const updateTextureBinding = (binding, value, renderMode) => {
867
+ const nextData = toTextureData(value);
868
+ if (!nextData) {
869
+ if (binding.source === null && binding.texture === null) return false;
870
+ binding.texture?.destroy();
871
+ binding.texture = null;
872
+ binding.view = binding.fallbackView;
873
+ binding.source = null;
874
+ binding.width = void 0;
875
+ binding.height = void 0;
876
+ binding.lastToken = null;
877
+ return true;
878
+ }
879
+ const source = nextData.source;
880
+ const colorSpace = nextData.colorSpace ?? binding.defaultColorSpace;
881
+ const format = colorSpace === "linear" ? "rgba8unorm" : "rgba8unorm-srgb";
882
+ const flipY = nextData.flipY ?? binding.defaultFlipY;
883
+ const premultipliedAlpha = nextData.premultipliedAlpha ?? binding.defaultPremultipliedAlpha;
884
+ const generateMipmaps = nextData.generateMipmaps ?? binding.defaultGenerateMipmaps;
885
+ const update = resolveTextureUpdateMode({
886
+ source,
887
+ ...nextData.update !== void 0 ? { override: nextData.update } : {},
888
+ ...binding.defaultUpdate !== void 0 ? { defaultMode: binding.defaultUpdate } : {}
889
+ });
890
+ const { width, height } = resolveTextureSize(nextData);
891
+ const mipLevelCount = generateMipmaps ? getTextureMipLevelCount(width, height) : 1;
892
+ const sourceChanged = binding.source !== source;
893
+ const tokenChanged = binding.lastToken !== value;
894
+ if (!(binding.texture === null || binding.width !== width || binding.height !== height || binding.mipLevelCount !== mipLevelCount || binding.format !== format)) {
895
+ if ((sourceChanged || update === "perFrame" || update === "onInvalidate" && (renderMode !== "always" || tokenChanged)) && binding.texture) {
896
+ binding.flipY = flipY;
897
+ binding.generateMipmaps = generateMipmaps;
898
+ binding.premultipliedAlpha = premultipliedAlpha;
899
+ binding.colorSpace = colorSpace;
900
+ uploadTexture(device, binding.texture, binding, source, width, height, mipLevelCount);
901
+ }
902
+ binding.source = source;
903
+ binding.width = width;
904
+ binding.height = height;
905
+ binding.mipLevelCount = mipLevelCount;
906
+ binding.update = update;
907
+ binding.lastToken = value;
908
+ return false;
909
+ }
910
+ let textureUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
911
+ if (storageTextureKeySet.has(binding.key)) textureUsage |= GPUTextureUsage.STORAGE_BINDING;
912
+ const texture = device.createTexture({
913
+ size: {
914
+ width,
915
+ height,
916
+ depthOrArrayLayers: 1
917
+ },
918
+ format,
919
+ mipLevelCount,
920
+ usage: textureUsage
921
+ });
922
+ registerInitializationCleanup(() => {
923
+ texture.destroy();
924
+ });
925
+ binding.flipY = flipY;
926
+ binding.generateMipmaps = generateMipmaps;
927
+ binding.premultipliedAlpha = premultipliedAlpha;
928
+ binding.colorSpace = colorSpace;
929
+ binding.format = format;
930
+ uploadTexture(device, texture, binding, source, width, height, mipLevelCount);
931
+ binding.texture?.destroy();
932
+ binding.texture = texture;
933
+ binding.view = texture.createView();
934
+ binding.source = source;
935
+ binding.width = width;
936
+ binding.height = height;
937
+ binding.mipLevelCount = mipLevelCount;
938
+ binding.update = update;
939
+ binding.lastToken = value;
940
+ return true;
941
+ };
942
+ for (const binding of textureBindings) {
943
+ if (storageTextureKeySet.has(binding.key)) continue;
944
+ updateTextureBinding(binding, normalizedTextureDefinitions[binding.key]?.source ?? null, "always");
945
+ }
946
+ let bindGroup = createBindGroup();
947
+ let sourceSlotTarget = null;
948
+ let targetSlotTarget = null;
949
+ let renderTargetSignature = "";
950
+ let renderTargetSnapshot = {};
951
+ let renderTargetKeys = [];
952
+ let cachedGraphPlan = null;
953
+ let cachedGraphRenderTargetSignature = "";
954
+ const cachedGraphClearColor = [
955
+ NaN,
956
+ NaN,
957
+ NaN,
958
+ NaN
959
+ ];
960
+ const cachedGraphPasses = [];
961
+ let contextConfigured = false;
962
+ let configuredWidth = 0;
963
+ let configuredHeight = 0;
964
+ const runtimeRenderTargets = /* @__PURE__ */ new Map();
965
+ const activePasses = [];
966
+ const lifecyclePreviousSet = /* @__PURE__ */ new Set();
967
+ const lifecycleNextSet = /* @__PURE__ */ new Set();
968
+ const lifecycleUniquePasses = [];
969
+ let lifecyclePassesRef = null;
970
+ let passWidth = 0;
971
+ let passHeight = 0;
972
+ /**
973
+ * Resolves active render pass list for current frame.
974
+ */
975
+ const resolvePasses = () => {
976
+ return options.getPasses?.() ?? options.passes ?? [];
977
+ };
978
+ /**
979
+ * Resolves active render target declarations for current frame.
980
+ */
981
+ const resolveRenderTargets = () => {
982
+ return options.getRenderTargets?.() ?? options.renderTargets;
983
+ };
984
+ /**
985
+ * Checks whether cached render-graph plan can be reused for this frame.
986
+ */
987
+ const isGraphPlanCacheValid = (passes, clearColor) => {
988
+ if (!cachedGraphPlan) return false;
989
+ if (cachedGraphRenderTargetSignature !== renderTargetSignature) return false;
990
+ if (cachedGraphClearColor[0] !== clearColor[0] || cachedGraphClearColor[1] !== clearColor[1] || cachedGraphClearColor[2] !== clearColor[2] || cachedGraphClearColor[3] !== clearColor[3]) return false;
991
+ if (cachedGraphPasses.length !== passes.length) return false;
992
+ for (let index = 0; index < passes.length; index += 1) {
993
+ const pass = passes[index];
994
+ const rp = pass;
995
+ const snapshot = cachedGraphPasses[index];
996
+ if (!pass || !snapshot || snapshot.pass !== pass) return false;
997
+ if (snapshot.enabled !== pass.enabled || snapshot.needsSwap !== rp.needsSwap || snapshot.input !== rp.input || snapshot.output !== rp.output || snapshot.clear !== rp.clear || snapshot.preserve !== rp.preserve) return false;
998
+ const passClearColor = rp.clearColor;
999
+ const hasPassClearColor = passClearColor !== void 0;
1000
+ if (snapshot.hasClearColor !== hasPassClearColor) return false;
1001
+ if (passClearColor) {
1002
+ if (snapshot.clearColor0 !== passClearColor[0] || snapshot.clearColor1 !== passClearColor[1] || snapshot.clearColor2 !== passClearColor[2] || snapshot.clearColor3 !== passClearColor[3]) return false;
1003
+ }
1004
+ }
1005
+ return true;
1006
+ };
1007
+ /**
1008
+ * Updates render-graph cache with current pass set.
1009
+ */
1010
+ const updateGraphPlanCache = (passes, clearColor, graphPlan) => {
1011
+ cachedGraphPlan = graphPlan;
1012
+ cachedGraphRenderTargetSignature = renderTargetSignature;
1013
+ cachedGraphClearColor[0] = clearColor[0];
1014
+ cachedGraphClearColor[1] = clearColor[1];
1015
+ cachedGraphClearColor[2] = clearColor[2];
1016
+ cachedGraphClearColor[3] = clearColor[3];
1017
+ cachedGraphPasses.length = passes.length;
1018
+ let index = 0;
1019
+ for (const pass of passes) {
1020
+ const rp = pass;
1021
+ const passClearColor = rp.clearColor;
1022
+ const hasPassClearColor = passClearColor !== void 0;
1023
+ const snapshot = cachedGraphPasses[index];
1024
+ if (!snapshot) {
1025
+ cachedGraphPasses[index] = {
1026
+ pass,
1027
+ enabled: pass.enabled,
1028
+ needsSwap: rp.needsSwap,
1029
+ input: rp.input,
1030
+ output: rp.output,
1031
+ clear: rp.clear,
1032
+ preserve: rp.preserve,
1033
+ hasClearColor: hasPassClearColor,
1034
+ clearColor0: passClearColor?.[0] ?? 0,
1035
+ clearColor1: passClearColor?.[1] ?? 0,
1036
+ clearColor2: passClearColor?.[2] ?? 0,
1037
+ clearColor3: passClearColor?.[3] ?? 0
1038
+ };
1039
+ index += 1;
1040
+ continue;
1041
+ }
1042
+ snapshot.pass = pass;
1043
+ snapshot.enabled = pass.enabled;
1044
+ snapshot.needsSwap = rp.needsSwap;
1045
+ snapshot.input = rp.input;
1046
+ snapshot.output = rp.output;
1047
+ snapshot.clear = rp.clear;
1048
+ snapshot.preserve = rp.preserve;
1049
+ snapshot.hasClearColor = hasPassClearColor;
1050
+ snapshot.clearColor0 = passClearColor?.[0] ?? 0;
1051
+ snapshot.clearColor1 = passClearColor?.[1] ?? 0;
1052
+ snapshot.clearColor2 = passClearColor?.[2] ?? 0;
1053
+ snapshot.clearColor3 = passClearColor?.[3] ?? 0;
1054
+ index += 1;
1055
+ }
1056
+ };
1057
+ /**
1058
+ * Synchronizes pass lifecycle callbacks and resize notifications.
1059
+ */
1060
+ const syncPassLifecycle = (passes, width, height) => {
1061
+ const resized = passWidth !== width || passHeight !== height;
1062
+ if (!resized && lifecyclePassesRef === passes && passes.length === activePasses.length) {
1063
+ let isSameOrder = true;
1064
+ for (let index = 0; index < passes.length; index += 1) if (activePasses[index] !== passes[index]) {
1065
+ isSameOrder = false;
1066
+ break;
1067
+ }
1068
+ if (isSameOrder) return;
1069
+ }
1070
+ lifecycleNextSet.clear();
1071
+ lifecycleUniquePasses.length = 0;
1072
+ for (const pass of passes) {
1073
+ if (lifecycleNextSet.has(pass)) continue;
1074
+ lifecycleNextSet.add(pass);
1075
+ lifecycleUniquePasses.push(pass);
1076
+ }
1077
+ lifecyclePreviousSet.clear();
1078
+ for (const pass of activePasses) lifecyclePreviousSet.add(pass);
1079
+ for (const pass of activePasses) if (!lifecycleNextSet.has(pass)) pass.dispose?.();
1080
+ for (const pass of lifecycleUniquePasses) if (resized || !lifecyclePreviousSet.has(pass)) pass.setSize?.(width, height);
1081
+ activePasses.length = 0;
1082
+ for (const pass of lifecycleUniquePasses) activePasses.push(pass);
1083
+ lifecyclePassesRef = passes;
1084
+ passWidth = width;
1085
+ passHeight = height;
1086
+ };
1087
+ /**
1088
+ * Ensures internal ping-pong slot texture matches current canvas size/format.
1089
+ */
1090
+ const ensureSlotTarget = (slot, width, height) => {
1091
+ const current = slot === "source" ? sourceSlotTarget : targetSlotTarget;
1092
+ if (current && current.width === width && current.height === height && current.format === format) return current;
1093
+ destroyRenderTexture(current);
1094
+ const next = createRenderTexture(device, width, height, format);
1095
+ if (slot === "source") sourceSlotTarget = next;
1096
+ else targetSlotTarget = next;
1097
+ return next;
1098
+ };
1099
+ /**
1100
+ * Creates/updates runtime render targets and returns immutable pass snapshot.
1101
+ */
1102
+ const syncRenderTargets = (canvasWidth, canvasHeight) => {
1103
+ const resolvedDefinitions = resolveRenderTargetDefinitions(resolveRenderTargets(), canvasWidth, canvasHeight, format);
1104
+ const nextSignature = buildRenderTargetSignature(resolvedDefinitions);
1105
+ if (nextSignature !== renderTargetSignature) {
1106
+ const activeKeys = new Set(resolvedDefinitions.map((definition) => definition.key));
1107
+ for (const [key, target] of runtimeRenderTargets.entries()) if (!activeKeys.has(key)) {
1108
+ target.texture.destroy();
1109
+ runtimeRenderTargets.delete(key);
1110
+ }
1111
+ for (const definition of resolvedDefinitions) {
1112
+ const current = runtimeRenderTargets.get(definition.key);
1113
+ if (current && current.width === definition.width && current.height === definition.height && current.format === definition.format) continue;
1114
+ current?.texture.destroy();
1115
+ runtimeRenderTargets.set(definition.key, createRenderTexture(device, definition.width, definition.height, definition.format));
1116
+ }
1117
+ renderTargetSignature = nextSignature;
1118
+ const nextSnapshot = {};
1119
+ const nextKeys = [];
1120
+ for (const definition of resolvedDefinitions) {
1121
+ const target = runtimeRenderTargets.get(definition.key);
1122
+ if (!target) continue;
1123
+ nextKeys.push(definition.key);
1124
+ nextSnapshot[definition.key] = {
1125
+ texture: target.texture,
1126
+ view: target.view,
1127
+ width: target.width,
1128
+ height: target.height,
1129
+ format: target.format
1130
+ };
1131
+ }
1132
+ renderTargetSnapshot = nextSnapshot;
1133
+ renderTargetKeys = nextKeys;
1134
+ }
1135
+ return renderTargetSnapshot;
1136
+ };
1137
+ /**
1138
+ * Blits a texture view to the current canvas texture.
1139
+ */
1140
+ const blitToCanvas = (commandEncoder, sourceView, canvasView, clearColor) => {
1141
+ let bindGroup = blitBindGroupByView.get(sourceView);
1142
+ if (!bindGroup) {
1143
+ bindGroup = device.createBindGroup({
1144
+ layout: blitBindGroupLayout,
1145
+ entries: [{
1146
+ binding: 0,
1147
+ resource: blitSampler
1148
+ }, {
1149
+ binding: 1,
1150
+ resource: sourceView
1151
+ }]
1152
+ });
1153
+ blitBindGroupByView.set(sourceView, bindGroup);
1154
+ }
1155
+ const pass = commandEncoder.beginRenderPass({ colorAttachments: [{
1156
+ view: canvasView,
1157
+ clearValue: {
1158
+ r: clearColor[0],
1159
+ g: clearColor[1],
1160
+ b: clearColor[2],
1161
+ a: clearColor[3]
1162
+ },
1163
+ loadOp: "clear",
1164
+ storeOp: "store"
1165
+ }] });
1166
+ pass.setPipeline(blitPipeline);
1167
+ pass.setBindGroup(0, bindGroup);
1168
+ pass.draw(3);
1169
+ pass.end();
1170
+ };
1171
+ /**
1172
+ * Executes a full frame render.
1173
+ */
1174
+ const render = ({ time, delta, renderMode, uniforms, textures, canvasSize, pendingStorageWrites }) => {
1175
+ if (deviceLostMessage) throw new Error(deviceLostMessage);
1176
+ if (uncapturedErrorMessage) {
1177
+ const message = uncapturedErrorMessage;
1178
+ uncapturedErrorMessage = null;
1179
+ throw new Error(message);
1180
+ }
1181
+ const { width, height } = resizeCanvas(options.canvas, options.getDpr(), canvasSize);
1182
+ if (!contextConfigured || configuredWidth !== width || configuredHeight !== height) {
1183
+ context.configure({
1184
+ device,
1185
+ format,
1186
+ alphaMode: "premultiplied"
1187
+ });
1188
+ contextConfigured = true;
1189
+ configuredWidth = width;
1190
+ configuredHeight = height;
1191
+ }
1192
+ frameScratch[0] = time;
1193
+ frameScratch[1] = delta;
1194
+ frameScratch[2] = width;
1195
+ frameScratch[3] = height;
1196
+ device.queue.writeBuffer(frameBuffer, 0, frameScratch.buffer, frameScratch.byteOffset, frameScratch.byteLength);
1197
+ packUniformsInto(uniforms, options.uniformLayout, uniformScratch);
1198
+ if (!hasUniformSnapshot) {
1199
+ device.queue.writeBuffer(uniformBuffer, 0, uniformScratch.buffer, uniformScratch.byteOffset, uniformScratch.byteLength);
1200
+ uniformPrevious.set(uniformScratch);
1201
+ hasUniformSnapshot = true;
1202
+ } else {
1203
+ const dirtyRanges = findDirtyFloatRanges(uniformPrevious, uniformScratch);
1204
+ for (const range of dirtyRanges) {
1205
+ const byteOffset = range.start * 4;
1206
+ const byteLength = range.count * 4;
1207
+ device.queue.writeBuffer(uniformBuffer, byteOffset, uniformScratch.buffer, uniformScratch.byteOffset + byteOffset, byteLength);
1208
+ }
1209
+ if (dirtyRanges.length > 0) uniformPrevious.set(uniformScratch);
1210
+ }
1211
+ let bindGroupDirty = false;
1212
+ for (const binding of textureBindings) {
1213
+ if (storageTextureKeySet.has(binding.key)) continue;
1214
+ if (updateTextureBinding(binding, textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null, renderMode)) bindGroupDirty = true;
1215
+ }
1216
+ if (bindGroupDirty) bindGroup = createBindGroup();
1217
+ if (pendingStorageWrites) for (const write of pendingStorageWrites) {
1218
+ const buffer = storageBufferMap.get(write.name);
1219
+ if (buffer) {
1220
+ const data = write.data;
1221
+ device.queue.writeBuffer(buffer, write.offset, data.buffer, data.byteOffset, data.byteLength);
1222
+ }
1223
+ }
1224
+ const commandEncoder = device.createCommandEncoder();
1225
+ const passes = resolvePasses();
1226
+ const clearColor = options.getClearColor();
1227
+ syncPassLifecycle(passes, width, height);
1228
+ const runtimeTargets = syncRenderTargets(width, height);
1229
+ const graphPlan = isGraphPlanCacheValid(passes, clearColor) ? cachedGraphPlan : (() => {
1230
+ const nextPlan = planRenderGraph(passes, clearColor, renderTargetKeys);
1231
+ updateGraphPlanCache(passes, clearColor, nextPlan);
1232
+ return nextPlan;
1233
+ })();
1234
+ const canvasTexture = context.getCurrentTexture();
1235
+ const canvasSurface = {
1236
+ texture: canvasTexture,
1237
+ view: canvasTexture.createView(),
1238
+ width,
1239
+ height,
1240
+ format
1241
+ };
1242
+ const slots = graphPlan.steps.length > 0 ? {
1243
+ source: ensureSlotTarget("source", width, height),
1244
+ target: ensureSlotTarget("target", width, height),
1245
+ canvas: canvasSurface
1246
+ } : null;
1247
+ const sceneOutput = slots ? slots.source : canvasSurface;
1248
+ if (slots) for (const step of graphPlan.steps) {
1249
+ if (step.kind !== "compute") continue;
1250
+ const computePass = step.pass;
1251
+ if (computePass.getCompute && computePass.resolveDispatch && computePass.getWorkgroupSize) {
1252
+ const computeSource = computePass.getCompute();
1253
+ const pingPongTarget = computePass.isPingPong && computePass.getTarget ? computePass.getTarget() : void 0;
1254
+ if (computePass.isPingPong && !pingPongTarget) throw new Error("PingPongComputePass must provide a target texture key.");
1255
+ const pingPongPair = pingPongTarget ? ensurePingPongTexturePair(pingPongTarget) : null;
1256
+ const pipelineEntry = buildComputePipelineEntry({
1257
+ computeSource,
1258
+ ...pingPongPair ? {
1259
+ pingPongTarget: pingPongPair.target,
1260
+ pingPongFormat: pingPongPair.format
1261
+ } : {}
1262
+ });
1263
+ const workgroupSize = computePass.getWorkgroupSize();
1264
+ const storageBindGroup = getComputeStorageBindGroup();
1265
+ const storageTextureBindGroup = getComputeStorageTextureBindGroup();
1266
+ const iterations = computePass.isPingPong && computePass.getIterations ? computePass.getIterations() : 1;
1267
+ const currentOutput = computePass.isPingPong && computePass.getCurrentOutput ? computePass.getCurrentOutput() : null;
1268
+ const readFromAAtIterationZero = pingPongPair && currentOutput ? currentOutput !== `${pingPongPair.target}B` : true;
1269
+ for (let iter = 0; iter < iterations; iter += 1) {
1270
+ const dispatch = computePass.resolveDispatch({
1271
+ width,
1272
+ height,
1273
+ time,
1274
+ delta,
1275
+ workgroupSize
1276
+ });
1277
+ const cPass = commandEncoder.beginComputePass();
1278
+ cPass.setPipeline(pipelineEntry.pipeline);
1279
+ cPass.setBindGroup(0, pipelineEntry.bindGroup);
1280
+ if (storageBindGroup) cPass.setBindGroup(1, storageBindGroup);
1281
+ if (pingPongPair) {
1282
+ const readFromA = iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
1283
+ cPass.setBindGroup(2, getPingPongStorageTextureBindGroup(pingPongPair.target, readFromA));
1284
+ } else if (storageTextureBindGroup) cPass.setBindGroup(2, storageTextureBindGroup);
1285
+ cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
1286
+ cPass.end();
1287
+ }
1288
+ if (computePass.isPingPong && computePass.advanceFrame) computePass.advanceFrame();
1289
+ }
1290
+ }
1291
+ const scenePass = commandEncoder.beginRenderPass({ colorAttachments: [{
1292
+ view: sceneOutput.view,
1293
+ clearValue: {
1294
+ r: clearColor[0],
1295
+ g: clearColor[1],
1296
+ b: clearColor[2],
1297
+ a: clearColor[3]
1298
+ },
1299
+ loadOp: "clear",
1300
+ storeOp: "store"
1301
+ }] });
1302
+ scenePass.setPipeline(pipeline);
1303
+ scenePass.setBindGroup(0, bindGroup);
1304
+ if (fragmentStorageBindGroup) scenePass.setBindGroup(1, fragmentStorageBindGroup);
1305
+ scenePass.draw(3);
1306
+ scenePass.end();
1307
+ if (slots) {
1308
+ const resolveStepSurface = (slot) => {
1309
+ if (slot === "source") return slots.source;
1310
+ if (slot === "target") return slots.target;
1311
+ if (slot === "canvas") return slots.canvas;
1312
+ const named = runtimeTargets[slot];
1313
+ if (!named) throw new Error(`Render graph references unknown runtime target "${slot}".`);
1314
+ return named;
1315
+ };
1316
+ for (const step of graphPlan.steps) {
1317
+ if (step.kind === "compute") continue;
1318
+ const input = resolveStepSurface(step.input);
1319
+ const output = resolveStepSurface(step.output);
1320
+ step.pass.render({
1321
+ device,
1322
+ commandEncoder,
1323
+ source: slots.source,
1324
+ target: slots.target,
1325
+ canvas: slots.canvas,
1326
+ input,
1327
+ output,
1328
+ targets: runtimeTargets,
1329
+ time,
1330
+ delta,
1331
+ width,
1332
+ height,
1333
+ clear: step.clear,
1334
+ clearColor: step.clearColor,
1335
+ preserve: step.preserve,
1336
+ beginRenderPass: (passOptions) => {
1337
+ const clear = passOptions?.clear ?? step.clear;
1338
+ const clearColor = passOptions?.clearColor ?? step.clearColor;
1339
+ const preserve = passOptions?.preserve ?? step.preserve;
1340
+ return commandEncoder.beginRenderPass({ colorAttachments: [{
1341
+ view: passOptions?.view ?? output.view,
1342
+ clearValue: {
1343
+ r: clearColor[0],
1344
+ g: clearColor[1],
1345
+ b: clearColor[2],
1346
+ a: clearColor[3]
1347
+ },
1348
+ loadOp: clear ? "clear" : "load",
1349
+ storeOp: preserve ? "store" : "discard"
1350
+ }] });
1351
+ }
1352
+ });
1353
+ if (step.needsSwap) {
1354
+ const previousSource = slots.source;
1355
+ slots.source = slots.target;
1356
+ slots.target = previousSource;
1357
+ }
1358
+ }
1359
+ if (graphPlan.finalOutput !== "canvas") blitToCanvas(commandEncoder, resolveStepSurface(graphPlan.finalOutput).view, slots.canvas.view, clearColor);
1360
+ }
1361
+ device.queue.submit([commandEncoder.finish()]);
1362
+ };
1363
+ acceptInitializationCleanups = false;
1364
+ initializationCleanups.length = 0;
1365
+ return {
1366
+ render,
1367
+ getStorageBuffer: (name) => {
1368
+ return storageBufferMap.get(name);
1369
+ },
1370
+ getDevice: () => {
1371
+ return device;
1372
+ },
1373
+ destroy: () => {
1374
+ isDestroyed = true;
1375
+ device.removeEventListener("uncapturederror", handleUncapturedError);
1376
+ frameBuffer.destroy();
1377
+ uniformBuffer.destroy();
1378
+ for (const buffer of storageBufferMap.values()) buffer.destroy();
1379
+ storageBufferMap.clear();
1380
+ for (const pair of pingPongTexturePairs.values()) {
1381
+ pair.textureA.destroy();
1382
+ pair.textureB.destroy();
1383
+ }
1384
+ pingPongTexturePairs.clear();
1385
+ computePipelineCache.clear();
1386
+ destroyRenderTexture(sourceSlotTarget);
1387
+ destroyRenderTexture(targetSlotTarget);
1388
+ for (const target of runtimeRenderTargets.values()) target.texture.destroy();
1389
+ runtimeRenderTargets.clear();
1390
+ for (const pass of activePasses) pass.dispose?.();
1391
+ activePasses.length = 0;
1392
+ lifecyclePassesRef = null;
1393
+ for (const binding of textureBindings) {
1394
+ binding.texture?.destroy();
1395
+ binding.fallbackTexture.destroy();
1396
+ }
1397
+ blitBindGroupByView = /* @__PURE__ */ new WeakMap();
1398
+ cachedGraphPlan = null;
1399
+ cachedGraphPasses.length = 0;
1400
+ renderTargetSnapshot = {};
1401
+ renderTargetKeys = [];
1402
+ }
1403
+ };
1404
+ } catch (error) {
1405
+ isDestroyed = true;
1406
+ acceptInitializationCleanups = false;
1407
+ device.removeEventListener("uncapturederror", handleUncapturedError);
1408
+ runInitializationCleanups();
1409
+ throw error;
1410
+ }
1159
1411
  }
1412
+ //#endregion
1413
+ export { createRenderer, findDirtyFloatRanges };
1414
+
1415
+ //# sourceMappingURL=renderer.js.map