@motion-core/motion-gpu 0.4.2 → 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 (111) 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-shader.d.ts +87 -0
  5. package/dist/core/compute-shader.d.ts.map +1 -0
  6. package/dist/core/compute-shader.js +205 -0
  7. package/dist/core/compute-shader.js.map +1 -0
  8. package/dist/core/error-report.d.ts +1 -1
  9. package/dist/core/error-report.d.ts.map +1 -1
  10. package/dist/core/error-report.js +63 -0
  11. package/dist/core/error-report.js.map +1 -1
  12. package/dist/core/frame-registry.d.ts.map +1 -1
  13. package/dist/core/frame-registry.js +1 -1
  14. package/dist/core/frame-registry.js.map +1 -1
  15. package/dist/core/index.d.ts +5 -2
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +3 -1
  18. package/dist/core/material-preprocess.d.ts.map +1 -1
  19. package/dist/core/material-preprocess.js +5 -3
  20. package/dist/core/material-preprocess.js.map +1 -1
  21. package/dist/core/material.d.ts +22 -6
  22. package/dist/core/material.d.ts.map +1 -1
  23. package/dist/core/material.js +30 -3
  24. package/dist/core/material.js.map +1 -1
  25. package/dist/core/render-graph.d.ts +7 -3
  26. package/dist/core/render-graph.d.ts.map +1 -1
  27. package/dist/core/render-graph.js +22 -6
  28. package/dist/core/render-graph.js.map +1 -1
  29. package/dist/core/renderer.d.ts.map +1 -1
  30. package/dist/core/renderer.js +418 -23
  31. package/dist/core/renderer.js.map +1 -1
  32. package/dist/core/runtime-loop.d.ts +2 -2
  33. package/dist/core/runtime-loop.d.ts.map +1 -1
  34. package/dist/core/runtime-loop.js +49 -1
  35. package/dist/core/runtime-loop.js.map +1 -1
  36. package/dist/core/shader.d.ts +9 -1
  37. package/dist/core/shader.d.ts.map +1 -1
  38. package/dist/core/shader.js +21 -2
  39. package/dist/core/shader.js.map +1 -1
  40. package/dist/core/storage-buffers.d.ts +37 -0
  41. package/dist/core/storage-buffers.d.ts.map +1 -0
  42. package/dist/core/storage-buffers.js +95 -0
  43. package/dist/core/storage-buffers.js.map +1 -0
  44. package/dist/core/texture-loader.d.ts.map +1 -1
  45. package/dist/core/texture-loader.js +4 -0
  46. package/dist/core/texture-loader.js.map +1 -1
  47. package/dist/core/textures.d.ts +12 -0
  48. package/dist/core/textures.d.ts.map +1 -1
  49. package/dist/core/textures.js +7 -2
  50. package/dist/core/textures.js.map +1 -1
  51. package/dist/core/types.d.ts +146 -4
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/index.js +3 -1
  54. package/dist/passes/ComputePass.d.ts +83 -0
  55. package/dist/passes/ComputePass.d.ts.map +1 -0
  56. package/dist/passes/ComputePass.js +92 -0
  57. package/dist/passes/ComputePass.js.map +1 -0
  58. package/dist/passes/PingPongComputePass.d.ts +104 -0
  59. package/dist/passes/PingPongComputePass.d.ts.map +1 -0
  60. package/dist/passes/PingPongComputePass.js +132 -0
  61. package/dist/passes/PingPongComputePass.js.map +1 -0
  62. package/dist/passes/ShaderPass.d.ts.map +1 -1
  63. package/dist/passes/ShaderPass.js +2 -1
  64. package/dist/passes/ShaderPass.js.map +1 -1
  65. package/dist/passes/index.d.ts +2 -0
  66. package/dist/passes/index.d.ts.map +1 -1
  67. package/dist/passes/index.js +3 -1
  68. package/dist/react/FragCanvas.d.ts +2 -2
  69. package/dist/react/FragCanvas.d.ts.map +1 -1
  70. package/dist/react/FragCanvas.js.map +1 -1
  71. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
  72. package/dist/react/MotionGPUErrorOverlay.js +123 -20
  73. package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
  74. package/dist/react/advanced.js +3 -1
  75. package/dist/react/index.d.ts +5 -2
  76. package/dist/react/index.d.ts.map +1 -1
  77. package/dist/react/index.js +3 -1
  78. package/dist/svelte/FragCanvas.svelte +2 -2
  79. package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
  80. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
  81. package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
  82. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
  83. package/dist/svelte/advanced.js +3 -1
  84. package/dist/svelte/index.d.ts +5 -2
  85. package/dist/svelte/index.d.ts.map +1 -1
  86. package/dist/svelte/index.js +3 -1
  87. package/package.json +1 -1
  88. package/src/lib/core/compute-shader.ts +326 -0
  89. package/src/lib/core/error-report.ts +129 -0
  90. package/src/lib/core/frame-registry.ts +2 -1
  91. package/src/lib/core/index.ts +18 -1
  92. package/src/lib/core/material-preprocess.ts +17 -6
  93. package/src/lib/core/material.ts +101 -20
  94. package/src/lib/core/render-graph.ts +39 -9
  95. package/src/lib/core/renderer.ts +655 -41
  96. package/src/lib/core/runtime-loop.ts +82 -3
  97. package/src/lib/core/shader.ts +45 -2
  98. package/src/lib/core/storage-buffers.ts +142 -0
  99. package/src/lib/core/texture-loader.ts +6 -0
  100. package/src/lib/core/textures.ts +24 -2
  101. package/src/lib/core/types.ts +165 -4
  102. package/src/lib/passes/ComputePass.ts +136 -0
  103. package/src/lib/passes/PingPongComputePass.ts +180 -0
  104. package/src/lib/passes/ShaderPass.ts +2 -1
  105. package/src/lib/passes/index.ts +6 -0
  106. package/src/lib/react/FragCanvas.tsx +3 -3
  107. package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
  108. package/src/lib/react/index.ts +18 -1
  109. package/src/lib/svelte/FragCanvas.svelte +2 -2
  110. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
  111. package/src/lib/svelte/index.ts +18 -1
