@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.
- package/README.md +99 -0
- package/dist/advanced.js +3 -1
- package/dist/core/advanced.js +3 -1
- package/dist/core/compute-bindgroup-cache.d.ts +13 -0
- package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
- package/dist/core/compute-bindgroup-cache.js +45 -0
- package/dist/core/compute-bindgroup-cache.js.map +1 -0
- package/dist/core/compute-shader.d.ts +135 -0
- package/dist/core/compute-shader.d.ts.map +1 -0
- package/dist/core/compute-shader.js +238 -0
- package/dist/core/compute-shader.js.map +1 -0
- package/dist/core/error-diagnostics.d.ts +8 -1
- package/dist/core/error-diagnostics.d.ts.map +1 -1
- package/dist/core/error-diagnostics.js +7 -3
- package/dist/core/error-diagnostics.js.map +1 -1
- 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 +82 -1
- 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 +32 -4
- 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 +489 -29
- 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 +74 -14
- package/dist/core/runtime-loop.js.map +1 -1
- package/dist/core/shader.d.ts +16 -3
- package/dist/core/shader.d.ts.map +1 -1
- package/dist/core/shader.js +22 -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 +16 -0
- package/dist/core/textures.d.ts.map +1 -1
- package/dist/core/textures.js +8 -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-bindgroup-cache.ts +73 -0
- package/src/lib/core/compute-shader.ts +412 -0
- package/src/lib/core/error-diagnostics.ts +29 -4
- package/src/lib/core/error-report.ts +155 -1
- 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 +103 -21
- package/src/lib/core/render-graph.ts +39 -9
- package/src/lib/core/renderer.ts +768 -48
- package/src/lib/core/runtime-loop.ts +116 -16
- package/src/lib/core/shader.ts +58 -4
- package/src/lib/core/storage-buffers.ts +142 -0
- package/src/lib/core/texture-loader.ts +6 -0
- package/src/lib/core/textures.ts +29 -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;
|
|
@@ -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
|
|
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
|
|
163
|
+
errorHistory.push(report);
|
|
145
164
|
if (errorHistory.length > historyLimit) {
|
|
146
|
-
errorHistory
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/core/shader.ts
CHANGED
|
@@ -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?: {
|
|
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(
|
|
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
|
|
package/src/lib/core/textures.ts
CHANGED
|
@@ -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?.
|
|
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
|
}
|