@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
@@ -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
  /**
@@ -202,6 +211,19 @@ function buildSourceFromDiagnostics(error: unknown): MotionGPUErrorSource | null
202
211
  };
203
212
  }
204
213
 
214
+ if (location.kind === 'compute') {
215
+ const computeSource = diagnostics.computeSource ?? diagnostics.fragmentSource;
216
+ const component = 'Compute shader';
217
+ const locationLabel = formatShaderSourceLocation(location) ?? `compute line ${location.line}`;
218
+ return {
219
+ component,
220
+ location: `${component} (${locationLabel})`,
221
+ line: location.line,
222
+ ...(column !== undefined ? { column } : {}),
223
+ snippet: toSnippet(computeSource, location.line)
224
+ };
225
+ }
226
+
205
227
  const defineName = location.define ?? 'unknown';
206
228
  const defineLine = Math.max(1, location.line);
207
229
  const component = `#define ${defineName}`;
@@ -274,6 +296,50 @@ function classifyErrorMessage(
274
296
  };
275
297
  }
276
298
 
299
+ if (
300
+ message.includes('Invalid include directive in fragment shader.') ||
301
+ message.includes('Unknown include "') ||
302
+ message.includes('Circular include detected for "') ||
303
+ message.includes('Invalid define value for "') ||
304
+ message.includes('Invalid include "')
305
+ ) {
306
+ return {
307
+ code: 'MATERIAL_PREPROCESS_FAILED',
308
+ severity: 'error',
309
+ recoverable: true,
310
+ title: 'Material preprocess failed',
311
+ hint: 'Validate #include keys, define values and include expansion order before retrying.'
312
+ };
313
+ }
314
+
315
+ if (message.includes('Compute shader compilation failed')) {
316
+ return {
317
+ code: 'COMPUTE_COMPILATION_FAILED',
318
+ severity: 'error',
319
+ recoverable: true,
320
+ title: 'Compute shader compilation failed',
321
+ hint: 'Check WGSL compute shader sources below and verify storage bindings.'
322
+ };
323
+ }
324
+
325
+ if (
326
+ message.includes(
327
+ 'Compute shader must declare `@compute @workgroup_size(...) fn compute(...)`.'
328
+ ) ||
329
+ message.includes('Compute shader must include a `@builtin(global_invocation_id)` parameter.') ||
330
+ message.includes('Could not extract @workgroup_size from compute shader source.') ||
331
+ message.includes('@workgroup_size dimensions must be integers in range') ||
332
+ message.includes('Unsupported storage buffer access mode "')
333
+ ) {
334
+ return {
335
+ code: 'COMPUTE_CONTRACT_INVALID',
336
+ severity: 'error',
337
+ recoverable: true,
338
+ title: 'Compute contract is invalid',
339
+ hint: 'Ensure compute shader contract (@compute, @workgroup_size, global_invocation_id, storage access) is valid.'
340
+ };
341
+ }
342
+
277
343
  if (message.includes('WebGPU device lost') || message.includes('Device Lost')) {
278
344
  return {
279
345
  code: 'WEBGPU_DEVICE_LOST',
@@ -304,6 +370,82 @@ function classifyErrorMessage(
304
370
  };
305
371
  }
306
372
 
373
+ if (message.includes('Storage buffer "') && message.includes('write out of bounds:')) {
374
+ return {
375
+ code: 'STORAGE_BUFFER_OUT_OF_BOUNDS',
376
+ severity: 'error',
377
+ recoverable: true,
378
+ title: 'Storage buffer write out of bounds',
379
+ hint: 'Ensure offset + write byte length does not exceed declared storage buffer size.'
380
+ };
381
+ }
382
+
383
+ if (
384
+ message.includes('Cannot read storage buffer "') ||
385
+ message.includes('Cannot read storage buffer: GPU device unavailable.') ||
386
+ message.includes('not allocated on GPU.')
387
+ ) {
388
+ return {
389
+ code: 'STORAGE_BUFFER_READ_FAILED',
390
+ severity: 'error',
391
+ recoverable: true,
392
+ title: 'Storage buffer read failed',
393
+ hint: 'Readbacks require an initialized renderer, allocated GPU buffer and active device.'
394
+ };
395
+ }
396
+
397
+ if (
398
+ message.includes('Unknown uniform "') ||
399
+ message.includes('Unknown uniform type for "') ||
400
+ message.includes('Unknown texture "') ||
401
+ message.includes('Unknown storage buffer "') ||
402
+ message.includes('Missing definition for storage buffer "') ||
403
+ message.includes('Missing texture definition for "') ||
404
+ (message.includes('Storage buffer "') && message.includes('" not allocated.')) ||
405
+ (message.includes('Storage texture "') && message.includes('" not allocated.'))
406
+ ) {
407
+ return {
408
+ code: 'RUNTIME_RESOURCE_MISSING',
409
+ severity: 'error',
410
+ recoverable: true,
411
+ title: 'Runtime resource binding failed',
412
+ hint: 'Check material declarations and runtime keys for uniforms, textures and storage resources.'
413
+ };
414
+ }
415
+
416
+ if (message.includes('Uniform ') && message.includes(' value must')) {
417
+ return {
418
+ code: 'UNIFORM_VALUE_INVALID',
419
+ severity: 'error',
420
+ recoverable: true,
421
+ title: 'Uniform value is invalid',
422
+ hint: 'Provide finite values with tuple/matrix sizes matching the uniform type.'
423
+ };
424
+ }
425
+
426
+ if (
427
+ message.includes('Render pass #') ||
428
+ message.includes('Render graph references unknown runtime target')
429
+ ) {
430
+ return {
431
+ code: 'RENDER_GRAPH_INVALID',
432
+ severity: 'error',
433
+ recoverable: true,
434
+ title: 'Render graph configuration is invalid',
435
+ hint: 'Verify pass inputs/outputs, declared render targets and execution order.'
436
+ };
437
+ }
438
+
439
+ if (message.includes('PingPongComputePass must provide a target texture key.')) {
440
+ return {
441
+ code: 'PINGPONG_CONFIGURATION_INVALID',
442
+ severity: 'error',
443
+ recoverable: true,
444
+ title: 'Ping-pong compute pass is misconfigured',
445
+ hint: 'Configure a valid target texture key for PingPongComputePass.'
446
+ };
447
+ }
448
+
307
449
  if (message.includes('Destination texture needs to have CopyDst')) {
308
450
  return {
309
451
  code: 'TEXTURE_USAGE_INVALID',
@@ -387,7 +529,19 @@ export function toMotionGPUErrorReport(
387
529
  error instanceof Error && error.stack
388
530
  ? splitLines(error.stack).filter((line) => line !== message)
389
531
  : [];
390
- const classification = classifyErrorMessage(rawMessage);
532
+ let classification = classifyErrorMessage(rawMessage);
533
+ if (
534
+ shaderDiagnostics?.shaderStage === 'compute' &&
535
+ classification.code === 'WGSL_COMPILATION_FAILED'
536
+ ) {
537
+ classification = {
538
+ code: 'COMPUTE_COMPILATION_FAILED',
539
+ severity: 'error',
540
+ recoverable: true,
541
+ title: 'Compute shader compilation failed',
542
+ hint: 'Check WGSL compute shader sources below and verify storage bindings.'
543
+ };
544
+ }
391
545
 
392
546
  return {
393
547
  code: classification.code,
@@ -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+)\)?$/;
@@ -519,7 +542,8 @@ function buildTextureConfigSignature<TTextureKey extends string>(
519
542
  normalized.anisotropy,
520
543
  normalized.filter,
521
544
  normalized.addressModeU,
522
- normalized.addressModeV
545
+ normalized.addressModeV,
546
+ normalized.fragmentVisible ? '1' : '0'
523
547
  ].join(':');
524
548
  }
525
549
 
@@ -576,10 +600,11 @@ export function defineMaterial<
576
600
  TUniformKey extends string = string,
577
601
  TTextureKey extends string = string,
578
602
  TDefineKey extends string = string,
579
- TIncludeKey extends string = string
603
+ TIncludeKey extends string = string,
604
+ TStorageBufferKey extends string = string
580
605
  >(
581
- input: FragMaterialInput<TUniformKey, TTextureKey, TDefineKey, TIncludeKey>
582
- ): FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey> {
606
+ input: FragMaterialInput<TUniformKey, TTextureKey, TDefineKey, TIncludeKey, TStorageBufferKey>
607
+ ): FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey, TStorageBufferKey> {
583
608
  const fragment = resolveFragment(input.fragment);
584
609
  const uniforms = Object.freeze(resolveUniforms(input.uniforms));
585
610
  const textures = Object.freeze(resolveTextures(input.textures));
@@ -587,18 +612,60 @@ export function defineMaterial<
587
612
  const includes = Object.freeze(resolveIncludes(input.includes));
588
613
  const source = Object.freeze(resolveSourceMetadata(undefined));
589
614
 
615
+ // Validate and freeze storage buffers
616
+ const rawStorageBuffers =
617
+ input.storageBuffers ?? ({} as StorageBufferDefinitionMap<TStorageBufferKey>);
618
+ for (const [name, definition] of Object.entries(rawStorageBuffers) as Array<
619
+ [string, StorageBufferDefinition]
620
+ >) {
621
+ assertStorageBufferDefinition(name, definition);
622
+ }
623
+ const storageBuffers = Object.freeze(
624
+ Object.fromEntries(
625
+ Object.entries(rawStorageBuffers).map(([name, definition]) => {
626
+ const def = definition as StorageBufferDefinition;
627
+ const cloned: StorageBufferDefinition = {
628
+ size: def.size,
629
+ type: def.type,
630
+ ...(def.access !== undefined ? { access: def.access } : {}),
631
+ ...(def.initialData !== undefined
632
+ ? { initialData: def.initialData.slice() as typeof def.initialData }
633
+ : {})
634
+ };
635
+ return [name, Object.freeze(cloned)];
636
+ })
637
+ )
638
+ ) as Readonly<StorageBufferDefinitionMap<TStorageBufferKey>>;
639
+
640
+ // Validate storage textures
641
+ for (const [name, definition] of Object.entries(textures) as Array<[string, TextureDefinition]>) {
642
+ if (definition?.storage) {
643
+ if (!definition.format) {
644
+ throw new Error(`Texture "${name}" with storage:true requires a \`format\` field.`);
645
+ }
646
+ assertStorageTextureFormat(name, definition.format);
647
+ }
648
+ }
649
+
590
650
  const preprocessed = preprocessMaterialFragment({
591
651
  fragment,
592
652
  defines,
593
653
  includes
594
654
  });
595
655
 
596
- const material: FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey> = Object.freeze({
656
+ const material: FragMaterial<
657
+ TUniformKey,
658
+ TTextureKey,
659
+ TDefineKey,
660
+ TIncludeKey,
661
+ TStorageBufferKey
662
+ > = Object.freeze({
597
663
  fragment,
598
664
  uniforms,
599
665
  textures,
600
666
  defines,
601
- includes
667
+ includes,
668
+ storageBuffers
602
669
  });
603
670
 
604
671
  preprocessedFragmentCache.set(material, preprocessed);
@@ -616,10 +683,11 @@ export function resolveMaterial<
616
683
  TUniformKey extends string = string,
617
684
  TTextureKey extends string = string,
618
685
  TDefineKey extends string = string,
619
- TIncludeKey extends string = string
686
+ TIncludeKey extends string = string,
687
+ TStorageBufferKey extends string = string
620
688
  >(
621
- material: FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey>
622
- ): ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey> {
689
+ material: FragMaterial<TUniformKey, TTextureKey, TDefineKey, TIncludeKey, TStorageBufferKey>
690
+ ): ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey, TStorageBufferKey> {
623
691
  assertDefinedMaterial(material);
624
692
 
625
693
  const cached = getCachedResolvedMaterial(material);
@@ -641,14 +709,26 @@ export function resolveMaterial<
641
709
  const fragmentWgsl = preprocessed.fragment;
642
710
  const textureConfig = buildTextureConfigSignature(textures, textureKeys);
643
711
 
712
+ const storageBufferKeys = Object.keys(
713
+ material.storageBuffers ?? {}
714
+ ).sort() as TStorageBufferKey[];
715
+ const storageTextureKeys = textureKeys.filter(
716
+ (key) => (textures[key] as TextureDefinition)?.storage === true
717
+ );
718
+
644
719
  const signature = JSON.stringify({
645
720
  fragmentWgsl,
646
721
  uniforms: uniformLayout.entries.map((entry) => `${entry.name}:${entry.type}`),
647
722
  textureKeys,
648
- textureConfig
723
+ textureConfig,
724
+ storageBufferKeys: storageBufferKeys.map((key) => {
725
+ const def = (material.storageBuffers as StorageBufferDefinitionMap)[key];
726
+ return `${key}:${def?.type ?? '?'}:${def?.size ?? 0}:${def?.access ?? 'read-write'}`;
727
+ }),
728
+ storageTextureKeys
649
729
  });
650
730
 
651
- const resolved: ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey> = {
731
+ const resolved: ResolvedMaterial<TUniformKey, TTextureKey, TIncludeKey, TStorageBufferKey> = {
652
732
  fragmentWgsl,
653
733
  fragmentLineMap: preprocessed.lineMap,
654
734
  uniforms,
@@ -659,7 +739,9 @@ export function resolveMaterial<
659
739
  fragmentSource: material.fragment,
660
740
  includeSources: material.includes as MaterialIncludes<TIncludeKey>,
661
741
  defineBlockSource: preprocessed.defineBlockSource,
662
- source: materialSourceMetadataCache.get(material) ?? null
742
+ source: materialSourceMetadataCache.get(material) ?? null,
743
+ storageBufferKeys,
744
+ storageTextureKeys
663
745
  };
664
746
 
665
747
  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