@@ -17,13 +17,22 @@ export type MotionGPUErrorCode =
17
17
  | 'WEBGPU_ADAPTER_UNAVAILABLE'
18
18
  | 'WEBGPU_CONTEXT_UNAVAILABLE'
19
19
  | 'WGSL_COMPILATION_FAILED'
20
+ | 'MATERIAL_PREPROCESS_FAILED'
20
21
  | 'WEBGPU_DEVICE_LOST'
21
22
  | 'WEBGPU_UNCAPTURED_ERROR'
22
23
  | 'BIND_GROUP_MISMATCH'
24
+ | 'RUNTIME_RESOURCE_MISSING'
25
+ | 'UNIFORM_VALUE_INVALID'
26
+ | 'STORAGE_BUFFER_OUT_OF_BOUNDS'
27
+ | 'STORAGE_BUFFER_READ_FAILED'
28
+ | 'RENDER_GRAPH_INVALID'
29
+ | 'PINGPONG_CONFIGURATION_INVALID'
23
30
  | 'TEXTURE_USAGE_INVALID'
24
31
  | 'TEXTURE_REQUEST_FAILED'
25
32
  | 'TEXTURE_DECODE_UNAVAILABLE'
26
33
  | 'TEXTURE_REQUEST_ABORTED'
34
+ | 'COMPUTE_COMPILATION_FAILED'
35
+ | 'COMPUTE_CONTRACT_INVALID'
27
36
  | 'MOTIONGPU_RUNTIME_ERROR';
28
37
 
