@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.
- package/README.md +99 -0
- package/dist/advanced.js +3 -1
- package/dist/core/advanced.js +3 -1
- package/dist/core/compute-shader.d.ts +87 -0
- package/dist/core/compute-shader.d.ts.map +1 -0
- package/dist/core/compute-shader.js +205 -0
- package/dist/core/compute-shader.js.map +1 -0
- package/dist/core/error-report.d.ts +1 -1
- package/dist/core/error-report.d.ts.map +1 -1
- package/dist/core/error-report.js +63 -0
- package/dist/core/error-report.js.map +1 -1
- package/dist/core/frame-registry.d.ts.map +1 -1
- package/dist/core/frame-registry.js +1 -1
- package/dist/core/frame-registry.js.map +1 -1
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/material-preprocess.d.ts.map +1 -1
- package/dist/core/material-preprocess.js +5 -3
- package/dist/core/material-preprocess.js.map +1 -1
- package/dist/core/material.d.ts +22 -6
- package/dist/core/material.d.ts.map +1 -1
- package/dist/core/material.js +30 -3
- package/dist/core/material.js.map +1 -1
- package/dist/core/render-graph.d.ts +7 -3
- package/dist/core/render-graph.d.ts.map +1 -1
- package/dist/core/render-graph.js +22 -6
- package/dist/core/render-graph.js.map +1 -1
- package/dist/core/renderer.d.ts.map +1 -1
- package/dist/core/renderer.js +418 -23
- package/dist/core/renderer.js.map +1 -1
- package/dist/core/runtime-loop.d.ts +2 -2
- package/dist/core/runtime-loop.d.ts.map +1 -1
- package/dist/core/runtime-loop.js +49 -1
- package/dist/core/runtime-loop.js.map +1 -1
- package/dist/core/shader.d.ts +9 -1
- package/dist/core/shader.d.ts.map +1 -1
- package/dist/core/shader.js +21 -2
- package/dist/core/shader.js.map +1 -1
- package/dist/core/storage-buffers.d.ts +37 -0
- package/dist/core/storage-buffers.d.ts.map +1 -0
- package/dist/core/storage-buffers.js +95 -0
- package/dist/core/storage-buffers.js.map +1 -0
- package/dist/core/texture-loader.d.ts.map +1 -1
- package/dist/core/texture-loader.js +4 -0
- package/dist/core/texture-loader.js.map +1 -1
- package/dist/core/textures.d.ts +12 -0
- package/dist/core/textures.d.ts.map +1 -1
- package/dist/core/textures.js +7 -2
- package/dist/core/textures.js.map +1 -1
- package/dist/core/types.d.ts +146 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/passes/ComputePass.d.ts +83 -0
- package/dist/passes/ComputePass.d.ts.map +1 -0
- package/dist/passes/ComputePass.js +92 -0
- package/dist/passes/ComputePass.js.map +1 -0
- package/dist/passes/PingPongComputePass.d.ts +104 -0
- package/dist/passes/PingPongComputePass.d.ts.map +1 -0
- package/dist/passes/PingPongComputePass.js +132 -0
- package/dist/passes/PingPongComputePass.js.map +1 -0
- package/dist/passes/ShaderPass.d.ts.map +1 -1
- package/dist/passes/ShaderPass.js +2 -1
- package/dist/passes/ShaderPass.js.map +1 -1
- package/dist/passes/index.d.ts +2 -0
- package/dist/passes/index.d.ts.map +1 -1
- package/dist/passes/index.js +3 -1
- package/dist/react/FragCanvas.d.ts +2 -2
- package/dist/react/FragCanvas.d.ts.map +1 -1
- package/dist/react/FragCanvas.js.map +1 -1
- package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
- package/dist/react/MotionGPUErrorOverlay.js +123 -20
- package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
- package/dist/react/advanced.js +3 -1
- package/dist/react/index.d.ts +5 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/svelte/FragCanvas.svelte +2 -2
- package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
- package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
- package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
- package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
- package/dist/svelte/advanced.js +3 -1
- package/dist/svelte/index.d.ts +5 -2
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +3 -1
- package/package.json +1 -1
- package/src/lib/core/compute-shader.ts +326 -0
- package/src/lib/core/error-report.ts +129 -0
- package/src/lib/core/frame-registry.ts +2 -1
- package/src/lib/core/index.ts +18 -1
- package/src/lib/core/material-preprocess.ts +17 -6
- package/src/lib/core/material.ts +101 -20
- package/src/lib/core/render-graph.ts +39 -9
- package/src/lib/core/renderer.ts +655 -41
- package/src/lib/core/runtime-loop.ts +82 -3
- package/src/lib/core/shader.ts +45 -2
- package/src/lib/core/storage-buffers.ts +142 -0
- package/src/lib/core/texture-loader.ts +6 -0
- package/src/lib/core/textures.ts +24 -2
- package/src/lib/core/types.ts +165 -4
- package/src/lib/passes/ComputePass.ts +136 -0
- package/src/lib/passes/PingPongComputePass.ts +180 -0
- package/src/lib/passes/ShaderPass.ts +2 -1
- package/src/lib/passes/index.ts +6 -0
- package/src/lib/react/FragCanvas.tsx +3 -3
- package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
- package/src/lib/react/index.ts +18 -1
- package/src/lib/svelte/FragCanvas.svelte +2 -2
- package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
- 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
|
-
|
|
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: () =>
|
|
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
|
|
package/src/lib/core/shader.ts
CHANGED
|
@@ -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?: {
|
|
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
|
|
package/src/lib/core/textures.ts
CHANGED
|
@@ -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?.
|
|
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
|
}
|
package/src/lib/core/types.ts
CHANGED
|
@@ -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?:
|
|
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?: () =>
|
|
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
|
*/
|