@motion-core/motion-gpu 0.4.2 → 0.6.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 (121) hide show
  1. package/README.md +99 -0
  2. package/dist/advanced.js +3 -1
  3. package/dist/core/advanced.js +3 -1
  4. package/dist/core/compute-bindgroup-cache.d.ts +13 -0
  5. package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
  6. package/dist/core/compute-bindgroup-cache.js +45 -0
  7. package/dist/core/compute-bindgroup-cache.js.map +1 -0
  8. package/dist/core/compute-shader.d.ts +135 -0
  9. package/dist/core/compute-shader.d.ts.map +1 -0
  10. package/dist/core/compute-shader.js +238 -0
  11. package/dist/core/compute-shader.js.map +1 -0
  12. package/dist/core/error-diagnostics.d.ts +8 -1
  13. package/dist/core/error-diagnostics.d.ts.map +1 -1
  14. package/dist/core/error-diagnostics.js +7 -3
  15. package/dist/core/error-diagnostics.js.map +1 -1
  16. package/dist/core/error-report.d.ts +1 -1
  17. package/dist/core/error-report.d.ts.map +1 -1
  18. package/dist/core/error-report.js +82 -1
  19. package/dist/core/error-report.js.map +1 -1
  20. package/dist/core/frame-registry.d.ts.map +1 -1
  21. package/dist/core/frame-registry.js +1 -1
  22. package/dist/core/frame-registry.js.map +1 -1
  23. package/dist/core/index.d.ts +5 -2
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +3 -1
  26. package/dist/core/material-preprocess.d.ts.map +1 -1
  27. package/dist/core/material-preprocess.js +5 -3
  28. package/dist/core/material-preprocess.js.map +1 -1
  29. package/dist/core/material.d.ts +22 -6
  30. package/dist/core/material.d.ts.map +1 -1
  31. package/dist/core/material.js +32 -4
  32. package/dist/core/material.js.map +1 -1
  33. package/dist/core/render-graph.d.ts +7 -3
  34. package/dist/core/render-graph.d.ts.map +1 -1
  35. package/dist/core/render-graph.js +22 -6
  36. package/dist/core/render-graph.js.map +1 -1
  37. package/dist/core/renderer.d.ts.map +1 -1
  38. package/dist/core/renderer.js +489 -29
  39. package/dist/core/renderer.js.map +1 -1
  40. package/dist/core/runtime-loop.d.ts +2 -2
  41. package/dist/core/runtime-loop.d.ts.map +1 -1
  42. package/dist/core/runtime-loop.js +74 -14
  43. package/dist/core/runtime-loop.js.map +1 -1
  44. package/dist/core/shader.d.ts +16 -3
  45. package/dist/core/shader.d.ts.map +1 -1
  46. package/dist/core/shader.js +22 -2
  47. package/dist/core/shader.js.map +1 -1
  48. package/dist/core/storage-buffers.d.ts +37 -0
  49. package/dist/core/storage-buffers.d.ts.map +1 -0
  50. package/dist/core/storage-buffers.js +95 -0
  51. package/dist/core/storage-buffers.js.map +1 -0
  52. package/dist/core/texture-loader.d.ts.map +1 -1
  53. package/dist/core/texture-loader.js +4 -0
  54. package/dist/core/texture-loader.js.map +1 -1
  55. package/dist/core/textures.d.ts +16 -0
  56. package/dist/core/textures.d.ts.map +1 -1
  57. package/dist/core/textures.js +8 -2
  58. package/dist/core/textures.js.map +1 -1
  59. package/dist/core/types.d.ts +146 -4
  60. package/dist/core/types.d.ts.map +1 -1
  61. package/dist/index.js +3 -1
  62. package/dist/passes/ComputePass.d.ts +83 -0
  63. package/dist/passes/ComputePass.d.ts.map +1 -0
  64. package/dist/passes/ComputePass.js +92 -0
  65. package/dist/passes/ComputePass.js.map +1 -0
  66. package/dist/passes/PingPongComputePass.d.ts +104 -0
  67. package/dist/passes/PingPongComputePass.d.ts.map +1 -0
  68. package/dist/passes/PingPongComputePass.js +132 -0
  69. package/dist/passes/PingPongComputePass.js.map +1 -0
  70. package/dist/passes/ShaderPass.d.ts.map +1 -1
  71. package/dist/passes/ShaderPass.js +2 -1
  72. package/dist/passes/ShaderPass.js.map +1 -1
  73. package/dist/passes/index.d.ts +2 -0
  74. package/dist/passes/index.d.ts.map +1 -1
  75. package/dist/passes/index.js +3 -1
  76. package/dist/react/FragCanvas.d.ts +2 -2
  77. package/dist/react/FragCanvas.d.ts.map +1 -1
  78. package/dist/react/FragCanvas.js.map +1 -1
  79. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
  80. package/dist/react/MotionGPUErrorOverlay.js +123 -20
  81. package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
  82. package/dist/react/advanced.js +3 -1
  83. package/dist/react/index.d.ts +5 -2
  84. package/dist/react/index.d.ts.map +1 -1
  85. package/dist/react/index.js +3 -1
  86. package/dist/svelte/FragCanvas.svelte +2 -2
  87. package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
  88. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
  89. package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
  90. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
  91. package/dist/svelte/advanced.js +3 -1
  92. package/dist/svelte/index.d.ts +5 -2
  93. package/dist/svelte/index.d.ts.map +1 -1
  94. package/dist/svelte/index.js +3 -1
  95. package/package.json +1 -1
  96. package/src/lib/core/compute-bindgroup-cache.ts +73 -0
  97. package/src/lib/core/compute-shader.ts +412 -0
  98. package/src/lib/core/error-diagnostics.ts +29 -4
  99. package/src/lib/core/error-report.ts +155 -1
  100. package/src/lib/core/frame-registry.ts +2 -1
  101. package/src/lib/core/index.ts +18 -1
  102. package/src/lib/core/material-preprocess.ts +17 -6
  103. package/src/lib/core/material.ts +103 -21
  104. package/src/lib/core/render-graph.ts +39 -9
  105. package/src/lib/core/renderer.ts +768 -48
  106. package/src/lib/core/runtime-loop.ts +116 -16
  107. package/src/lib/core/shader.ts +58 -4
  108. package/src/lib/core/storage-buffers.ts +142 -0
  109. package/src/lib/core/texture-loader.ts +6 -0
  110. package/src/lib/core/textures.ts +29 -2
  111. package/src/lib/core/types.ts +165 -4
  112. package/src/lib/passes/ComputePass.ts +136 -0
  113. package/src/lib/passes/PingPongComputePass.ts +180 -0
  114. package/src/lib/passes/ShaderPass.ts +2 -1
  115. package/src/lib/passes/index.ts +6 -0
  116. package/src/lib/react/FragCanvas.tsx +3 -3
  117. package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
  118. package/src/lib/react/index.ts +18 -1
  119. package/src/lib/svelte/FragCanvas.svelte +2 -2
  120. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
  121. package/src/lib/svelte/index.ts +18 -1