29
38
  /**
@@ -274,6 +283,50 @@ function classifyErrorMessage(
274
283
  };
275
284
  }
276
285
 
286
+ if (
287
+ message.includes('Invalid include directive in fragment shader.') ||
288
+ message.includes('Unknown include "') ||
289
+ message.includes('Circular include detected for "') ||
290
+ message.includes('Invalid define value for "') ||
291
+ message.includes('Invalid include "')
292
+ ) {
293
+ return {
294
+ code: 'MATERIAL_PREPROCESS_FAILED',
295
+ severity: 'error',
296
+ recoverable: true,
297
+ title: 'Material preprocess failed',
298
+ hint: 'Validate #include keys, define values and include expansion order before retrying.'
299
+ };
300
+ }
301
+
302
+ if (message.includes('Compute shader compilation failed')) {
303
+ return {
304
+ code: 'COMPUTE_COMPILATION_FAILED',
305
+ severity: 'error',
306
+ recoverable: true,
307
+ title: 'Compute shader compilation failed',
308
+ hint: 'Check WGSL compute shader sources below and verify storage bindings.'
309
+ };
310
+ }
311
+
312
+ if (
313
+ message.includes(
314
+ 'Compute shader must declare `@compute @workgroup_size(...) fn compute(...)`.'
315
+ ) ||
316
+ message.includes('Compute shader must include a `@builtin(global_invocation_id)` parameter.') ||
317
+ message.includes('Could not extract @workgroup_size from compute shader source.') ||
318
+ message.includes('@workgroup_size dimensions must be integers in range') ||
319
+ message.includes('Unsupported storage buffer access mode "')
320
+ ) {
321
+ return {
322
+ code: 'COMPUTE_CONTRACT_INVALID',
323
+ severity: 'error',
324
+ recoverable: true,
325
+ title: 'Compute contract is invalid',
326
+ hint: 'Ensure compute shader contract (@compute, @workgroup_size, global_invocation_id, storage access) is valid.'
327
+ };
328
+ }
329
+
277
330
  if (message.includes('WebGPU device lost') || message.includes('Device Lost')) {
278
331
  return {
279
332
  code: 'WEBGPU_DEVICE_LOST',
@@ -304,6 +357,82 @@ function classifyErrorMessage(
304
357
  };
305
358
  }
306
359
 
360
+ if (message.includes('Storage buffer "') && message.includes('write out of bounds:')) {
361
+ return {
362
+ code: 'STORAGE_BUFFER_OUT_OF_BOUNDS',
363
+ severity: 'error',
364
+ recoverable: true,
365
+ title: 'Storage buffer write out of bounds',
366
+ hint: 'Ensure offset + write byte length does not exceed declared storage buffer size.'
367
+ };
368
+ }
369
+
370
+ if (
371
+ message.includes('Cannot read storage buffer "') ||
372
+ message.includes('Cannot read storage buffer: GPU device unavailable.') ||
373
+ message.includes('not allocated on GPU.')
374
+ ) {
375
+ return {
376
+ code: 'STORAGE_BUFFER_READ_FAILED',
377
+ severity: 'error',
378
+ recoverable: true,
379
+ title: 'Storage buffer read failed',
380
+ hint: 'Readbacks require an initialized renderer, allocated GPU buffer and active device.'
381
+ };
382
+ }
383
+
384
+ if (
385
+ message.includes('Unknown uniform "') ||
386
+ message.includes('Unknown uniform type for "') ||
387
+ message.includes('Unknown texture "') ||
388
+ message.includes('Unknown storage buffer "') ||
389
+ message.includes('Missing definition for storage buffer "') ||
390
+ message.includes('Missing texture definition for "') ||
391
+ (message.includes('Storage buffer "') && message.includes('" not allocated.')) ||
392
+ (message.includes('Storage texture "') && message.includes('" not allocated.'))
393
+ ) {
394
+ return {
395
+ code: 'RUNTIME_RESOURCE_MISSING',
396
+ severity: 'error',
397
+ recoverable: true,
398
+ title: 'Runtime resource binding failed',
399
+ hint: 'Check material declarations and runtime keys for uniforms, textures and storage resources.'
400
+ };
401
+ }
402
+
403
+ if (message.includes('Uniform ') && message.includes(' value must')) {
404
+ return {
405
+ code: 'UNIFORM_VALUE_INVALID',
406
+ severity: 'error',
407
+ recoverable: true,
408
+ title: 'Uniform value is invalid',
409
+ hint: 'Provide finite values with tuple/matrix sizes matching the uniform type.'
410
+ };
411
+ }
412
+
413
+ if (
414
+ message.includes('Render pass #') ||
415
+ message.includes('Render graph references unknown runtime target')
416
+ ) {
417
+ return {
418
+ code: 'RENDER_GRAPH_INVALID',
419
+ severity: 'error',
420
+ recoverable: true,
421
+ title: 'Render graph configuration is invalid',
422
+ hint: 'Verify pass inputs/outputs, declared render targets and execution order.'
423
+ };
424
+ }
425
+
426
+ if (message.includes('PingPongComputePass must provide a target texture key.')) {
427
+ return {
428
+ code: 'PINGPONG_CONFIGURATION_INVALID',
429
+ severity: 'error',
430
+ recoverable: true,
431
+ title: 'Ping-pong compute pass is misconfigured',
432
+ hint: 'Configure a valid target texture key for PingPongComputePass.'
433
+ };
434
+ }
435
+
307
436
  if (message.includes('Destination texture needs to have CopyDst')) {
308
437
  return {
309
438
  code: 'TEXTURE_USAGE_INVALID',
@@ -1003,7 +1003,8 @@ export function createFrameRegistry(options?: {
1003
1003
  stop,
1004
1004
  started: internalTask.startedStore,
1005
1005
  unsubscribe: () => {
1006
- if (stage.tasks.delete(key)) {
1006
+ const current = stage.tasks.get(key);
1007
+ if (current === internalTask && stage.tasks.delete(key)) {
1007
1008
  markScheduleDirty();
1008
1009
  }
1009
1010
  }
@@ -9,7 +9,13 @@ export { createCurrentWritable } from './current-value.js';
9
9
  export { createFrameRegistry } from './frame-registry.js';
10
10
  export { createMotionGPURuntimeLoop } from './runtime-loop.js';
11
11
  export { loadTexturesFromUrls } from './texture-loader.js';
12
- export { BlitPass, CopyPass, ShaderPass } from '../passes/index.js';
12
+ export {
13
+ BlitPass,
14
+ CopyPass,
15
+ ShaderPass,
16
+ ComputePass,
17
+ PingPongComputePass
18
+ } from '../passes/index.js';
13
19
  export type { CurrentReadable, CurrentWritable, Subscribable } from './current-value.js';
14
20
  export type {
15
21
  MotionGPUErrorCode,
@@ -51,6 +57,8 @@ export type {
51
57
  FrameInvalidationToken,
52
58
  FrameState,
53
59
  OutputColorSpace,
60
+ AnyPass,
61
+ ComputePassLike,
54
62
  RenderPass,
55
63
  RenderPassContext,
56
64
  RenderPassFlags,
@@ -75,3 +83,12 @@ export type {
75
83
  UniformType,
76
84
  UniformValue
77
85
  } from './types.js';
86
+ export type {
87
+ StorageBufferAccess,
88
+ StorageBufferDefinition,
89
+ StorageBufferDefinitionMap,
90
+ StorageBufferType,
91
+ ComputePassContext
92
+ } from './types.js';
93
+ export type { ComputePassOptions, ComputeDispatchContext } from '../passes/ComputePass.js';
94
+ export type { PingPongComputePassOptions } from '../passes/PingPongComputePass.js';
@@ -175,7 +175,8 @@ function expandChunk(
175
175
  kind: 'fragment' | 'include',
176
176
  includeName: string | undefined,
177
177
  includes: Record<string, string>,
178
- stack: string[]
178
+ stack: string[],
179
+ expandedIncludes: Set<string>
179
180
  ): { lines: string[]; mapEntries: MaterialSourceLocation[] } {
180
181
  const sourceLines = source.split('\n');
181
182
  const lines: string[] = [];
@@ -215,10 +216,19 @@ function expandChunk(
215
216
  );
216
217
  }
217
218
 
218
- const nested = expandChunk(includeSource, 'include', includeKey, includes, [
219
- ...stack,
220
- includeKey
221
- ]);
219
+ if (expandedIncludes.has(includeKey)) {
220
+ continue;
221
+ }
222
+ expandedIncludes.add(includeKey);
223
+
224
+ const nested = expandChunk(
225
+ includeSource,
226
+ 'include',
227
+ includeKey,
228
+ includes,
229
+ [...stack, includeKey],
230
+ expandedIncludes
231
+ );
222
232
  lines.push(...nested.lines);
223
233
  mapEntries.push(...nested.mapEntries);
224
234
  }
@@ -245,7 +255,8 @@ export function preprocessMaterialFragment<
245
255
  'fragment',
246
256
  undefined,
247
257
  normalizedIncludes,
248
- []
258
+ [],
259
+ new Set()
249
260
  );
250
261
  const defineEntries = (
251
262
  Object.entries(normalizedDefines) as Array<[TDefineKey, MaterialDefineValue]>
@@ -14,7 +14,10 @@ import {
14
14
  type MaterialLineMap,
15
15
  type PreprocessedMaterialFragment
16
16
  } from './material-preprocess.js';
17
+ import { assertStorageBufferDefinition, assertStorageTextureFormat } from './storage-buffers.js';
17
18
  import type {
19
+ StorageBufferDefinition,
20
+ StorageBufferDefinitionMap,
18
21
  TextureData,
19
22
  TextureDefinition,
20
23
  TextureDefinitionMap,
@@ -71,7 +74,8 @@ export interface FragMaterialInput<
71
74
  TUniformKey extends string = string,
72
75
  TTextureKey extends string = string,
73
76
  TDefineKey extends string = string,
74
- TIncludeKey extends string = string
77
+ TIncludeKey extends string = string,
78
+ TStorageBufferKey extends string = string
75
79
  > {
76
80
  /**
77
81
  * User WGSL source containing `frag(uv: vec2f) -> vec4f`.
@@ -93,6 +97,10 @@ export interface FragMaterialInput<
93
97
  * Optional WGSL include chunks used by `#include <name>` directives.
94
98
  */
