@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
@@ -10,11 +10,13 @@ import { buildRendererPipelineSignature } from './recompile-policy.js';
10
10
  import { assertUniformValueForType } from './uniforms.js';
11
11
  import type { FrameRegistry } from './frame-registry.js';
12
12
  import type {
13
+ AnyPass,
13
14
  FrameInvalidationToken,
14
15
  OutputColorSpace,
15
- RenderPass,
16
+ PendingStorageWrite,
16
17
  Renderer,
17
18
  RenderTargetDefinitionMap,
19
+ StorageBufferDefinitionMap,
18
20
  TextureMap,
19
21
  TextureValue,
20
22
  UniformType,
@@ -29,7 +31,7 @@ export interface MotionGPURuntimeLoopOptions {
29
31
  maxDelta: CurrentReadable<number>;
30
32
  getMaterial: () => FragMaterial;
31
33
  getRenderTargets: () => RenderTargetDefinitionMap;
32
- getPasses: () => RenderPass[];
34
+ getPasses: () => AnyPass[];
33
35
  getClearColor: () => [number, number, number, number];
34
36
  getOutputColorSpace: () => OutputColorSpace;
35
37
  getAdapterOptions: () => GPURequestAdapterOptions | undefined;
@@ -52,6 +54,8 @@ function getRendererRetryDelayMs(attempt: number): number {
52
54
  return Math.min(8000, 250 * 2 ** Math.max(0, attempt - 1));
53
55
  }
54
56
 
57
+ const ERROR_CLEAR_GRACE_MS = 750;
58
+
55
59
  export function createMotionGPURuntimeLoop(
56
60
  options: MotionGPURuntimeLoopOptions
57
61
  ): MotionGPURuntimeLoop {
@@ -81,9 +85,23 @@ export function createMotionGPURuntimeLoop(
81
85
  const renderUniforms: Record<string, UniformValue> = {};
82
86
  const renderTextures: TextureMap = {};
83
87
  const canvasSize = { width: 0, height: 0 };
88
+ let storageBufferKeys: string[] = [];
89
+ let storageBufferKeySet = new Set<string>();
90
+ let storageBufferDefinitions: StorageBufferDefinitionMap = {};
91
+ const pendingStorageWrites: PendingStorageWrite[] = [];
84
92
  let shouldContinueAfterFrame = false;
85
93
  let activeErrorKey: string | null = null;
86
94
  let errorHistory: MotionGPUErrorReport[] = [];
95
+ let errorClearReadyAtMs = 0;
96
+ let lastFrameTimestampMs = performance.now();
97
+
98
+ const resolveNowMs = (nowMs?: number): number => {
99
+ if (typeof nowMs === 'number' && Number.isFinite(nowMs)) {
100
+ return nowMs;
101
+ }
102
+
103
+ return lastFrameTimestampMs;
104
+ };
87
105
 
88
106
  const getHistoryLimit = (): number => {
89
107
  const value = options.getErrorHistoryLimit?.() ?? 0;
@@ -123,12 +141,13 @@ export function createMotionGPURuntimeLoop(
123
141
  return;
124
142
  }
125
143
 
126
- errorHistory = errorHistory.slice(errorHistory.length - limit);
144
+ errorHistory.splice(0, errorHistory.length - limit);
127
145
  publishErrorHistory();
128
146
  };
129
147
 
130
- const setError = (error: unknown, phase: MotionGPUErrorPhase): void => {
148
+ const setError = (error: unknown, phase: MotionGPUErrorPhase, nowMs?: number): void => {
131
149
  const report = toMotionGPUErrorReport(error, phase);
150
+ errorClearReadyAtMs = resolveNowMs(nowMs) + ERROR_CLEAR_GRACE_MS;
132
151
  const reportKey = JSON.stringify({
133
152
  phase: report.phase,
134
153
  title: report.title,
@@ -141,9 +160,9 @@ export function createMotionGPURuntimeLoop(
141
160
  activeErrorKey = reportKey;
142
161
  const historyLimit = getHistoryLimit();
143
162
  if (historyLimit > 0) {
144
- errorHistory = [...errorHistory, report];
163
+ errorHistory.push(report);
145
164
  if (errorHistory.length > historyLimit) {
146
- errorHistory = errorHistory.slice(errorHistory.length - historyLimit);
165
+ errorHistory.splice(0, errorHistory.length - historyLimit);
147
166
  }
148
167
  publishErrorHistory();
149
168
  }
@@ -160,12 +179,16 @@ export function createMotionGPURuntimeLoop(
160
179
  }
161
180
  };
162
181
 
163
- const clearError = (): void => {
182
+ const maybeClearError = (nowMs?: number): void => {
164
183
  if (activeErrorKey === null) {
165
184
  return;
166
185
  }
186
+ if (resolveNowMs(nowMs) < errorClearReadyAtMs) {
187
+ return;
188
+ }
167
189
 
168
190
  activeErrorKey = null;
191
+ errorClearReadyAtMs = 0;
169
192
  options.reportError(null);
170
193
  };
171
194
 
@@ -194,13 +217,13 @@ export function createMotionGPURuntimeLoop(
194
217
  const resetRuntimeMaps = (): void => {
195
218
  for (const key of Object.keys(runtimeUniforms)) {
196
219
  if (!uniformKeySet.has(key)) {
197
- Reflect.deleteProperty(runtimeUniforms, key);
220
+ delete runtimeUniforms[key];
198
221
  }
199
222
  }
200
223
 
201
224
  for (const key of Object.keys(runtimeTextures)) {
202
225
  if (!textureKeySet.has(key)) {
203
- Reflect.deleteProperty(runtimeTextures, key);
226
+ delete runtimeTextures[key];
204
227
  }
205
228
  }
206
229
  };
@@ -208,13 +231,13 @@ export function createMotionGPURuntimeLoop(
208
231
  const resetRenderPayloadMaps = (): void => {
209
232
  for (const key of Object.keys(renderUniforms)) {
210
233
  if (!uniformKeySet.has(key)) {
211
- Reflect.deleteProperty(renderUniforms, key);
234
+ delete renderUniforms[key];
212
235
  }
213
236
  }
214
237
 
215
238
  for (const key of Object.keys(renderTextures)) {
216
239
  if (!textureKeySet.has(key)) {
217
- Reflect.deleteProperty(renderTextures, key);
240
+ delete renderTextures[key];
218
241
  }
219
242
  }
220
243
  };
@@ -241,6 +264,10 @@ export function createMotionGPURuntimeLoop(
241
264
  textureKeys = materialState.textureKeys;
242
265
  uniformKeySet = new Set(uniformKeys);
243
266
  textureKeySet = new Set(textureKeys);
267
+ storageBufferKeys = materialState.storageBufferKeys;
268
+ storageBufferKeySet = new Set(storageBufferKeys);
269
+ storageBufferDefinitions = (options.getMaterial().storageBuffers ??
270
+ {}) as StorageBufferDefinitionMap;
244
271
  resetRuntimeMaps();
245
272
  resetRenderPayloadMaps();
246
273
  activeMaterialSignature = materialState.signature;
@@ -269,18 +296,80 @@ export function createMotionGPURuntimeLoop(
269
296
  runtimeTextures[name] = value;
270
297
  };
271
298
 
299
+ const writeStorageBuffer = (
300
+ name: string,
301
+ data: ArrayBufferView,
302
+ writeOptions?: { offset?: number }
303
+ ): void => {
304
+ if (!storageBufferKeySet.has(name)) {
305
+ throw new Error(
306
+ `Unknown storage buffer "${name}". Declare it in material.storageBuffers first.`
307
+ );
308
+ }
309
+ const definition = storageBufferDefinitions[name];
310
+ if (!definition) {
311
+ throw new Error(`Missing definition for storage buffer "${name}".`);
312
+ }
313
+ const offset = writeOptions?.offset ?? 0;
314
+ if (offset < 0 || offset + data.byteLength > definition.size) {
315
+ throw new Error(
316
+ `Storage buffer "${name}" write out of bounds: offset=${offset}, dataSize=${data.byteLength}, bufferSize=${definition.size}.`
317
+ );
318
+ }
319
+ pendingStorageWrites.push({ name, data, offset });
320
+ };
321
+
322
+ const readStorageBuffer = (name: string): Promise<ArrayBuffer> => {
323
+ if (!storageBufferKeySet.has(name)) {
324
+ throw new Error(
325
+ `Unknown storage buffer "${name}". Declare it in material.storageBuffers first.`
326
+ );
327
+ }
328
+ if (!renderer) {
329
+ return Promise.reject(
330
+ new Error(`Cannot read storage buffer "${name}": renderer not initialized.`)
331
+ );
332
+ }
333
+ const gpuBuffer = renderer.getStorageBuffer?.(name);
334
+ if (!gpuBuffer) {
335
+ return Promise.reject(new Error(`Storage buffer "${name}" not allocated on GPU.`));
336
+ }
337
+ const device = renderer.getDevice?.();
338
+ if (!device) {
339
+ return Promise.reject(new Error('Cannot read storage buffer: GPU device unavailable.'));
340
+ }
341
+ const definition = storageBufferDefinitions[name];
342
+ if (!definition) {
343
+ return Promise.reject(new Error(`Missing definition for storage buffer "${name}".`));
344
+ }
345
+ const stagingBuffer = device.createBuffer({
346
+ size: definition.size,
347
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
348
+ });
349
+ const commandEncoder = device.createCommandEncoder();
350
+ commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, definition.size);
351
+ device.queue.submit([commandEncoder.finish()]);
352
+ return stagingBuffer.mapAsync(GPUMapMode.READ).then(() => {
353
+ const result = stagingBuffer.getMappedRange().slice(0);
354
+ stagingBuffer.unmap();
355
+ stagingBuffer.destroy();
356
+ return result;
357
+ });
358
+ };
359
+
272
360
  const renderFrame = (timestamp: number): void => {
273
361
  frameId = null;
274
362
  if (isDisposed) {
275
363
  return;
276
364
  }
365
+ lastFrameTimestampMs = timestamp;
277
366
  syncErrorHistory();
278
367
 
279
368
  let materialState: ResolvedMaterial;
280
369
  try {
281
370
  materialState = resolveActiveMaterial();
282
371
  } catch (error) {
283
- setError(error, 'initialization');
372
+ setError(error, 'initialization', timestamp);
284
373
  scheduleFrame();
285
374
  return;
286
375
  }
@@ -324,6 +413,9 @@ export function createMotionGPURuntimeLoop(
324
413
  uniformLayout: materialState.uniformLayout,
325
414
  textureKeys: materialState.textureKeys,
326
415
  textureDefinitions: materialState.textures,
416
+ storageBufferKeys: materialState.storageBufferKeys,
417
+ storageBufferDefinitions,
418
+ storageTextureKeys: materialState.storageTextureKeys,
327
419
  getRenderTargets: options.getRenderTargets,
328
420
  getPasses: options.getPasses,
329
421
  outputColorSpace,
@@ -344,7 +436,7 @@ export function createMotionGPURuntimeLoop(
344
436
  failedRendererSignature = null;
345
437
  failedRendererAttempts = 0;
346
438
  nextRendererRetryAt = 0;
347
- clearError();
439
+ maybeClearError(performance.now());
348
440
  } catch (error) {
349
441
  failedRendererSignature = rendererSignature;
350
442
  failedRendererAttempts += 1;
@@ -380,6 +472,8 @@ export function createMotionGPURuntimeLoop(
380
472
  delta,
381
473
  setUniform,
382
474
  setTexture,
475
+ writeStorageBuffer,
476
+ readStorageBuffer,
383
477
  invalidate,
384
478
  advance,
385
479
  renderMode: registry.getRenderMode(),
@@ -413,13 +507,19 @@ export function createMotionGPURuntimeLoop(
413
507
  renderMode: registry.getRenderMode(),
414
508
  uniforms: renderUniforms,
415
509
  textures: renderTextures,
416
- canvasSize
510
+ canvasSize,
511
+ pendingStorageWrites: pendingStorageWrites.length > 0 ? pendingStorageWrites : undefined
417
512
  });
513
+ // Clear in-place after synchronous render() completes — avoids
514
+ // the splice(0) copy and eliminates the conditional spread object.
515
+ if (pendingStorageWrites.length > 0) {
516
+ pendingStorageWrites.length = 0;
517
+ }
418
518
  }
419
519
 
420
- clearError();
520
+ maybeClearError(timestamp);
421
521
  } catch (error) {
422
- setError(error, 'render');
522
+ setError(error, 'render', timestamp);
423
523
  } finally {
424
524
  registry.endFrame();
425
525
  }
@@ -1,6 +1,11 @@
1
1
  import { assertUniformName } from './uniforms.js';
2
2
  import type { MaterialLineMap, MaterialSourceLocation } from './material-preprocess.js';
3
- import type { UniformLayout } from './types.js';
3
+ import type { StorageBufferType, UniformLayout } from './types.js';
4
+
5
+ type ComputeShaderSourceLocation = {
6
+ kind: 'compute';
7
+ line: number;
8
+ };
4
9
 
5
10
  /**
6
11
  * Fallback uniform field used when no custom uniforms are provided.
@@ -72,6 +77,38 @@ function buildTextureBindings(textureKeys: string[]): string {
72
77
  return declarations.join('\n');
73
78
  }
74
79
 
80
+ /**
81
+ * Builds read-only storage buffer bindings for fragment shader.
82
+ */
83
+ function buildFragmentStorageBufferBindings(
84
+ storageBufferKeys: string[],
85
+ definitions: Record<string, { type: StorageBufferType }>
86
+ ): string {
87
+ if (storageBufferKeys.length === 0) {
88
+ return '';
89
+ }
90
+
91
+ const declarations: string[] = [];
92
+
93
+ for (let index = 0; index < storageBufferKeys.length; index += 1) {
94
+ const key = storageBufferKeys[index];
95
+ if (key === undefined) {
96
+ continue;
97
+ }
98
+
99
+ const definition = definitions[key];
100
+ if (!definition) {
101
+ continue;
102
+ }
103
+
104
+ declarations.push(
105
+ `@group(1) @binding(${index}) var<storage, read> ${key}: ${definition.type};`
106
+ );
107
+ }
108
+
109
+ return declarations.join('\n');
110
+ }
111
+
75
112
  /**
76
113
  * Optionally returns helper WGSL for linear-to-sRGB conversion.
77
114
  */
@@ -114,7 +151,7 @@ function buildFragmentOutput(keepAliveExpression: string, enableSrgbTransform: b
114
151
  /**
115
152
  * 1-based map from generated WGSL lines to original material source lines.
116
153
  */
117
- export type ShaderLineMap = Array<MaterialSourceLocation | null>;
154
+ export type ShaderLineMap = Array<(MaterialSourceLocation | ComputeShaderSourceLocation) | null>;
118
155
 
119
156
  /**
120
157
  * Result of shader source generation with line mapping metadata.
@@ -143,7 +180,11 @@ export function buildShaderSource(
143
180
  fragmentWgsl: string,
144
181
  uniformLayout: UniformLayout,
145
182
  textureKeys: string[] = [],
146
- options?: { convertLinearToSrgb?: boolean }
183
+ options?: {
184
+ convertLinearToSrgb?: boolean;
185
+ storageBufferKeys?: string[];
186
+ storageBufferDefinitions?: Record<string, { type: StorageBufferType }>;
187
+ }
147
188
  ): string {
148
189
  const uniformFields = buildUniformStruct(uniformLayout);
149
190
  const keepAliveExpression = getKeepAliveExpression(uniformLayout);
@@ -151,6 +192,10 @@ export function buildShaderSource(
151
192
  const enableSrgbTransform = options?.convertLinearToSrgb ?? false;
152
193
  const colorTransformHelpers = buildColorTransformHelpers(enableSrgbTransform);
153
194
  const fragmentOutput = buildFragmentOutput(keepAliveExpression, enableSrgbTransform);
195
+ const storageBufferBindings = buildFragmentStorageBufferBindings(
196
+ options?.storageBufferKeys ?? [],
197
+ options?.storageBufferDefinitions ?? {}
198
+ );
154
199
 
155
200
  return `
156
201
  struct MotionGPUFrame {
@@ -166,6 +211,7 @@ struct MotionGPUUniforms {
166
211
  @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame;
167
212
  @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms;
168
213
  ${textureBindings}
214
+ ${storageBufferBindings ? '\n' + storageBufferBindings : ''}
169
215
  ${colorTransformHelpers}
170
216
 
171
217
  struct MotionGPUVertexOut {
@@ -207,6 +253,8 @@ export function buildShaderSourceWithMap(
207
253
  options?: {
208
254
  convertLinearToSrgb?: boolean;
209
255
  fragmentLineMap?: MaterialLineMap;
256
+ storageBufferKeys?: string[];
257
+ storageBufferDefinitions?: Record<string, { type: StorageBufferType }>;
210
258
  }
211
259
  ): BuiltShaderSource {
212
260
  const code = buildShaderSource(fragmentWgsl, uniformLayout, textureKeys, options);
@@ -241,7 +289,9 @@ export function buildShaderSourceWithMap(
241
289
  /**
242
290
  * Converts source location metadata to user-facing diagnostics label.
243
291
  */
244
- export function formatShaderSourceLocation(location: MaterialSourceLocation | null): string | null {
292
+ export function formatShaderSourceLocation(
293
+ location: (MaterialSourceLocation | ComputeShaderSourceLocation) | null
294
+ ): string | null {
245
295
  if (!location) {
246
296
  return null;
247
297
  }
@@ -254,5 +304,9 @@ export function formatShaderSourceLocation(location: MaterialSourceLocation | nu
254
304
  return `include <${location.include}> line ${location.line}`;
255
305
  }
256
306
 
307
+ if (location.kind === 'compute') {
308
+ return `compute line ${location.line}`;
309
+ }
310
+
257
311
  return `define "${location.define}" line ${location.line}`;
258
312
  }
@@ -0,0 +1,142 @@
1
+ import { assertUniformName } from './uniforms.js';
2
+ import type {
3
+ StorageBufferDefinition,
4
+ StorageBufferDefinitionMap,
5
+ StorageBufferType
6
+ } from './types.js';
7
+
8
+ /**
9
+ * Valid WGSL storage buffer element types.
10
+ */
11
+ const VALID_STORAGE_BUFFER_TYPES: ReadonlySet<string> = new Set<StorageBufferType>([
12
+ 'array<f32>',
13
+ 'array<vec2f>',
14
+ 'array<vec3f>',
15
+ 'array<vec4f>',
16
+ 'array<u32>',
17
+ 'array<i32>',
18
+ 'array<vec4u>',
19
+ 'array<vec4i>'
20
+ ]);
21
+
22
+ /**
23
+ * Storage-compatible texture formats for `texture_storage_2d`.
24
+ */
25
+ export const STORAGE_TEXTURE_FORMATS: ReadonlySet<GPUTextureFormat> = new Set([
26
+ 'r32float',
27
+ 'r32sint',
28
+ 'r32uint',
29
+ 'rg32float',
30
+ 'rg32sint',
31
+ 'rg32uint',
32
+ 'rgba8unorm',
33
+ 'rgba8snorm',
34
+ 'rgba8uint',
35
+ 'rgba8sint',
36
+ 'rgba16float',
37
+ 'rgba16uint',
38
+ 'rgba16sint',
39
+ 'rgba32float',
40
+ 'rgba32uint',
41
+ 'rgba32sint',
42
+ 'bgra8unorm'
43
+ ] as GPUTextureFormat[]);
44
+
45
+ /**
46
+ * Validates a single storage buffer definition.
47
+ *
48
+ * @param name - Buffer identifier.
49
+ * @param definition - Storage buffer definition to validate.
50
+ * @throws {Error} When any field is invalid.
51
+ */
52
+ export function assertStorageBufferDefinition(
53
+ name: string,
54
+ definition: StorageBufferDefinition
55
+ ): void {
56
+ assertUniformName(name);
57
+
58
+ if (!Number.isFinite(definition.size) || definition.size <= 0) {
59
+ throw new Error(
60
+ `Storage buffer "${name}" size must be a finite number greater than 0, got ${definition.size}`
61
+ );
62
+ }
63
+
64
+ if (definition.size % 4 !== 0) {
65
+ throw new Error(
66
+ `Storage buffer "${name}" size must be a multiple of 4, got ${definition.size}`
67
+ );
68
+ }
69
+
70
+ if (!VALID_STORAGE_BUFFER_TYPES.has(definition.type)) {
71
+ throw new Error(
72
+ `Storage buffer "${name}" has unknown type "${definition.type}". Supported types: ${[...VALID_STORAGE_BUFFER_TYPES].join(', ')}`
73
+ );
74
+ }
75
+
76
+ if (
77
+ definition.access !== undefined &&
78
+ definition.access !== 'read' &&
79
+ definition.access !== 'read-write'
80
+ ) {
81
+ throw new Error(
82
+ `Storage buffer "${name}" has invalid access mode "${definition.access}". Use 'read' or 'read-write'.`
83
+ );
84
+ }
85
+
86
+ if (definition.initialData !== undefined) {
87
+ if (definition.initialData.byteLength > definition.size) {
88
+ throw new Error(
89
+ `Storage buffer "${name}" initialData byte length (${definition.initialData.byteLength}) exceeds buffer size (${definition.size})`
90
+ );
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Validates and returns sorted storage buffer keys.
97
+ *
98
+ * @param definitions - Storage buffer definition map.
99
+ * @returns Lexicographically sorted buffer keys.
100
+ */
101
+ export function resolveStorageBufferKeys(definitions: StorageBufferDefinitionMap): string[] {
102
+ const keys = Object.keys(definitions).sort();
103
+ for (const key of keys) {
104
+ const definition = definitions[key];
105
+ if (definition) {
106
+ assertStorageBufferDefinition(key, definition);
107
+ }
108
+ }
109
+ return keys;
110
+ }
111
+
112
+ /**
113
+ * Normalizes a storage buffer definition with defaults applied.
114
+ *
115
+ * @param definition - Raw definition.
116
+ * @returns Normalized definition with access default.
117
+ */
118
+ export function normalizeStorageBufferDefinition(
119
+ definition: StorageBufferDefinition
120
+ ): Required<Pick<StorageBufferDefinition, 'size' | 'type' | 'access'>> {
121
+ return {
122
+ size: definition.size,
123
+ type: definition.type,
124
+ access: definition.access ?? 'read-write'
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Validates that a texture format is storage-compatible.
130
+ *
131
+ * @param name - Texture identifier.
132
+ * @param format - GPU texture format.
133
+ * @throws {Error} When format is not storage-compatible.
134
+ */
135
+ export function assertStorageTextureFormat(name: string, format: GPUTextureFormat): void {
136
+ if (!STORAGE_TEXTURE_FORMATS.has(format)) {
137
+ throw new Error(
138
+ `Texture "${name}" with storage:true requires a storage-compatible format, but got "${format}". ` +
139
+ `Supported formats: ${[...STORAGE_TEXTURE_FORMATS].join(', ')}`
140
+ );
141
+ }
142
+ }
@@ -406,6 +406,7 @@ export async function loadTextureFromUrl(
406
406
  throw createAbortError();
407
407
  }
408
408
 
409
+ let disposed = false;
409
410
  const loaded: LoadedTexture = {
410
411
  url,
411
412
  source: bitmap,
@@ -413,7 +414,12 @@ export async function loadTextureFromUrl(
413
414
  height: bitmap.height,
414
415
  colorSpace: normalized.colorSpace,
415
416
  dispose: () => {
417
+ if (disposed) {
418
+ return;
419
+ }
420
+ disposed = true;
416
421
  bitmap?.close();
422
+ bitmap = null;
417
423
  }
418
424
  };
419
425
 
@@ -55,6 +55,22 @@ export interface NormalizedTextureDefinition {
55
55
  * Effective V address mode.
56
56
  */
57
57
  addressModeV: GPUAddressMode;
58
+ /**
59
+ * Whether this texture is a storage texture (writable by compute).
60
+ */
61
+ storage: boolean;
62
+ /**
63
+ * Whether this texture should be exposed as a fragment-stage sampled binding.
64
+ */
65
+ fragmentVisible: boolean;
66
+ /**
67
+ * Explicit width for storage textures. Undefined when derived from source.
68
+ */
69
+ width?: number;
70
+ /**
71
+ * Explicit height for storage textures. Undefined when derived from source.
72
+ */
73
+ height?: number;
58
74
  }
59
75
 
60
76
  /**
@@ -90,19 +106,30 @@ export function resolveTextureKeys(textures: TextureDefinitionMap): string[] {
90
106
  export function normalizeTextureDefinition(
91
107
  definition: TextureDefinition | undefined
92
108
  ): NormalizedTextureDefinition {
109
+ const isStorage = definition?.storage === true;
110
+ const defaultFormat = definition?.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb';
93
111
  const normalized: NormalizedTextureDefinition = {
94
112
  source: definition?.source ?? null,
95
113
  colorSpace: definition?.colorSpace ?? 'srgb',
96
- format: definition?.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb',
114
+ format: definition?.format ?? defaultFormat,
97
115
  flipY: definition?.flipY ?? true,
98
116
  generateMipmaps: definition?.generateMipmaps ?? false,
99
117
  premultipliedAlpha: definition?.premultipliedAlpha ?? false,
100
118
  anisotropy: Math.max(1, Math.min(16, Math.floor(definition?.anisotropy ?? 1))),
101
119
  filter: definition?.filter ?? DEFAULT_TEXTURE_FILTER,
102
120
  addressModeU: definition?.addressModeU ?? DEFAULT_TEXTURE_ADDRESS_MODE,
103
- addressModeV: definition?.addressModeV ?? DEFAULT_TEXTURE_ADDRESS_MODE
121
+ addressModeV: definition?.addressModeV ?? DEFAULT_TEXTURE_ADDRESS_MODE,
122
+ storage: isStorage,
123
+ fragmentVisible: definition?.fragmentVisible ?? true
104
124
  };
105
125
 
126
+ if (definition?.width !== undefined) {
127
+ normalized.width = definition.width;
128
+ }
129
+ if (definition?.height !== undefined) {
130
+ normalized.height = definition.height;
131
+ }
132
+
106
133
  if (definition?.update !== undefined) {
107
134
  normalized.update = definition.update;
108
135
  }