@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
@@ -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;
@@ -81,6 +83,10 @@ export function createMotionGPURuntimeLoop(
81
83
  const renderUniforms: Record<string, UniformValue> = {};
82
84
  const renderTextures: TextureMap = {};
83
85
  const canvasSize = { width: 0, height: 0 };
86
+ let storageBufferKeys: string[] = [];
87
+ let storageBufferKeySet = new Set<string>();
88
+ let storageBufferDefinitions: StorageBufferDefinitionMap = {};
89
+ const pendingStorageWrites: PendingStorageWrite[] = [];
84
90
  let shouldContinueAfterFrame = false;
85
91
  let activeErrorKey: string | null = null;
86
92
  let errorHistory: MotionGPUErrorReport[] = [];
@@ -241,6 +247,10 @@ export function createMotionGPURuntimeLoop(
241
247
  textureKeys = materialState.textureKeys;
242
248
  uniformKeySet = new Set(uniformKeys);
243
249
  textureKeySet = new Set(textureKeys);
250
+ storageBufferKeys = materialState.storageBufferKeys;
251
+ storageBufferKeySet = new Set(storageBufferKeys);
252
+ storageBufferDefinitions = (options.getMaterial().storageBuffers ??
253
+ {}) as StorageBufferDefinitionMap;
244
254
  resetRuntimeMaps();
245
255
  resetRenderPayloadMaps();
246
256
  activeMaterialSignature = materialState.signature;
@@ -269,6 +279,67 @@ export function createMotionGPURuntimeLoop(
269
279
  runtimeTextures[name] = value;
270
280
  };
271
281
 
282
+ const writeStorageBuffer = (
283
+ name: string,
284
+ data: ArrayBufferView,
285
+ writeOptions?: { offset?: number }
286
+ ): void => {
287
+ if (!storageBufferKeySet.has(name)) {
288
+ throw new Error(
289
+ `Unknown storage buffer "${name}". Declare it in material.storageBuffers first.`
290
+ );
291
+ }
292
+ const definition = storageBufferDefinitions[name];
293
+ if (!definition) {
294
+ throw new Error(`Missing definition for storage buffer "${name}".`);
295
+ }
296
+ const offset = writeOptions?.offset ?? 0;
297
+ if (offset < 0 || offset + data.byteLength > definition.size) {
298
+ throw new Error(
299
+ `Storage buffer "${name}" write out of bounds: offset=${offset}, dataSize=${data.byteLength}, bufferSize=${definition.size}.`
300
+ );
301
+ }
302
+ pendingStorageWrites.push({ name, data, offset });
303
+ };
304
+
305
+ const readStorageBuffer = (name: string): Promise<ArrayBuffer> => {
306
+ if (!storageBufferKeySet.has(name)) {
307
+ throw new Error(
308
+ `Unknown storage buffer "${name}". Declare it in material.storageBuffers first.`
309
+ );
310
+ }
311
+ if (!renderer) {
312
+ return Promise.reject(
313
+ new Error(`Cannot read storage buffer "${name}": renderer not initialized.`)
314
+ );
315
+ }
316
+ const gpuBuffer = renderer.getStorageBuffer?.(name);
317
+ if (!gpuBuffer) {
318
+ return Promise.reject(new Error(`Storage buffer "${name}" not allocated on GPU.`));
319
+ }
320
+ const device = renderer.getDevice?.();
321
+ if (!device) {
322
+ return Promise.reject(new Error('Cannot read storage buffer: GPU device unavailable.'));
323
+ }
324
+ const definition = storageBufferDefinitions[name];
325
+ if (!definition) {
326
+ return Promise.reject(new Error(`Missing definition for storage buffer "${name}".`));
327
+ }
328
+ const stagingBuffer = device.createBuffer({
329
+ size: definition.size,
330
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
331
+ });
332
+ const commandEncoder = device.createCommandEncoder();
333
+ commandEncoder.copyBufferToBuffer(gpuBuffer, 0, stagingBuffer, 0, definition.size);
334
+ device.queue.submit([commandEncoder.finish()]);
335
+ return stagingBuffer.mapAsync(GPUMapMode.READ).then(() => {
336
+ const result = stagingBuffer.getMappedRange().slice(0);
337
+ stagingBuffer.unmap();
338
+ stagingBuffer.destroy();
339
+ return result;
340
+ });
341
+ };
342
+
272
343
  const renderFrame = (timestamp: number): void => {
273
344
  frameId = null;
274
345
  if (isDisposed) {
@@ -324,6 +395,9 @@ export function createMotionGPURuntimeLoop(
324
395
  uniformLayout: materialState.uniformLayout,
325
396
  textureKeys: materialState.textureKeys,
326
397
  textureDefinitions: materialState.textures,
398
+ storageBufferKeys: materialState.storageBufferKeys,
399
+ storageBufferDefinitions,
400
+ storageTextureKeys: materialState.storageTextureKeys,
327
401
  getRenderTargets: options.getRenderTargets,
328
402
  getPasses: options.getPasses,
329
403
  outputColorSpace,
@@ -380,6 +454,8 @@ export function createMotionGPURuntimeLoop(
380
454
  delta,
381
455
  setUniform,
382
456
  setTexture,
457
+ writeStorageBuffer,
458
+ readStorageBuffer,
383
459
  invalidate,
384
460
  advance,
385
461
  renderMode: registry.getRenderMode(),
@@ -413,7 +489,10 @@ export function createMotionGPURuntimeLoop(
413
489
  renderMode: registry.getRenderMode(),
414
490
  uniforms: renderUniforms,
415
491
  textures: renderTextures,
416
- canvasSize
492
+ canvasSize,
493
+ ...(pendingStorageWrites.length > 0
494
+ ? { pendingStorageWrites: pendingStorageWrites.splice(0) }
495
+ : {})
417
496
  });
418
497
  }
419
498
 
@@ -1,6 +1,6 @@
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
4
 
5
5
  /**
6
6
  * Fallback uniform field used when no custom uniforms are provided.
@@ -72,6 +72,38 @@ function buildTextureBindings(textureKeys: string[]): string {
72
72
  return declarations.join('\n');
73
73
  }
74
74
 
75
+ /**
76
+ * Builds read-only storage buffer bindings for fragment shader.
77
+ */
78
+ function buildFragmentStorageBufferBindings(
79
+ storageBufferKeys: string[],
80
+ definitions: Record<string, { type: StorageBufferType }>
81
+ ): string {
82
+ if (storageBufferKeys.length === 0) {
83
+ return '';
84
+ }
85
+
86
+ const declarations: string[] = [];
87
+
88
+ for (let index = 0; index < storageBufferKeys.length; index += 1) {
89
+ const key = storageBufferKeys[index];
90
+ if (key === undefined) {
91
+ continue;
92
+ }
93
+
94
+ const definition = definitions[key];
95
+ if (!definition) {
96
+ continue;
97
+ }
98
+
99
+ declarations.push(
100
+ `@group(1) @binding(${index}) var<storage, read> ${key}: ${definition.type};`
101
+ );
102
+ }
103
+
104
+ return declarations.join('\n');
105
+ }
106
+
75
107
  /**
76
108
  * Optionally returns helper WGSL for linear-to-sRGB conversion.
77
109
  */
@@ -143,7 +175,11 @@ export function buildShaderSource(
143
175
  fragmentWgsl: string,
144
176
  uniformLayout: UniformLayout,
145
177
  textureKeys: string[] = [],
146
- options?: { convertLinearToSrgb?: boolean }
178
+ options?: {
179
+ convertLinearToSrgb?: boolean;
180
+ storageBufferKeys?: string[];
181
+ storageBufferDefinitions?: Record<string, { type: StorageBufferType }>;
182
+ }
147
183
  ): string {
148
184
  const uniformFields = buildUniformStruct(uniformLayout);
149
185
  const keepAliveExpression = getKeepAliveExpression(uniformLayout);
@@ -151,6 +187,10 @@ export function buildShaderSource(
151
187
  const enableSrgbTransform = options?.convertLinearToSrgb ?? false;
152
188
  const colorTransformHelpers = buildColorTransformHelpers(enableSrgbTransform);
153
189
  const fragmentOutput = buildFragmentOutput(keepAliveExpression, enableSrgbTransform);
190
+ const storageBufferBindings = buildFragmentStorageBufferBindings(
191
+ options?.storageBufferKeys ?? [],
192
+ options?.storageBufferDefinitions ?? {}
193
+ );
154
194
 
155
195
  return `
156
196
  struct MotionGPUFrame {
@@ -166,6 +206,7 @@ struct MotionGPUUniforms {
166
206
  @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame;
167
207
  @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms;
168
208
  ${textureBindings}
209
+ ${storageBufferBindings ? '\n' + storageBufferBindings : ''}
169
210
  ${colorTransformHelpers}
170
211
 
171
212
  struct MotionGPUVertexOut {
@@ -207,6 +248,8 @@ export function buildShaderSourceWithMap(
207
248
  options?: {
208
249
  convertLinearToSrgb?: boolean;
209
250
  fragmentLineMap?: MaterialLineMap;
251
+ storageBufferKeys?: string[];
252
+ storageBufferDefinitions?: Record<string, { type: StorageBufferType }>;
210
253
  }
211
254
  ): BuiltShaderSource {
212
255
  const code = buildShaderSource(fragmentWgsl, uniformLayout, textureKeys, options);
@@ -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,18 @@ 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
+ * Explicit width for storage textures. Undefined when derived from source.
64
+ */
65
+ width?: number;
66
+ /**
67
+ * Explicit height for storage textures. Undefined when derived from source.
68
+ */
69
+ height?: number;
58
70
  }
59
71
 
60
72
  /**
@@ -90,19 +102,29 @@ export function resolveTextureKeys(textures: TextureDefinitionMap): string[] {
90
102
  export function normalizeTextureDefinition(
91
103
  definition: TextureDefinition | undefined
92
104
  ): NormalizedTextureDefinition {
105
+ const isStorage = definition?.storage === true;
106
+ const defaultFormat = definition?.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb';
93
107
  const normalized: NormalizedTextureDefinition = {
94
108
  source: definition?.source ?? null,
95
109
  colorSpace: definition?.colorSpace ?? 'srgb',
96
- format: definition?.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb',
110
+ format: definition?.format ?? defaultFormat,
97
111
  flipY: definition?.flipY ?? true,
98
112
  generateMipmaps: definition?.generateMipmaps ?? false,
99
113
  premultipliedAlpha: definition?.premultipliedAlpha ?? false,
100
114
  anisotropy: Math.max(1, Math.min(16, Math.floor(definition?.anisotropy ?? 1))),
101
115
  filter: definition?.filter ?? DEFAULT_TEXTURE_FILTER,
102
116
  addressModeU: definition?.addressModeU ?? DEFAULT_TEXTURE_ADDRESS_MODE,
103
- addressModeV: definition?.addressModeV ?? DEFAULT_TEXTURE_ADDRESS_MODE
117
+ addressModeV: definition?.addressModeV ?? DEFAULT_TEXTURE_ADDRESS_MODE,
118
+ storage: isStorage
104
119
  };
105
120
 
121
+ if (definition?.width !== undefined) {
122
+ normalized.width = definition.width;
123
+ }
124
+ if (definition?.height !== undefined) {
125
+ normalized.height = definition.height;
126
+ }
127
+
106
128
  if (definition?.update !== undefined) {
107
129
  normalized.update = definition.update;
108
130
  }
@@ -199,6 +199,26 @@ export interface TextureDefinition {
199
199
  * V axis address mode.
200
200
  */
201
201
  addressModeV?: GPUAddressMode;
202
+ /**
203
+ * When true, this texture is also writable by compute passes.
204
+ */
205
+ storage?: boolean;
206
+ /**
207
+ * Required when storage is true. Must be a storage-compatible format.
208
+ */
209
+ format?: GPUTextureFormat;
210
+ /**
211
+ * Explicit texture width. Required for storage textures without a source.
212
+ */
213
+ width?: number;
214
+ /**
215
+ * Explicit texture height. Required for storage textures without a source.
216
+ */
217
+ height?: number;
218
+ /**
219
+ * When true, texture is visible (sampled) in fragment shader. Default: true.
220
+ */
221
+ fragmentVisible?: boolean;
202
222
  }
203
223
 
204
224
  /**
@@ -211,6 +231,56 @@ export type TextureDefinitionMap<TKey extends string = string> = Record<TKey, Te
211
231
  */
212
232
  export type TextureMap<TKey extends string = string> = Record<TKey, TextureValue>;
213
233
 
234
+ // ── Storage buffer types ────────────────────────────────────────────────────
235
+
236
+ /**
237
+ * Access mode for storage buffers in compute shaders.
238
+ */
239
+ export type StorageBufferAccess = 'read' | 'read-write';
240
+
241
+ /**
242
+ * WGSL storage buffer element type.
243
+ */
244
+ export type StorageBufferType =
245
+ | 'array<f32>'
246
+ | 'array<vec2f>'
247
+ | 'array<vec3f>'
248
+ | 'array<vec4f>'
249
+ | 'array<u32>'
250
+ | 'array<i32>'
251
+ | 'array<vec4u>'
252
+ | 'array<vec4i>';
253
+
254
+ /**
255
+ * Definition of a single storage buffer resource.
256
+ */
257
+ export interface StorageBufferDefinition {
258
+ /**
259
+ * Buffer size in bytes. Must be > 0 and multiple of 4.
260
+ */
261
+ size: number;
262
+ /**
263
+ * WGSL type annotation for codegen.
264
+ */
265
+ type: StorageBufferType;
266
+ /**
267
+ * Access mode in compute shader. Default: 'read-write'.
268
+ */
269
+ access?: StorageBufferAccess;
270
+ /**
271
+ * Initial data uploaded on creation.
272
+ */
273
+ initialData?: Float32Array | Uint32Array | Int32Array;
274
+ }
275
+
276
+ /**
277
+ * Map of named storage buffer definitions.
278
+ */
279
+ export type StorageBufferDefinitionMap<TKey extends string = string> = Record<
280
+ TKey,
281
+ StorageBufferDefinition
282
+ >;
283
+
214
284
  /**
215
285
  * Output color space requested for final canvas presentation.
216
286
  */
@@ -368,6 +438,40 @@ export interface RenderPassContext extends Required<RenderPassFlags> {
368
438
  }) => GPURenderPassEncoder;
369
439
  }
370
440
 
441
+ /**
442
+ * Context provided to compute pass render calls.
443
+ */
444
+ export interface ComputePassContext {
445
+ /**
446
+ * Active GPU device.
447
+ */
448
+ device: GPUDevice;
449
+ /**
450
+ * Shared command encoder for this frame.
451
+ */
452
+ commandEncoder: GPUCommandEncoder;
453
+ /**
454
+ * Frame width in pixels.
455
+ */
456
+ width: number;
457
+ /**
458
+ * Frame height in pixels.
459
+ */
460
+ height: number;
461
+ /**
462
+ * Frame timestamp in seconds.
463
+ */
464
+ time: number;
465
+ /**
466
+ * Frame delta in seconds.
467
+ */
468
+ delta: number;
469
+ /**
470
+ * Begins a compute pass on the shared command encoder.
471
+ */
472
+ beginComputePass: () => GPUComputePassEncoder;
473
+ }
474
+
371
475
  /**
372
476
  * Formal render pass contract used by MotionGPU render graph.
373
477
  */
@@ -402,6 +506,22 @@ export interface RenderPass extends RenderPassFlags {
402
506
  dispose?: () => void;
403
507
  }
404
508
 
509
+ /**
510
+ * Minimal interface for compute passes in the render graph.
511
+ * Compute passes do not participate in slot routing.
512
+ */
513
+ export interface ComputePassLike {
514
+ readonly isCompute: true;
515
+ enabled?: boolean;
516
+ setSize?: (width: number, height: number) => void;
517
+ dispose?: () => void;
518
+ }
519
+
520
+ /**
521
+ * Union type for all pass types accepted by the render graph.
522
+ */
523
+ export type AnyPass = RenderPass | ComputePassLike;
524
+
405
525
  /**
406
526
  * Frame submission strategy for the scheduler.
407
527
  */
@@ -432,6 +552,14 @@ export interface FrameState {
432
552
  * Sets a texture value for current/next frame.
433
553
  */
434
554
  setTexture: (name: string, value: TextureValue) => void;
555
+ /**
556
+ * Writes data to a named storage buffer.
557
+ */
558
+ writeStorageBuffer: (name: string, data: ArrayBufferView, options?: { offset?: number }) => void;
559
+ /**
560
+ * Async readback of storage buffer data.
561
+ */
562
+ readStorageBuffer: (name: string) => Promise<ArrayBuffer>;
435
563
  /**
436
564
  * Invalidates frame for on-demand rendering.
437
565
  */
@@ -457,6 +585,18 @@ export interface FrameState {
457
585
  /**
458
586
  * Internal renderer construction options resolved from material/context state.
459
587
  */
588
+ /**
589
+ * Pending storage buffer write queued from FrameState.
590
+ */
591
+ export interface PendingStorageWrite {
592
+ /** Storage buffer name. */
593
+ name: string;
594
+ /** Data to write. */
595
+ data: ArrayBufferView;
596
+ /** Byte offset into the storage buffer. */
597
+ offset: number;
598
+ }
599
+
460
600
  export interface RendererOptions {
461
601
  /**
462
602
  * Target canvas.
@@ -513,22 +653,34 @@ export interface RendererOptions {
513
653
  * Texture definitions by key.
514
654
  */
515
655
  textureDefinitions: TextureDefinitionMap;
656
+ /**
657
+ * Sorted storage buffer keys.
658
+ */
659
+ storageBufferKeys?: string[];
660
+ /**
661
+ * Storage buffer definitions by key.
662
+ */
663
+ storageBufferDefinitions?: Record<string, import('./types.js').StorageBufferDefinition>;
664
+ /**
665
+ * Sorted storage texture keys (textures with storage:true).
666
+ */
667
+ storageTextureKeys?: string[];
516
668
  /**
517
669
  * Static render target definitions.
518
670
  */
519
671
  renderTargets?: RenderTargetDefinitionMap;
520
672
  /**
521
- * Static render passes.
673
+ * Static render and compute passes.
522
674
  */
523
- passes?: RenderPass[];
675
+ passes?: AnyPass[];
524
676
  /**
525
677
  * Dynamic render targets provider.
526
678
  */
527
679
  getRenderTargets?: () => RenderTargetDefinitionMap | undefined;
528
680
  /**
529
- * Dynamic render passes provider.
681
+ * Dynamic render and compute passes provider.
530
682
  */
531
- getPasses?: () => RenderPass[] | undefined;
683
+ getPasses?: () => AnyPass[] | undefined;
532
684
  /**
533
685
  * Requested output color space.
534
686
  */
@@ -574,7 +726,16 @@ export interface Renderer {
574
726
  width: number;
575
727
  height: number;
576
728
  };
729
+ pendingStorageWrites?: PendingStorageWrite[];
577
730
  }) => void;
731
+ /**
732
+ * Returns the GPU buffer for a named storage buffer, if allocated.
733
+ */
734
+ getStorageBuffer?: (name: string) => GPUBuffer | undefined;
735
+ /**
736
+ * Returns the active GPU device (for readback operations).
737
+ */
738
+ getDevice?: () => GPUDevice;
578
739
  /**
579
740
  * Releases GPU resources and subscriptions.
580
741
  */