95
99
  includes?: MaterialIncludes<TIncludeKey>;
100
+ /**
101
+ * Optional storage buffer definitions for compute shaders.
102
+ */
103
+ storageBuffers?: StorageBufferDefinitionMap<TStorageBufferKey>;
96
104
  }
97
105
 
98
106
  /**
@@ -102,7 +110,8 @@ export interface FragMaterial<
102
110
  TUniformKey extends string = string,
103
111
  TTextureKey extends string = string,
104
112
  TDefineKey extends string = string,
105
- TIncludeKey extends string = string
113
+ TIncludeKey extends string = string,
114
+ TStorageBufferKey extends string = string
106
115
  > {
107
116
  /**
108
117
  * User WGSL source containing `frag(uv: vec2f) -> vec4f`.
@@ -124,6 +133,10 @@ export interface FragMaterial<
124
133
  * Optional WGSL include chunks used by `#include <name>` directives.
125
134
  */
126
135
  readonly includes: Readonly<MaterialIncludes<TIncludeKey>>;
136
+ /**
137
+ * Storage buffer definitions for compute shaders. Empty when not provided.
138
+ */
139
+ readonly storageBuffers: Readonly<StorageBufferDefinitionMap<TStorageBufferKey>>;
127
140
  }
128
141
 
129
142
  /**
@@ -132,7 +145,8 @@ export interface FragMaterial<
132
145
  export interface ResolvedMaterial<
133
146
  TUniformKey extends string = string,
134
147
  TTextureKey extends string = string,
135
- TIncludeKey extends string = string
148
+ TIncludeKey extends string = string,
149
+ TStorageBufferKey extends string = string
136
150
  > {
137
151
  /**
138
152
  * Final fragment WGSL after define injection.
@@ -178,6 +192,14 @@ export interface ResolvedMaterial<
178
192
  * Source metadata used for diagnostics.
179
193
  */