@@ -2,9 +2,11 @@ import { defineMaterial } from "../core/material.js";
2
2
  import { BlitPass } from "../passes/BlitPass.js";
3
3
  import { CopyPass } from "../passes/CopyPass.js";
4
4
  import { ShaderPass } from "../passes/ShaderPass.js";
5
+ import { ComputePass } from "../passes/ComputePass.js";
6
+ import { PingPongComputePass } from "../passes/PingPongComputePass.js";
5
7
  import "../passes/index.js";
6
8
  import { useMotionGPU } from "./motiongpu-context.js";
7
9
  import { useFrame } from "./frame-context.js";
8
10
  import { useTexture } from "./use-texture.js";
9
11
  import FragCanvas from "./FragCanvas.svelte";
10
- export { BlitPass, CopyPass, FragCanvas, ShaderPass, defineMaterial, useFrame, useMotionGPU, useTexture };
12
+ export { BlitPass, ComputePass, CopyPass, FragCanvas, PingPongComputePass, ShaderPass, defineMaterial, useFrame, useMotionGPU, useTexture };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-core/motion-gpu",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte and React adapter entrypoints.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -0,0 +1,73 @@
1
+ export interface ComputeStorageBindGroupCacheRequest {
2
+ topologyKey: string;
3
+ layoutEntries: GPUBindGroupLayoutEntry[];
4
+ bindGroupEntries: GPUBindGroupEntry[];
5
+ resourceRefs: readonly unknown[];
6
+ }
7
+
8
+ export interface ComputeStorageBindGroupCache {
9
+ getOrCreate: (request: ComputeStorageBindGroupCacheRequest) => GPUBindGroup | null;
10
+ reset: () => void;
11
+ }
12
+
13
+ function equalResourceRefs(previous: readonly unknown[], next: readonly unknown[]): boolean {
14
+ if (previous.length !== next.length) {
15
+ return false;
16
+ }
17
+
18
+ for (let index = 0; index < previous.length; index += 1) {
19
+ if (!Object.is(previous[index], next[index])) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ export function createComputeStorageBindGroupCache(
28
+ device: GPUDevice
29
+ ): ComputeStorageBindGroupCache {
30
+ let cachedTopologyKey: string | null = null;
31
+ let cachedLayout: GPUBindGroupLayout | null = null;
32
+ let cachedBindGroup: GPUBindGroup | null = null;
33
+ let cachedResourceRefs: readonly unknown[] = [];
34
+
35
+ const reset = (): void => {
36
+ cachedTopologyKey = null;
37
+ cachedLayout = null;
38
+ cachedBindGroup = null;
39
+ cachedResourceRefs = [];
40
+ };
41
+
42
+ return {
43
+ getOrCreate(request) {
44
+ if (request.layoutEntries.length === 0) {
45
+ reset();
46
+ return null;
47
+ }
48
+
49
+ if (cachedTopologyKey !== request.topologyKey) {
50
+ cachedTopologyKey = request.topologyKey;
51
+ cachedLayout = device.createBindGroupLayout({ entries: request.layoutEntries });
52
+ cachedBindGroup = null;
53
+ cachedResourceRefs = [];
54
+ }
55
+
56
+ if (!cachedLayout) {
57
+ throw new Error('Compute storage bind group cache is missing a layout.');
58
+ }
59
+
60
+ if (cachedBindGroup && equalResourceRefs(cachedResourceRefs, request.resourceRefs)) {
61
+ return cachedBindGroup;
62
+ }
63
+
64
+ cachedBindGroup = device.createBindGroup({
65
+ layout: cachedLayout,
66
+ entries: request.bindGroupEntries
67
+ });
68
+ cachedResourceRefs = [...request.resourceRefs];
69
+ return cachedBindGroup;
70
+ },
71
+ reset
72
+ };
73
+ }
@@ -0,0 +1,412 @@
1
+ import type { StorageBufferAccess, StorageBufferType, UniformLayout } from './types.js';
2
+
3
+ /**
4
+ * Regex contract for compute entrypoint.
5
+ * Matches: @compute @workgroup_size(...) fn compute(
6
+ * with @builtin(global_invocation_id) parameter.
7
+ */
8
+ export const COMPUTE_ENTRY_CONTRACT = /@compute\s+@workgroup_size\s*\([^)]+\)\s*fn\s+compute\s*\(/;
9
+
10
+ /**
11
+ * Regex to extract @workgroup_size values.
12
+ */
13
+ const WORKGROUP_SIZE_PATTERN =
14
+ /@workgroup_size\s*\(\s*(\d+)(?:\s*,\s*(\d+))?(?:\s*,\s*(\d+))?\s*\)/;
15
+
16
+ /**
17
+ * Regex to verify @builtin(global_invocation_id) parameter.
18
+ */
19
+ const GLOBAL_INVOCATION_ID_PATTERN = /@builtin\s*\(\s*global_invocation_id\s*\)/;
20
+ const WORKGROUP_DIMENSION_MIN = 1;
21
+ const WORKGROUP_DIMENSION_MAX = 65535;
22
+
23
+ function extractComputeParamList(compute: string): string | null {
24
+ const computeFnIndex = compute.indexOf('fn compute');
25
+ if (computeFnIndex === -1) {
26
+ return null;
27
+ }
28
+
29
+ const openParenIndex = compute.indexOf('(', computeFnIndex);
30
+ if (openParenIndex === -1) {
31
+ return null;
32
+ }
33
+
34
+ let depth = 0;
35
+ for (let index = openParenIndex; index < compute.length; index += 1) {
36
+ const char = compute[index];
37
+ if (char === '(') {
38
+ depth += 1;
39
+ continue;
40
+ }
41
+
42
+ if (char === ')') {
43
+ depth -= 1;
44
+ if (depth === 0) {
45
+ return compute.slice(openParenIndex + 1, index);
46
+ }
47
+ }
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ function assertWorkgroupDimension(value: number): void {
54
+ if (
55
+ !Number.isFinite(value) ||
56
+ !Number.isInteger(value) ||
57
+ value < WORKGROUP_DIMENSION_MIN ||
58
+ value > WORKGROUP_DIMENSION_MAX
59
+ ) {
60
+ throw new Error(
61
+ `@workgroup_size dimensions must be integers in range ${WORKGROUP_DIMENSION_MIN}-${WORKGROUP_DIMENSION_MAX}, got ${value}.`
62
+ );
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Default uniform field used when no custom uniforms are provided in compute.
68
+ */
69
+ const DEFAULT_UNIFORM_FIELD = 'motiongpu_unused: vec4f,';
70
+
71
+ /**
72
+ * Validates compute shader user code matches the compute contract.
73
+ *
74
+ * @param compute - User compute shader WGSL source.
75
+ * @throws {Error} When shader does not match the compute contract.
76
+ */
77
+ export function assertComputeContract(compute: string): void {
78
+ if (!COMPUTE_ENTRY_CONTRACT.test(compute)) {
79
+ throw new Error(
80
+ 'Compute shader must declare `@compute @workgroup_size(...) fn compute(...)`. ' +
81
+ 'Ensure the function is named `compute` and includes @compute and @workgroup_size annotations.'
82
+ );
83
+ }
84
+
85
+ const params = extractComputeParamList(compute);
86
+ if (!params || !GLOBAL_INVOCATION_ID_PATTERN.test(params)) {
87
+ throw new Error('Compute shader must include a `@builtin(global_invocation_id)` parameter.');
88
+ }
89
+
90
+ extractWorkgroupSize(compute);
91
+ }
92
+
93
+ /**
94
+ * Extracts @workgroup_size values from WGSL compute shader.
95
+ *
96
+ * @param compute - Validated compute shader source.
97
+ * @returns Tuple [x, y, z] with defaults of 1 for omitted dimensions.
98
+ */
99
+ export function extractWorkgroupSize(compute: string): [number, number, number] {
100
+ const match = compute.match(WORKGROUP_SIZE_PATTERN);
101
+ if (!match) {
102
+ throw new Error('Could not extract @workgroup_size from compute shader source.');
103
+ }
104
+
105
+ const x = Number.parseInt(match[1] ?? '1', 10);
106
+ const y = Number.parseInt(match[2] ?? '1', 10);
107
+ const z = Number.parseInt(match[3] ?? '1', 10);
108
+ assertWorkgroupDimension(x);
109
+ assertWorkgroupDimension(y);
110
+ assertWorkgroupDimension(z);
111
+
112
+ return [x, y, z];
113
+ }
114
+
115
+ /**
116
+ * Maps StorageBufferAccess to WGSL var qualifier.
117
+ */
118
+ function toWgslAccessMode(access: StorageBufferAccess): string {
119
+ switch (access) {
120
+ case 'read':
121
+ return 'read';
122
+ case 'read-write':
123
+ return 'read_write';
124
+ default:
125
+ throw new Error(`Unsupported storage buffer access mode "${String(access)}".`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Builds WGSL struct fields for uniforms used in compute shader preamble.
131
+ */
132
+ function buildUniformStructForCompute(layout: UniformLayout): string {
133
+ if (layout.entries.length === 0) {
134
+ return DEFAULT_UNIFORM_FIELD;
135
+ }
136
+
137
+ return layout.entries.map((entry) => `${entry.name}: ${entry.type},`).join('\n\t');
138
+ }
139
+
140
+ /**
141
+ * Builds storage buffer binding declarations for compute shader.
142
+ *
143
+ * @param storageBufferKeys - Sorted buffer keys.
144
+ * @param definitions - Type/access definitions per key.
145
+ * @param groupIndex - Bind group index for storage buffers.
146
+ * @returns WGSL binding declaration string.
147
+ */
148
+ export function buildComputeStorageBufferBindings(
149
+ storageBufferKeys: string[],
150
+ definitions: Record<string, { type: StorageBufferType; access: StorageBufferAccess }>,
151
+ groupIndex: number
152
+ ): string {
153
+ if (storageBufferKeys.length === 0) {
154
+ return '';
155
+ }
156
+
157
+ const declarations: string[] = [];
158
+
159
+ for (let index = 0; index < storageBufferKeys.length; index += 1) {
160
+ const key = storageBufferKeys[index];
161
+ if (key === undefined) {
162
+ continue;
163
+ }
164
+
165
+ const definition = definitions[key];
166
+ if (!definition) {
167
+ continue;
168
+ }
169
+
170
+ const accessMode = toWgslAccessMode(definition.access);
171
+ declarations.push(
172
+ `@group(${groupIndex}) @binding(${index}) var<storage, ${accessMode}> ${key}: ${definition.type};`
173
+ );
174
+ }
175
+
176
+ return declarations.join('\n');
177
+ }
178
+
179
+ /**
180
+ * Builds storage texture binding declarations for compute shader.
181
+ *
182
+ * @param storageTextureKeys - Sorted storage texture keys.
183
+ * @param definitions - Format definitions per key.
184
+ * @param groupIndex - Bind group index for storage textures.
185
+ * @returns WGSL binding declaration string.
186
+ */
187
+ export function buildComputeStorageTextureBindings(
188
+ storageTextureKeys: string[],
189
+ definitions: Record<string, { format: GPUTextureFormat }>,
190
+ groupIndex: number
191
+ ): string {
192
+ if (storageTextureKeys.length === 0) {
193
+ return '';
194
+ }
195
+
196
+ const declarations: string[] = [];
197
+
198
+ for (let index = 0; index < storageTextureKeys.length; index += 1) {
199
+ const key = storageTextureKeys[index];
200
+ if (key === undefined) {
201
+ continue;
202
+ }
203
+
204
+ const definition = definitions[key];
205
+ if (!definition) {
206
+ continue;
207
+ }
208
+
209
+ declarations.push(
210
+ `@group(${groupIndex}) @binding(${index}) var ${key}: texture_storage_2d<${definition.format}, write>;`
211
+ );
212
+ }
213
+
214
+ return declarations.join('\n');
215
+ }
216
+
217
+ /**
218
+ * Maps storage texture format to sampled texture scalar type for `texture_2d<T>`.
219
+ */
220
+ export function storageTextureSampleScalarType(format: GPUTextureFormat): 'f32' | 'u32' | 'i32' {
221
+ const normalized = String(format).toLowerCase();
222
+ if (normalized.endsWith('uint')) {
223
+ return 'u32';
224
+ }
225
+ if (normalized.endsWith('sint')) {
226
+ return 'i32';
227
+ }
228
+ return 'f32';
229
+ }
230
+
231
+ /**
232
+ * Assembles compute shader WGSL for ping-pong workflows.
233
+ *
234
+ * Exposes two generated bindings under group(2):
235
+ * - `${target}A`: sampled read texture (`texture_2d<T>`)
236
+ * - `${target}B`: storage write texture (`texture_storage_2d<format, write>`)
237
+ */
238
+ export function buildPingPongComputeShaderSource(options: {
239
+ compute: string;
240
+ uniformLayout: UniformLayout;
241
+ storageBufferKeys: string[];
242
+ storageBufferDefinitions: Record<
243
+ string,
244
+ { type: StorageBufferType; access: StorageBufferAccess }
245
+ >;
246
+ target: string;
247
+ targetFormat: GPUTextureFormat;
248
+ }): string {
249
+ const uniformFields = buildUniformStructForCompute(options.uniformLayout);
250
+ const storageBufferBindings = buildComputeStorageBufferBindings(
251
+ options.storageBufferKeys,
252
+ options.storageBufferDefinitions,
253
+ 1
254
+ );
255
+ const sampledType = storageTextureSampleScalarType(options.targetFormat);
256
+ const pingPongTextureBindings = [
257
+ `@group(2) @binding(0) var ${options.target}A: texture_2d<${sampledType}>;`,
258
+ `@group(2) @binding(1) var ${options.target}B: texture_storage_2d<${options.targetFormat}, write>;`
259
+ ].join('\n');
260
+
261
+ return `struct MotionGPUFrame {
262
+ time: f32,
263
+ delta: f32,
264
+ resolution: vec2f,
265
+ };
266
+
267
+ struct MotionGPUUniforms {
268
+ ${uniformFields}
269
+ };
270
+
271
+ @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame;
272
+ @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms;
273
+ ${storageBufferBindings ? '\n' + storageBufferBindings : ''}
274
+ ${pingPongTextureBindings ? '\n' + pingPongTextureBindings : ''}
275
+
276
+ ${options.compute}
277
+ `;
278
+ }
279
+
280
+ /**
281
+ * Source location for generated compute shader lines.
282
+ */
283
+ export interface ComputeShaderSourceLocation {
284
+ kind: 'compute';
285
+ line: number;
286
+ }
287
+
288
+ /**
289
+ * 1-based line map from generated compute WGSL to user compute source.
290
+ */
291
+ export type ComputeShaderLineMap = Array<ComputeShaderSourceLocation | null>;
292
+
293
+ /**
294
+ * Result of compute shader source generation with line mapping metadata.
295
+ */
296
+ export interface BuiltComputeShaderSource {
297
+ code: string;
298
+ lineMap: ComputeShaderLineMap;
299
+ }
300
+
301
+ /**
302
+ * Assembles full compute shader WGSL with preamble.
303
+ *
304
+ * @param options - Compute shader build options.
305
+ * @returns Complete WGSL source for compute stage.
306
+ */
307
+ export function buildComputeShaderSource(options: {
308
+ compute: string;
309
+ uniformLayout: UniformLayout;
310
+ storageBufferKeys: string[];
311
+ storageBufferDefinitions: Record<
312
+ string,
313
+ { type: StorageBufferType; access: StorageBufferAccess }
314
+ >;
315
+ storageTextureKeys: string[];
316
+ storageTextureDefinitions: Record<string, { format: GPUTextureFormat }>;
317
+ }): string {
318
+ const uniformFields = buildUniformStructForCompute(options.uniformLayout);
319
+ const storageBufferBindings = buildComputeStorageBufferBindings(
320
+ options.storageBufferKeys,
321
+ options.storageBufferDefinitions,
322
+ 1
323
+ );
324
+ const storageTextureBindings = buildComputeStorageTextureBindings(
325
+ options.storageTextureKeys,
326
+ options.storageTextureDefinitions,
327
+ 2
328
+ );
329
+
330
+ return `struct MotionGPUFrame {
331
+ time: f32,
332
+ delta: f32,
333
+ resolution: vec2f,
334
+ };
335
+
336
+ struct MotionGPUUniforms {
337
+ ${uniformFields}
338
+ };
339
+
340
+ @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame;
341
+ @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms;
342
+ ${storageBufferBindings ? '\n' + storageBufferBindings : ''}
343
+ ${storageTextureBindings ? '\n' + storageTextureBindings : ''}
344
+
345
+ ${options.compute}
346
+ `;
347
+ }
348
+
349
+ function buildComputeLineMap(
350
+ generatedCode: string,
351
+ userComputeSource: string
352
+ ): ComputeShaderLineMap {
353
+ const lineCount = generatedCode.split('\n').length;
354
+ const lineMap: ComputeShaderLineMap = new Array(lineCount + 1).fill(null);
355
+ const computeStartIndex = generatedCode.indexOf(userComputeSource);
356
+ if (computeStartIndex === -1) {
357
+ return lineMap;
358
+ }
359
+
360
+ const computeStartLine = generatedCode.slice(0, computeStartIndex).split('\n').length;
361
+ const computeLineCount = userComputeSource.split('\n').length;
362
+ for (let line = 0; line < computeLineCount; line += 1) {
363
+ lineMap[computeStartLine + line] = {
364
+ kind: 'compute',
365
+ line: line + 1
366
+ };
367
+ }
368
+
369
+ return lineMap;
370
+ }
371
+
372
+ /**
373
+ * Assembles full compute shader WGSL with source line mapping metadata.
374
+ */
375
+ export function buildComputeShaderSourceWithMap(options: {
376
+ compute: string;
377
+ uniformLayout: UniformLayout;
378
+ storageBufferKeys: string[];
379
+ storageBufferDefinitions: Record<
380
+ string,
381
+ { type: StorageBufferType; access: StorageBufferAccess }
382
+ >;
383
+ storageTextureKeys: string[];
384
+ storageTextureDefinitions: Record<string, { format: GPUTextureFormat }>;
385
+ }): BuiltComputeShaderSource {
386
+ const code = buildComputeShaderSource(options);
387
+ return {
388
+ code,
389
+ lineMap: buildComputeLineMap(code, options.compute)
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Assembles ping-pong compute shader WGSL with source line mapping metadata.
395
+ */
396
+ export function buildPingPongComputeShaderSourceWithMap(options: {
397
+ compute: string;
398
+ uniformLayout: UniformLayout;
399
+ storageBufferKeys: string[];
400
+ storageBufferDefinitions: Record<
401
+ string,
402
+ { type: StorageBufferType; access: StorageBufferAccess }
403
+ >;
404
+ target: string;
405
+ targetFormat: GPUTextureFormat;
406
+ }): BuiltComputeShaderSource {
407
+ const code = buildPingPongComputeShaderSource(options);
408
+ return {
409
+ code,
410
+ lineMap: buildComputeLineMap(code, options.compute)
411
+ };
412
+ }
@@ -11,6 +11,13 @@ export interface MaterialSourceMetadata {
11
11
  functionName?: string;
12
12
  }
13
13
 
14
+ export interface ComputeSourceLocation {
15
+ kind: 'compute';
16
+ line: number;
17
+ }
18
+
19
+ export type ShaderSourceLocation = MaterialSourceLocation | ComputeSourceLocation;
20
+
14
21
  /**
15
22
  * One WGSL compiler diagnostic enriched with source-location metadata.
16
23
  */
@@ -19,7 +26,7 @@ export interface ShaderCompilationDiagnostic {
19
26
  message: string;
20
27
  linePos?: number;
21
28
  lineLength?: number;
22
- sourceLocation: MaterialSourceLocation | null;
29
+ sourceLocation: ShaderSourceLocation | null;
23
30
  }
24
31
 
25
32
  /**
@@ -41,8 +48,10 @@ export interface ShaderCompilationRuntimeContext {
41
48
  */
42
49
  export interface ShaderCompilationDiagnosticsPayload {
43
50
  kind: 'shader-compilation';
51
+ shaderStage?: 'fragment' | 'compute';
44
52
  diagnostics: ShaderCompilationDiagnostic[];
45
53
  fragmentSource: string;
54
+ computeSource?: string;
46
55
  includeSources: Record<string, string>;
47
56
  defineBlockSource?: string;
48
57
  materialSource: MaterialSourceMetadata | null;
@@ -78,7 +87,7 @@ function isMaterialSourceMetadata(value: unknown): value is MaterialSourceMetada
78
87
  return true;
79
88
  }
80
89
 
81
- function isMaterialSourceLocation(value: unknown): value is MaterialSourceLocation | null {
90
+ function isShaderSourceLocation(value: unknown): value is ShaderSourceLocation | null {
82
91
  if (value === null) {
83
92
  return true;
84
93
  }
@@ -89,7 +98,7 @@ function isMaterialSourceLocation(value: unknown): value is MaterialSourceLocati
89
98
 
90
99
  const record = value as Record<string, unknown>;
91
100
  const kind = record.kind;
92
- if (kind !== 'fragment' && kind !== 'include' && kind !== 'define') {
101
+ if (kind !== 'fragment' && kind !== 'include' && kind !== 'define' && kind !== 'compute') {
93
102
  return false;
94
103
  }
95
104
 
@@ -114,7 +123,7 @@ function isShaderCompilationDiagnostic(value: unknown): value is ShaderCompilati
114
123
  if (record.lineLength !== undefined && typeof record.lineLength !== 'number') {
115
124
  return false;
116
125
  }
117
- if (!isMaterialSourceLocation(record.sourceLocation)) {
126
+ if (!isShaderSourceLocation(record.sourceLocation)) {
118
127
  return false;
119
128
  }
120
129
 
@@ -191,6 +200,13 @@ export function getShaderCompilationDiagnostics(
191
200
  if (record.kind !== 'shader-compilation') {
192
201
  return null;
193
202
  }
203
+ if (
204
+ record.shaderStage !== undefined &&
205
+ record.shaderStage !== 'fragment' &&
206
+ record.shaderStage !== 'compute'
207
+ ) {
208
+ return null;
209
+ }
194
210
  if (
195
211
  !Array.isArray(record.diagnostics) ||
196
212
  !record.diagnostics.every(isShaderCompilationDiagnostic)
@@ -200,6 +216,9 @@ export function getShaderCompilationDiagnostics(
200
216
  if (typeof record.fragmentSource !== 'string') {
201
217
  return null;
202
218
  }
219
+ if (record.computeSource !== undefined && typeof record.computeSource !== 'string') {
220
+ return null;
221
+ }
203
222
  if (record.defineBlockSource !== undefined && typeof record.defineBlockSource !== 'string') {
204
223
  return null;
205
224
  }
@@ -222,8 +241,14 @@ export function getShaderCompilationDiagnostics(
222
241
 
223
242
  return {
224
243
  kind: 'shader-compilation',
244
+ ...(record.shaderStage !== undefined
245
+ ? { shaderStage: record.shaderStage as 'fragment' | 'compute' }
246
+ : {}),
225
247
  diagnostics: record.diagnostics as ShaderCompilationDiagnostic[],
226
248
  fragmentSource: record.fragmentSource,
249
+ ...(record.computeSource !== undefined
250
+ ? { computeSource: record.computeSource as string }
251
+ : {}),
227
252
  includeSources: includeSources as Record<string, string>,
228
253
  ...(record.defineBlockSource !== undefined
229
254
  ? { defineBlockSource: record.defineBlockSource as string }