180
194
  source: Readonly<MaterialSourceMetadata> | null;
195
+ /**
196
+ * Sorted storage buffer keys. Empty array when no storage buffers declared.
197
+ */
198
+ storageBufferKeys: TStorageBufferKey[];
199
+ /**
200
+ * Sorted storage texture keys (textures with storage: true).
201
+ */
202
+ storageTextureKeys: TTextureKey[];
181
203
  }
182
204
 
183
205
  /**
@@ -190,8 +212,8 @@ const FRAGMENT_FUNCTION_NAME_PATTERN = /\bfn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
190
212
  /**
191
213
  * Cache of resolved material snapshots keyed by immutable material instance.
192
214
  */
193
- type AnyFragMaterial = FragMaterial<string, string, string, string>;
194
- type AnyResolvedMaterial = ResolvedMaterial<string, string, string>;
215
+ type AnyFragMaterial = FragMaterial<string, string, string, string, string>;
216
+ type AnyResolvedMaterial = ResolvedMaterial<string, string, string, string>;
195
217
 
196
218
  const resolvedMaterialCache = new WeakMap<AnyFragMaterial, AnyResolvedMaterial>();
197
219
  const preprocessedFragmentCache = new WeakMap<AnyFragMaterial, PreprocessedMaterialFragment>();
@@ -200,17 +222,18 @@ const materialSourceMetadataCache = new WeakMap<AnyFragMaterial, MaterialSourceM
200
222
  function getCachedResolvedMaterial<
201
223
  TUniformKey extends string,
202
224
  TTextureKey extends string,
203
- TIncludeKey extends string
225
+ TIncludeKey extends string,
226
+ TStorageBufferKey extends string
204
227
  >(
205
- material: FragMaterial<TUniformKey, TTextureKey, string, TIncludeKey>
206
- ): ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey> | null {
228
+ material: FragMaterial<TUniformKey, TTextureKey, string, TIncludeKey, TStorageBufferKey>
229
+ ): ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey, TStorageBufferKey> | null {
207
230
  const cached = resolvedMaterialCache.get(material);
208
231
  if (!cached) {
209
232
  return null;
210
233
  }
211
234
 
212
235
  // Invariant: the cache key is the same material object used to produce this resolved payload.
213
- return cached as ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey>;
236
+ return cached as ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey, TStorageBufferKey>;
214
237
  }
215
238
 
216
239
  const STACK_TRACE_CHROME_PATTERN = /^\s*at\s+(?:(.*?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
@@ -576,10 +599,11 @@ export function defineMaterial<
576
599
  TUniformKey extends string = string,
577
600
  TTextureKey extends string = string,
578
601
  TDefineKey extends string = string,
579
- TIncludeKey extends string = string
602
+ TIncludeKey extends string = string,
603
+ TStorageBufferKey extends string = string
580
604
  >(
581
- input: FragMaterialInput<TUniformKey, TTextureKey, TDefineKey, TIncludeKey>
582
- ): FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey> {
605
+ input: FragMaterialInput<TUniformKey, TTextureKey, TDefineKey, TIncludeKey, TStorageBufferKey>
606
+ ): FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey, TStorageBufferKey> {
583
607
  const fragment = resolveFragment(input.fragment);
584
608
  const uniforms = Object.freeze(resolveUniforms(input.uniforms));
585
609
  const textures = Object.freeze(resolveTextures(input.textures));
@@ -587,18 +611,60 @@ export function defineMaterial<
587
611
  const includes = Object.freeze(resolveIncludes(input.includes));
588
612
  const source = Object.freeze(resolveSourceMetadata(undefined));
589
613
 
614
+ // Validate and freeze storage buffers
615
+ const rawStorageBuffers =
616
+ input.storageBuffers ?? ({} as StorageBufferDefinitionMap<TStorageBufferKey>);
617
+ for (const [name, definition] of Object.entries(rawStorageBuffers) as Array<
618
+ [string, StorageBufferDefinition]
619
+ >) {
620
+ assertStorageBufferDefinition(name, definition);
621
+ }
622
+ const storageBuffers = Object.freeze(
623
+ Object.fromEntries(
624
+ Object.entries(rawStorageBuffers).map(([name, definition]) => {
625
+ const def = definition as StorageBufferDefinition;
626
+ const cloned: StorageBufferDefinition = {
627
+ size: def.size,
628
+ type: def.type,
629
+ ...(def.access !== undefined ? { access: def.access } : {}),
630
+ ...(def.initialData !== undefined
631
+ ? { initialData: def.initialData.slice() as typeof def.initialData }
632
+ : {})
633
+ };
634
+ return [name, Object.freeze(cloned)];
635
+ })
636
+ )
637
+ ) as Readonly<StorageBufferDefinitionMap<TStorageBufferKey>>;
638
+
639
+ // Validate storage textures
640
+ for (const [name, definition] of Object.entries(textures) as Array<[string, TextureDefinition]>) {
641
+ if (definition?.storage) {
642
+ if (!definition.format) {
643
+ throw new Error(`Texture "${name}" with storage:true requires a \`format\` field.`);
644
+ }
645
+ assertStorageTextureFormat(name, definition.format);
646
+ }
647
+ }
648
+
590
649
  const preprocessed = preprocessMaterialFragment({
591
650
  fragment,
592
651
  defines,
593
652
  includes
594
653
  });
595
654
 
596
- const material: FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey> = Object.freeze({
655
+ const material: FragMaterial<
656
+ TUniformKey,
657
+ TTextureKey,
658
+ TDefineKey,
659
+ TIncludeKey,
660
+ TStorageBufferKey
661
+ > = Object.freeze({
597
662
  fragment,
598
663
  uniforms,
599
664
  textures,
600
665
  defines,
601
- includes
666
+ includes,
667
+ storageBuffers
602
668
  });
603
669
 
604
670
  preprocessedFragmentCache.set(material, preprocessed);
@@ -616,10 +682,11 @@ export function resolveMaterial<
616
682
  TUniformKey extends string = string,
617
683
  TTextureKey extends string = string,
618
684
  TDefineKey extends string = string,
619
- TIncludeKey extends string = string
685
+ TIncludeKey extends string = string,
686
+ TStorageBufferKey extends string = string
620
687
  >(
621
- material: FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey>
622
- ): ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey> {
688
+ material: FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey, TStorageBufferKey>
689
+ ): ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey, TStorageBufferKey> {
623
690
  assertDefinedMaterial(material);
624
691
 
625
692
  const cached = getCachedResolvedMaterial(material);
@@ -641,14 +708,26 @@ export function resolveMaterial<
641
708
  const fragmentWgsl = preprocessed.fragment;
642
709
  const textureConfig = buildTextureConfigSignature(textures, textureKeys);
643
710
 
711
+ const storageBufferKeys = Object.keys(
712
+ material.storageBuffers ?? {}
713
+ ).sort() as TStorageBufferKey[];
714
+ const storageTextureKeys = textureKeys.filter(
715
+ (key) => (textures[key] as TextureDefinition)?.storage === true
716
+ );
717
+
644
718
  const signature = JSON.stringify({
645
719
  fragmentWgsl,
646
720
  uniforms: uniformLayout.entries.map((entry) => `${entry.name}:${entry.type}`),
647
721
  textureKeys,
648
- textureConfig
722
+ textureConfig,
723
+ storageBufferKeys: storageBufferKeys.map((key) => {
724
+ const def = (material.storageBuffers as StorageBufferDefinitionMap)[key];
725
+ return `${key}:${def?.type ?? '?'}:${def?.size ?? 0}:${def?.access ?? 'read-write'}`;
726
+ }),
727
+ storageTextureKeys
649
728
  });
650
729
 
651
- const resolved: ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey> = {
730
+ const resolved: ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey, TStorageBufferKey> = {
652
731
  fragmentWgsl,
653
732
  fragmentLineMap: preprocessed.lineMap,
654
733
  uniforms,
@@ -659,7 +738,9 @@ export function resolveMaterial<
659
738
  fragmentSource: material.fragment,
660
739
  includeSources: material.includes as MaterialIncludes<TIncludeKey>,
661
740
  defineBlockSource: preprocessed.defineBlockSource,
662
- source: materialSourceMetadataCache.get(material) ?? null
741
+ source: materialSourceMetadataCache.get(material) ?? null,
742
+ storageBufferKeys,
743
+ storageTextureKeys
663
744
  };
664
745
 
665
746
  resolvedMaterialCache.set(material, resolved);
@@ -1,13 +1,17 @@
1
- import type { RenderPass, RenderPassInputSlot, RenderPassOutputSlot } from './types.js';
1
+ import type { AnyPass, RenderPass, RenderPassInputSlot, RenderPassOutputSlot } from './types.js';
2
2
 
3
3
  /**
4
4
  * Resolved render-pass step with defaults applied.
5
5
  */
6
6
  export interface RenderGraphStep {
7
+ /**
8
+ * Step kind. 'render' for existing passes, 'compute' for compute passes.
9
+ */
10
+ kind: 'render' | 'compute';
7
11
  /**
8
12
  * User pass instance.
9
13
  */
10
- pass: RenderPass;
14
+ pass: AnyPass;
11
15
  /**
12
16
  * Resolved input slot.
13
17
  */
@@ -65,7 +69,7 @@ function cloneClearColor(
65
69
  * @returns Resolved render graph plan.
66
70
  */
67
71
  export function planRenderGraph(
68
- passes: RenderPass[] | undefined,
72
+ passes: AnyPass[] | undefined,
69
73
  defaultClearColor: [number, number, number, number],
70
74
  renderTargetSlots?: Iterable<string>
71
75
  ): RenderGraphPlan {
@@ -80,9 +84,27 @@ export function planRenderGraph(
80
84
  continue;
81
85
  }
82
86
 
83
- const needsSwap = pass.needsSwap ?? true;
84
- const input: RenderPassInputSlot = pass.input ?? 'source';
85
- const output: RenderPassOutputSlot = pass.output ?? (needsSwap ? 'target' : 'source');
87
+ // Compute passes don't participate in slot routing
88
+ const isCompute = 'isCompute' in pass && (pass as { isCompute?: boolean }).isCompute === true;
89
+ if (isCompute) {
90
+ steps.push({
91
+ kind: 'compute',
92
+ pass,
93
+ input: 'source',
94
+ output: 'source',
95
+ needsSwap: false,
96
+ clear: false,
97
+ clearColor: cloneClearColor(defaultClearColor),
98
+ preserve: true
99
+ });
100
+ continue;
101
+ }
102
+
103
+ // After compute guard, pass is a render pass
104
+ const rp = pass as RenderPass;
105
+ const needsSwap = rp.needsSwap ?? true;
106
+ const input: RenderPassInputSlot = rp.input ?? 'source';
107
+ const output: RenderPassOutputSlot = rp.output ?? (needsSwap ? 'target' : 'source');
86
108
 
87
109
  if (input === 'canvas') {
88
110
  throw new Error(`Render pass #${enabledIndex} cannot read from "canvas".`);
@@ -108,11 +130,12 @@ export function planRenderGraph(
108
130
  throw new Error(`Render pass #${enabledIndex} reads "${input}" before it is written.`);
109
131
  }
110
132
 
111
- const clear = pass.clear ?? false;
112
- const clearColor = cloneClearColor(pass.clearColor ?? defaultClearColor);
113
- const preserve = pass.preserve ?? true;
133
+ const clear = rp.clear ?? false;
134
+ const clearColor = cloneClearColor(rp.clearColor ?? defaultClearColor);
135
+ const preserve = rp.preserve ?? true;
114
136
 
115
137
  steps.push({
138
+ kind: 'render',
116
139
  pass,
117
140
  input,
118
141
  output,
@@ -136,6 +159,13 @@ export function planRenderGraph(
136
159
  enabledIndex += 1;
137
160
  }
138
161
 
162
+ // When steps exist (even compute-only) but no render pass changed
163
+ // finalOutput from 'canvas', the scene was drawn to 'source' and
164
+ // needs blitting to the canvas surface.
165
+ if (steps.length > 0 && enabledIndex === 0) {
166
+ finalOutput = 'source';
167
+ }
168
+
139
169
  return {
140
170
  steps,
141
171
  finalOutput