@motion-core/motion-gpu 0.5.0 → 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/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 +48 -0
- package/dist/core/compute-shader.d.ts.map +1 -1
- package/dist/core/compute-shader.js +34 -1
- package/dist/core/compute-shader.js.map +1 -1
- 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.map +1 -1
- package/dist/core/error-report.js +19 -1
- package/dist/core/error-report.js.map +1 -1
- package/dist/core/material.d.ts.map +1 -1
- package/dist/core/material.js +2 -1
- package/dist/core/material.js.map +1 -1
- package/dist/core/renderer.d.ts.map +1 -1
- package/dist/core/renderer.js +150 -85
- package/dist/core/renderer.js.map +1 -1
- package/dist/core/runtime-loop.d.ts.map +1 -1
- package/dist/core/runtime-loop.js +26 -14
- package/dist/core/runtime-loop.js.map +1 -1
- package/dist/core/shader.d.ts +7 -2
- package/dist/core/shader.d.ts.map +1 -1
- package/dist/core/shader.js +1 -0
- package/dist/core/shader.js.map +1 -1
- package/dist/core/textures.d.ts +4 -0
- package/dist/core/textures.d.ts.map +1 -1
- package/dist/core/textures.js +2 -1
- package/dist/core/textures.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/core/compute-bindgroup-cache.ts +73 -0
- package/src/lib/core/compute-shader.ts +86 -0
- package/src/lib/core/error-diagnostics.ts +29 -4
- package/src/lib/core/error-report.ts +26 -1
- package/src/lib/core/material.ts +2 -1
- package/src/lib/core/renderer.ts +198 -92
- package/src/lib/core/runtime-loop.ts +37 -16
- package/src/lib/core/shader.ts +13 -2
- package/src/lib/core/textures.ts +6 -1
- package/src/lib/core/types.ts +1 -1
|
@@ -211,6 +211,19 @@ function buildSourceFromDiagnostics(error: unknown): MotionGPUErrorSource | null
|
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
if (location.kind === 'compute') {
|
|
215
|
+
const computeSource = diagnostics.computeSource ?? diagnostics.fragmentSource;
|
|
216
|
+
const component = 'Compute shader';
|
|
217
|
+
const locationLabel = formatShaderSourceLocation(location) ?? `compute line ${location.line}`;
|
|
218
|
+
return {
|
|
219
|
+
component,
|
|
220
|
+
location: `${component} (${locationLabel})`,
|
|
221
|
+
line: location.line,
|
|
222
|
+
...(column !== undefined ? { column } : {}),
|
|
223
|
+
snippet: toSnippet(computeSource, location.line)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
214
227
|
const defineName = location.define ?? 'unknown';
|
|
215
228
|
const defineLine = Math.max(1, location.line);
|
|
216
229
|
const component = `#define ${defineName}`;
|
|
@@ -516,7 +529,19 @@ export function toMotionGPUErrorReport(
|
|
|
516
529
|
error instanceof Error && error.stack
|
|
517
530
|
? splitLines(error.stack).filter((line) => line !== message)
|
|
518
531
|
: [];
|
|
519
|
-
|
|
532
|
+
let classification = classifyErrorMessage(rawMessage);
|
|
533
|
+
if (
|
|
534
|
+
shaderDiagnostics?.shaderStage === 'compute' &&
|
|
535
|
+
classification.code === 'WGSL_COMPILATION_FAILED'
|
|
536
|
+
) {
|
|
537
|
+
classification = {
|
|
538
|
+
code: 'COMPUTE_COMPILATION_FAILED',
|
|
539
|
+
severity: 'error',
|
|
540
|
+
recoverable: true,
|
|
541
|
+
title: 'Compute shader compilation failed',
|
|
542
|
+
hint: 'Check WGSL compute shader sources below and verify storage bindings.'
|
|
543
|
+
};
|
|
544
|
+
}
|
|
520
545
|
|
|
521
546
|
return {
|
|
522
547
|
code: classification.code,
|
package/src/lib/core/material.ts
CHANGED
|
@@ -542,7 +542,8 @@ function buildTextureConfigSignature<TTextureKey extends string>(
|
|
|
542
542
|
normalized.anisotropy,
|
|
543
543
|
normalized.filter,
|
|
544
544
|
normalized.addressModeU,
|
|
545
|
-
normalized.addressModeV
|
|
545
|
+
normalized.addressModeV,
|
|
546
|
+
normalized.fragmentVisible ? '1' : '0'
|
|
546
547
|
].join(':');
|
|
547
548
|
}
|
|
548
549
|
|
package/src/lib/core/renderer.ts
CHANGED
|
@@ -18,11 +18,12 @@ import {
|
|
|
18
18
|
} from './textures.js';
|
|
19
19
|
import { packUniformsInto } from './uniforms.js';
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
buildComputeShaderSourceWithMap,
|
|
22
|
+
buildPingPongComputeShaderSourceWithMap,
|
|
23
23
|
extractWorkgroupSize,
|
|
24
24
|
storageTextureSampleScalarType
|
|
25
25
|
} from './compute-shader.js';
|
|
26
|
+
import { createComputeStorageBindGroupCache } from './compute-bindgroup-cache.js';
|
|
26
27
|
import { normalizeStorageBufferDefinition } from './storage-buffers.js';
|
|
27
28
|
import type {
|
|
28
29
|
AnyPass,
|
|
@@ -62,6 +63,7 @@ interface RuntimeTextureBinding {
|
|
|
62
63
|
key: string;
|
|
63
64
|
samplerBinding: number;
|
|
64
65
|
textureBinding: number;
|
|
66
|
+
fragmentVisible: boolean;
|
|
65
67
|
sampler: GPUSampler;
|
|
66
68
|
fallbackTexture: GPUTexture;
|
|
67
69
|
fallbackView: GPUTextureView;
|
|
@@ -109,6 +111,8 @@ interface PingPongTexturePair {
|
|
|
109
111
|
textureB: GPUTexture;
|
|
110
112
|
viewB: GPUTextureView;
|
|
111
113
|
bindGroupLayout: GPUBindGroupLayout;
|
|
114
|
+
readAWriteBBindGroup: GPUBindGroup | null;
|
|
115
|
+
readBWriteABindGroup: GPUBindGroup | null;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
/**
|
|
@@ -187,6 +191,7 @@ async function assertCompilation(
|
|
|
187
191
|
options?: {
|
|
188
192
|
lineMap?: ShaderLineMap;
|
|
189
193
|
fragmentSource?: string;
|
|
194
|
+
computeSource?: string;
|
|
190
195
|
includeSources?: Record<string, string>;
|
|
191
196
|
defineBlockSource?: string;
|
|
192
197
|
materialSource?: {
|
|
@@ -197,6 +202,8 @@ async function assertCompilation(
|
|
|
197
202
|
functionName?: string;
|
|
198
203
|
} | null;
|
|
199
204
|
runtimeContext?: ShaderCompilationRuntimeContext;
|
|
205
|
+
errorPrefix?: string;
|
|
206
|
+
shaderStage?: 'fragment' | 'compute';
|
|
200
207
|
}
|
|
201
208
|
): Promise<void> {
|
|
202
209
|
const info = await module.getCompilationInfo();
|
|
@@ -227,11 +234,14 @@ async function assertCompilation(
|
|
|
227
234
|
return `[${contextLabel.join(' | ')}] ${diagnostic.message}`;
|
|
228
235
|
})
|
|
229
236
|
.join('\n');
|
|
230
|
-
const
|
|
237
|
+
const prefix = options?.errorPrefix ?? 'WGSL compilation failed';
|
|
238
|
+
const error = new Error(`${prefix}:\n${summary}`);
|
|
231
239
|
throw attachShaderCompilationDiagnostics(error, {
|
|
232
240
|
kind: 'shader-compilation',
|
|
241
|
+
...(options?.shaderStage !== undefined ? { shaderStage: options.shaderStage } : {}),
|
|
233
242
|
diagnostics,
|
|
234
243
|
fragmentSource: options?.fragmentSource ?? '',
|
|
244
|
+
...(options?.computeSource !== undefined ? { computeSource: options.computeSource } : {}),
|
|
235
245
|
includeSources: options?.includeSources ?? {},
|
|
236
246
|
...(options?.defineBlockSource !== undefined
|
|
237
247
|
? { defineBlockSource: options.defineBlockSource }
|
|
@@ -245,6 +255,64 @@ function toSortedUniqueStrings(values: string[]): string[] {
|
|
|
245
255
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
246
256
|
}
|
|
247
257
|
|
|
258
|
+
function extractGeneratedLineFromComputeError(message: string): number | null {
|
|
259
|
+
const lineMatch = message.match(/\bline\s+(\d+)\b/i);
|
|
260
|
+
if (lineMatch) {
|
|
261
|
+
const parsed = Number.parseInt(lineMatch[1] ?? '', 10);
|
|
262
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
263
|
+
return parsed;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const colonMatch = message.match(/:(\d+):\d+/);
|
|
268
|
+
if (colonMatch) {
|
|
269
|
+
const parsed = Number.parseInt(colonMatch[1] ?? '', 10);
|
|
270
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function toComputeCompilationError(input: {
|
|
279
|
+
error: unknown;
|
|
280
|
+
lineMap: ShaderLineMap;
|
|
281
|
+
computeSource: string;
|
|
282
|
+
runtimeContext: ShaderCompilationRuntimeContext;
|
|
283
|
+
}): Error {
|
|
284
|
+
const baseError =
|
|
285
|
+
input.error instanceof Error ? input.error : new Error(String(input.error ?? 'Unknown error'));
|
|
286
|
+
const generatedLine = extractGeneratedLineFromComputeError(baseError.message) ?? 0;
|
|
287
|
+
const sourceLocation = generatedLine > 0 ? (input.lineMap[generatedLine] ?? null) : null;
|
|
288
|
+
const diagnostics = [
|
|
289
|
+
{
|
|
290
|
+
generatedLine,
|
|
291
|
+
message: baseError.message,
|
|
292
|
+
sourceLocation
|
|
293
|
+
}
|
|
294
|
+
];
|
|
295
|
+
const sourceLabel = formatShaderSourceLocation(sourceLocation);
|
|
296
|
+
const generatedLineLabel = generatedLine > 0 ? `generated WGSL line ${generatedLine}` : null;
|
|
297
|
+
const contextLabel = [sourceLabel, generatedLineLabel].filter((value) => Boolean(value));
|
|
298
|
+
const summary =
|
|
299
|
+
contextLabel.length > 0
|
|
300
|
+
? `[${contextLabel.join(' | ')}] ${baseError.message}`
|
|
301
|
+
: baseError.message;
|
|
302
|
+
const wrapped = new Error(`Compute shader compilation failed:\n${summary}`);
|
|
303
|
+
|
|
304
|
+
return attachShaderCompilationDiagnostics(wrapped, {
|
|
305
|
+
kind: 'shader-compilation',
|
|
306
|
+
shaderStage: 'compute',
|
|
307
|
+
diagnostics,
|
|
308
|
+
fragmentSource: '',
|
|
309
|
+
computeSource: input.computeSource,
|
|
310
|
+
includeSources: {},
|
|
311
|
+
materialSource: null,
|
|
312
|
+
runtimeContext: input.runtimeContext
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
248
316
|
function buildPassGraphSnapshot(
|
|
249
317
|
passes: AnyPass[] | undefined
|
|
250
318
|
): NonNullable<ShaderCompilationRuntimeContext['passGraph']> {
|
|
@@ -667,10 +735,13 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
667
735
|
try {
|
|
668
736
|
const runtimeContext = buildShaderCompilationRuntimeContext(options);
|
|
669
737
|
const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
|
|
738
|
+
const fragmentTextureKeys = options.textureKeys.filter(
|
|
739
|
+
(key) => options.textureDefinitions[key]?.fragmentVisible !== false
|
|
740
|
+
);
|
|
670
741
|
const builtShader = buildShaderSourceWithMap(
|
|
671
742
|
options.fragmentWgsl,
|
|
672
743
|
options.uniformLayout,
|
|
673
|
-
|
|
744
|
+
fragmentTextureKeys,
|
|
674
745
|
{
|
|
675
746
|
convertLinearToSrgb,
|
|
676
747
|
fragmentLineMap: options.fragmentLineMap,
|
|
@@ -702,13 +773,18 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
702
773
|
const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
|
|
703
774
|
const storageTextureKeys = options.storageTextureKeys ?? [];
|
|
704
775
|
const storageTextureKeySet = new Set(storageTextureKeys);
|
|
705
|
-
const
|
|
776
|
+
const fragmentTextureIndexByKey = new Map(
|
|
777
|
+
fragmentTextureKeys.map((key, index) => [key, index] as const)
|
|
778
|
+
);
|
|
779
|
+
const textureBindings = options.textureKeys.map((key): RuntimeTextureBinding => {
|
|
706
780
|
const config = normalizedTextureDefinitions[key];
|
|
707
781
|
if (!config) {
|
|
708
782
|
throw new Error(`Missing texture definition for "${key}"`);
|
|
709
783
|
}
|
|
710
784
|
|
|
711
|
-
const
|
|
785
|
+
const fragmentTextureIndex = fragmentTextureIndexByKey.get(key);
|
|
786
|
+
const fragmentVisible = fragmentTextureIndex !== undefined;
|
|
787
|
+
const { samplerBinding, textureBinding } = getTextureBindings(fragmentTextureIndex ?? 0);
|
|
712
788
|
const sampler = device.createSampler({
|
|
713
789
|
magFilter: config.filter,
|
|
714
790
|
minFilter: config.filter,
|
|
@@ -731,6 +807,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
731
807
|
key,
|
|
732
808
|
samplerBinding,
|
|
733
809
|
textureBinding,
|
|
810
|
+
fragmentVisible,
|
|
734
811
|
sampler,
|
|
735
812
|
fallbackTexture,
|
|
736
813
|
fallbackView,
|
|
@@ -779,9 +856,49 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
779
856
|
|
|
780
857
|
return runtimeBinding;
|
|
781
858
|
});
|
|
859
|
+
const textureBindingByKey = new Map(textureBindings.map((binding) => [binding.key, binding]));
|
|
860
|
+
const fragmentTextureBindings = textureBindings.filter((binding) => binding.fragmentVisible);
|
|
861
|
+
|
|
862
|
+
const computeStorageBufferLayoutEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map(
|
|
863
|
+
(key, index) => {
|
|
864
|
+
const def = storageBufferDefinitions[key];
|
|
865
|
+
const access = def?.access ?? 'read-write';
|
|
866
|
+
const bufferType: GPUBufferBindingType =
|
|
867
|
+
access === 'read' ? 'read-only-storage' : 'storage';
|
|
868
|
+
return {
|
|
869
|
+
binding: index,
|
|
870
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
871
|
+
buffer: { type: bufferType }
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
);
|
|
875
|
+
const computeStorageBufferTopologyKey = storageBufferKeys
|
|
876
|
+
.map((key) => `${key}:${storageBufferDefinitions[key]?.access ?? 'read-write'}`)
|
|
877
|
+
.join('|');
|
|
878
|
+
|
|
879
|
+
const computeStorageTextureLayoutEntries: GPUBindGroupLayoutEntry[] = storageTextureKeys.map(
|
|
880
|
+
(key, index) => {
|
|
881
|
+
const config = normalizedTextureDefinitions[key];
|
|
882
|
+
return {
|
|
883
|
+
binding: index,
|
|
884
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
885
|
+
storageTexture: {
|
|
886
|
+
access: 'write-only' as GPUStorageTextureAccess,
|
|
887
|
+
format: (config?.format ?? 'rgba8unorm') as GPUTextureFormat,
|
|
888
|
+
viewDimension: '2d'
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
const computeStorageTextureTopologyKey = storageTextureKeys
|
|
894
|
+
.map((key) => `${key}:${normalizedTextureDefinitions[key]?.format ?? 'rgba8unorm'}`)
|
|
895
|
+
.join('|');
|
|
896
|
+
|
|
897
|
+
const computeStorageBufferBindGroupCache = createComputeStorageBindGroupCache(device);
|
|
898
|
+
const computeStorageTextureBindGroupCache = createComputeStorageBindGroupCache(device);
|
|
782
899
|
|
|
783
900
|
const bindGroupLayout = device.createBindGroupLayout({
|
|
784
|
-
entries: createBindGroupLayoutEntries(
|
|
901
|
+
entries: createBindGroupLayoutEntries(fragmentTextureBindings)
|
|
785
902
|
});
|
|
786
903
|
const fragmentStorageBindGroupLayout =
|
|
787
904
|
storageBufferKeys.length > 0
|
|
@@ -977,7 +1094,9 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
977
1094
|
viewA: textureA.createView(),
|
|
978
1095
|
textureB,
|
|
979
1096
|
viewB: textureB.createView(),
|
|
980
|
-
bindGroupLayout
|
|
1097
|
+
bindGroupLayout,
|
|
1098
|
+
readAWriteBBindGroup: null,
|
|
1099
|
+
readBWriteABindGroup: null
|
|
981
1100
|
};
|
|
982
1101
|
pingPongTexturePairs.set(target, pair);
|
|
983
1102
|
return pair;
|
|
@@ -1028,8 +1147,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1028
1147
|
const isPingPongPipeline = Boolean(
|
|
1029
1148
|
buildOptions.pingPongTarget && buildOptions.pingPongFormat
|
|
1030
1149
|
);
|
|
1031
|
-
const
|
|
1032
|
-
?
|
|
1150
|
+
const builtComputeShader = isPingPongPipeline
|
|
1151
|
+
? buildPingPongComputeShaderSourceWithMap({
|
|
1033
1152
|
compute: buildOptions.computeSource,
|
|
1034
1153
|
uniformLayout: options.uniformLayout,
|
|
1035
1154
|
storageBufferKeys,
|
|
@@ -1037,7 +1156,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1037
1156
|
target: buildOptions.pingPongTarget!,
|
|
1038
1157
|
targetFormat: buildOptions.pingPongFormat!
|
|
1039
1158
|
})
|
|
1040
|
-
:
|
|
1159
|
+
: buildComputeShaderSourceWithMap({
|
|
1041
1160
|
compute: buildOptions.computeSource,
|
|
1042
1161
|
uniformLayout: options.uniformLayout,
|
|
1043
1162
|
storageBufferKeys,
|
|
@@ -1046,7 +1165,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1046
1165
|
storageTextureDefinitions: storageTextureDefs
|
|
1047
1166
|
});
|
|
1048
1167
|
|
|
1049
|
-
const computeShaderModule = device.createShaderModule({ code:
|
|
1168
|
+
const computeShaderModule = device.createShaderModule({ code: builtComputeShader.code });
|
|
1050
1169
|
const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
|
|
1051
1170
|
|
|
1052
1171
|
// Compute bind group layout: group(0)=uniforms, group(1)=storage buffers, group(2)=storage textures
|
|
@@ -1065,20 +1184,9 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1065
1184
|
]
|
|
1066
1185
|
});
|
|
1067
1186
|
|
|
1068
|
-
const storageBGLEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map((key, index) => {
|
|
1069
|
-
const def = storageBufferDefinitions[key];
|
|
1070
|
-
const access = def?.access ?? 'read-write';
|
|
1071
|
-
const bufferType: GPUBufferBindingType =
|
|
1072
|
-
access === 'read' ? 'read-only-storage' : 'storage';
|
|
1073
|
-
return {
|
|
1074
|
-
binding: index,
|
|
1075
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
1076
|
-
buffer: { type: bufferType }
|
|
1077
|
-
};
|
|
1078
|
-
});
|
|
1079
1187
|
const storageBGL =
|
|
1080
|
-
|
|
1081
|
-
? device.createBindGroupLayout({ entries:
|
|
1188
|
+
computeStorageBufferLayoutEntries.length > 0
|
|
1189
|
+
? device.createBindGroupLayout({ entries: computeStorageBufferLayoutEntries })
|
|
1082
1190
|
: null;
|
|
1083
1191
|
|
|
1084
1192
|
const storageTextureBGLEntries: GPUBindGroupLayoutEntry[] = isPingPongPipeline
|
|
@@ -1104,18 +1212,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1104
1212
|
}
|
|
1105
1213
|
}
|
|
1106
1214
|
]
|
|
1107
|
-
:
|
|
1108
|
-
const texDef = options.textureDefinitions[key];
|
|
1109
|
-
return {
|
|
1110
|
-
binding: index,
|
|
1111
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
1112
|
-
storageTexture: {
|
|
1113
|
-
access: 'write-only' as GPUStorageTextureAccess,
|
|
1114
|
-
format: (texDef?.format ?? 'rgba8unorm') as GPUTextureFormat,
|
|
1115
|
-
viewDimension: '2d'
|
|
1116
|
-
}
|
|
1117
|
-
};
|
|
1118
|
-
});
|
|
1215
|
+
: computeStorageTextureLayoutEntries;
|
|
1119
1216
|
const storageTextureBGL =
|
|
1120
1217
|
storageTextureBGLEntries.length > 0
|
|
1121
1218
|
? device.createBindGroupLayout({ entries: storageTextureBGLEntries })
|
|
@@ -1133,13 +1230,23 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1133
1230
|
}
|
|
1134
1231
|
|
|
1135
1232
|
const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1233
|
+
let pipeline: GPUComputePipeline;
|
|
1234
|
+
try {
|
|
1235
|
+
pipeline = device.createComputePipeline({
|
|
1236
|
+
layout: computePipelineLayout,
|
|
1237
|
+
compute: {
|
|
1238
|
+
module: computeShaderModule,
|
|
1239
|
+
entryPoint: 'compute'
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
throw toComputeCompilationError({
|
|
1244
|
+
error,
|
|
1245
|
+
lineMap: builtComputeShader.lineMap,
|
|
1246
|
+
computeSource: buildOptions.computeSource,
|
|
1247
|
+
runtimeContext
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1143
1250
|
|
|
1144
1251
|
// Build uniform bind group for compute (group 0)
|
|
1145
1252
|
const computeUniformBindGroup = device.createBindGroup({
|
|
@@ -1162,63 +1269,49 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1162
1269
|
|
|
1163
1270
|
// Helper to get the storage bind group for dispatch
|
|
1164
1271
|
const getComputeStorageBindGroup = (): GPUBindGroup | null => {
|
|
1165
|
-
if (
|
|
1272
|
+
if (computeStorageBufferLayoutEntries.length === 0) {
|
|
1166
1273
|
return null;
|
|
1167
1274
|
}
|
|
1168
|
-
|
|
1169
|
-
const storageBGLEntries: GPUBindGroupLayoutEntry[] = storageBufferKeys.map((key, index) => {
|
|
1170
|
-
const def = storageBufferDefinitions[key];
|
|
1171
|
-
const access = def?.access ?? 'read-write';
|
|
1172
|
-
const bufferType: GPUBufferBindingType =
|
|
1173
|
-
access === 'read' ? 'read-only-storage' : 'storage';
|
|
1174
|
-
return {
|
|
1175
|
-
binding: index,
|
|
1176
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
1177
|
-
buffer: { type: bufferType }
|
|
1178
|
-
};
|
|
1179
|
-
});
|
|
1180
|
-
const storageBGL = device.createBindGroupLayout({ entries: storageBGLEntries });
|
|
1181
|
-
const storageEntries: GPUBindGroupEntry[] = storageBufferKeys.map((key, index) => {
|
|
1275
|
+
const resources: GPUBuffer[] = storageBufferKeys.map((key) => {
|
|
1182
1276
|
const buffer = storageBufferMap.get(key);
|
|
1183
1277
|
if (!buffer) {
|
|
1184
1278
|
throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
1185
1279
|
}
|
|
1280
|
+
return buffer;
|
|
1281
|
+
});
|
|
1282
|
+
const storageEntries: GPUBindGroupEntry[] = resources.map((buffer, index) => {
|
|
1186
1283
|
return { binding: index, resource: { buffer } };
|
|
1187
1284
|
});
|
|
1188
|
-
return
|
|
1189
|
-
|
|
1190
|
-
|
|
1285
|
+
return computeStorageBufferBindGroupCache.getOrCreate({
|
|
1286
|
+
topologyKey: computeStorageBufferTopologyKey,
|
|
1287
|
+
layoutEntries: computeStorageBufferLayoutEntries,
|
|
1288
|
+
bindGroupEntries: storageEntries,
|
|
1289
|
+
resourceRefs: resources
|
|
1191
1290
|
});
|
|
1192
1291
|
};
|
|
1193
1292
|
|
|
1194
1293
|
// Helper to get the storage texture bind group for compute dispatch (group 2)
|
|
1195
1294
|
const getComputeStorageTextureBindGroup = (): GPUBindGroup | null => {
|
|
1196
|
-
if (
|
|
1295
|
+
if (computeStorageTextureLayoutEntries.length === 0) {
|
|
1197
1296
|
return null;
|
|
1198
1297
|
}
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1201
|
-
return {
|
|
1202
|
-
binding: index,
|
|
1203
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
1204
|
-
storageTexture: {
|
|
1205
|
-
access: 'write-only' as GPUStorageTextureAccess,
|
|
1206
|
-
format: (texDef?.format ?? 'rgba8unorm') as GPUTextureFormat,
|
|
1207
|
-
viewDimension: '2d' as GPUTextureViewDimension
|
|
1208
|
-
}
|
|
1209
|
-
};
|
|
1210
|
-
});
|
|
1211
|
-
const bgl = device.createBindGroupLayout({ entries });
|
|
1212
|
-
|
|
1213
|
-
const bgEntries: GPUBindGroupEntry[] = storageTextureKeys.map((key, index) => {
|
|
1214
|
-
const binding = textureBindings.find((b) => b.key === key);
|
|
1298
|
+
const resources: GPUTextureView[] = storageTextureKeys.map((key) => {
|
|
1299
|
+
const binding = textureBindingByKey.get(key);
|
|
1215
1300
|
if (!binding || !binding.texture) {
|
|
1216
1301
|
throw new Error(`Storage texture "${key}" not allocated.`);
|
|
1217
1302
|
}
|
|
1218
|
-
return
|
|
1303
|
+
return binding.view;
|
|
1304
|
+
});
|
|
1305
|
+
const bgEntries: GPUBindGroupEntry[] = resources.map((view, index) => {
|
|
1306
|
+
return { binding: index, resource: view };
|
|
1219
1307
|
});
|
|
1220
1308
|
|
|
1221
|
-
return
|
|
1309
|
+
return computeStorageTextureBindGroupCache.getOrCreate({
|
|
1310
|
+
topologyKey: computeStorageTextureTopologyKey,
|
|
1311
|
+
layoutEntries: computeStorageTextureLayoutEntries,
|
|
1312
|
+
bindGroupEntries: bgEntries,
|
|
1313
|
+
resourceRefs: resources
|
|
1314
|
+
});
|
|
1222
1315
|
};
|
|
1223
1316
|
|
|
1224
1317
|
// Helper to get ping-pong storage texture bind group for compute dispatch (group 2)
|
|
@@ -1227,15 +1320,28 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1227
1320
|
readFromA: boolean
|
|
1228
1321
|
): GPUBindGroup => {
|
|
1229
1322
|
const pair = ensurePingPongTexturePair(target);
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1323
|
+
if (readFromA) {
|
|
1324
|
+
if (!pair.readAWriteBBindGroup) {
|
|
1325
|
+
pair.readAWriteBBindGroup = device.createBindGroup({
|
|
1326
|
+
layout: pair.bindGroupLayout,
|
|
1327
|
+
entries: [
|
|
1328
|
+
{ binding: 0, resource: pair.viewA },
|
|
1329
|
+
{ binding: 1, resource: pair.viewB }
|
|
1330
|
+
]
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
return pair.readAWriteBBindGroup;
|
|
1334
|
+
}
|
|
1335
|
+
if (!pair.readBWriteABindGroup) {
|
|
1336
|
+
pair.readBWriteABindGroup = device.createBindGroup({
|
|
1337
|
+
layout: pair.bindGroupLayout,
|
|
1338
|
+
entries: [
|
|
1339
|
+
{ binding: 0, resource: pair.viewB },
|
|
1340
|
+
{ binding: 1, resource: pair.viewA }
|
|
1341
|
+
]
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
return pair.readBWriteABindGroup;
|
|
1239
1345
|
};
|
|
1240
1346
|
|
|
1241
1347
|
const frameBuffer = device.createBuffer({
|
|
@@ -1267,7 +1373,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1267
1373
|
{ binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
|
|
1268
1374
|
];
|
|
1269
1375
|
|
|
1270
|
-
for (const binding of
|
|
1376
|
+
for (const binding of fragmentTextureBindings) {
|
|
1271
1377
|
entries.push({
|
|
1272
1378
|
binding: binding.samplerBinding,
|
|
1273
1379
|
resource: binding.sampler
|
|
@@ -1839,7 +1945,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1839
1945
|
if (storageTextureKeySet.has(binding.key)) continue;
|
|
1840
1946
|
const nextTexture =
|
|
1841
1947
|
textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
|
|
1842
|
-
if (updateTextureBinding(binding, nextTexture, renderMode)) {
|
|
1948
|
+
if (updateTextureBinding(binding, nextTexture, renderMode) && binding.fragmentVisible) {
|
|
1843
1949
|
bindGroupDirty = true;
|
|
1844
1950
|
}
|
|
1845
1951
|
}
|
|
@@ -54,6 +54,8 @@ function getRendererRetryDelayMs(attempt: number): number {
|
|
|
54
54
|
return Math.min(8000, 250 * 2 ** Math.max(0, attempt - 1));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const ERROR_CLEAR_GRACE_MS = 750;
|
|
58
|
+
|
|
57
59
|
export function createMotionGPURuntimeLoop(
|
|
58
60
|
options: MotionGPURuntimeLoopOptions
|
|
59
61
|
): MotionGPURuntimeLoop {
|
|
@@ -90,6 +92,16 @@ export function createMotionGPURuntimeLoop(
|
|
|
90
92
|
let shouldContinueAfterFrame = false;
|
|
91
93
|
let activeErrorKey: string | null = null;
|
|
92
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
|
+
};
|
|
93
105
|
|
|
94
106
|
const getHistoryLimit = (): number => {
|
|
95
107
|
const value = options.getErrorHistoryLimit?.() ?? 0;
|
|
@@ -129,12 +141,13 @@ export function createMotionGPURuntimeLoop(
|
|
|
129
141
|
return;
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
errorHistory
|
|
144
|
+
errorHistory.splice(0, errorHistory.length - limit);
|
|
133
145
|
publishErrorHistory();
|
|
134
146
|
};
|
|
135
147
|
|
|
136
|
-
const setError = (error: unknown, phase: MotionGPUErrorPhase): void => {
|
|
148
|
+
const setError = (error: unknown, phase: MotionGPUErrorPhase, nowMs?: number): void => {
|
|
137
149
|
const report = toMotionGPUErrorReport(error, phase);
|
|
150
|
+
errorClearReadyAtMs = resolveNowMs(nowMs) + ERROR_CLEAR_GRACE_MS;
|
|
138
151
|
const reportKey = JSON.stringify({
|
|
139
152
|
phase: report.phase,
|
|
140
153
|
title: report.title,
|
|
@@ -147,9 +160,9 @@ export function createMotionGPURuntimeLoop(
|
|
|
147
160
|
activeErrorKey = reportKey;
|
|
148
161
|
const historyLimit = getHistoryLimit();
|
|
149
162
|
if (historyLimit > 0) {
|
|
150
|
-
errorHistory
|
|
163
|
+
errorHistory.push(report);
|
|
151
164
|
if (errorHistory.length > historyLimit) {
|
|
152
|
-
errorHistory
|
|
165
|
+
errorHistory.splice(0, errorHistory.length - historyLimit);
|
|
153
166
|
}
|
|
154
167
|
publishErrorHistory();
|
|
155
168
|
}
|
|
@@ -166,12 +179,16 @@ export function createMotionGPURuntimeLoop(
|
|
|
166
179
|
}
|
|
167
180
|
};
|
|
168
181
|
|
|
169
|
-
const
|
|
182
|
+
const maybeClearError = (nowMs?: number): void => {
|
|
170
183
|
if (activeErrorKey === null) {
|
|
171
184
|
return;
|
|
172
185
|
}
|
|
186
|
+
if (resolveNowMs(nowMs) < errorClearReadyAtMs) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
173
189
|
|
|
174
190
|
activeErrorKey = null;
|
|
191
|
+
errorClearReadyAtMs = 0;
|
|
175
192
|
options.reportError(null);
|
|
176
193
|
};
|
|
177
194
|
|
|
@@ -200,13 +217,13 @@ export function createMotionGPURuntimeLoop(
|
|
|
200
217
|
const resetRuntimeMaps = (): void => {
|
|
201
218
|
for (const key of Object.keys(runtimeUniforms)) {
|
|
202
219
|
if (!uniformKeySet.has(key)) {
|
|
203
|
-
|
|
220
|
+
delete runtimeUniforms[key];
|
|
204
221
|
}
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
for (const key of Object.keys(runtimeTextures)) {
|
|
208
225
|
if (!textureKeySet.has(key)) {
|
|
209
|
-
|
|
226
|
+
delete runtimeTextures[key];
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
};
|
|
@@ -214,13 +231,13 @@ export function createMotionGPURuntimeLoop(
|
|
|
214
231
|
const resetRenderPayloadMaps = (): void => {
|
|
215
232
|
for (const key of Object.keys(renderUniforms)) {
|
|
216
233
|
if (!uniformKeySet.has(key)) {
|
|
217
|
-
|
|
234
|
+
delete renderUniforms[key];
|
|
218
235
|
}
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
for (const key of Object.keys(renderTextures)) {
|
|
222
239
|
if (!textureKeySet.has(key)) {
|
|
223
|
-
|
|
240
|
+
delete renderTextures[key];
|
|
224
241
|
}
|
|
225
242
|
}
|
|
226
243
|
};
|
|
@@ -345,13 +362,14 @@ export function createMotionGPURuntimeLoop(
|
|
|
345
362
|
if (isDisposed) {
|
|
346
363
|
return;
|
|
347
364
|
}
|
|
365
|
+
lastFrameTimestampMs = timestamp;
|
|
348
366
|
syncErrorHistory();
|
|
349
367
|
|
|
350
368
|
let materialState: ResolvedMaterial;
|
|
351
369
|
try {
|
|
352
370
|
materialState = resolveActiveMaterial();
|
|
353
371
|
} catch (error) {
|
|
354
|
-
setError(error, 'initialization');
|
|
372
|
+
setError(error, 'initialization', timestamp);
|
|
355
373
|
scheduleFrame();
|
|
356
374
|
return;
|
|
357
375
|
}
|
|
@@ -418,7 +436,7 @@ export function createMotionGPURuntimeLoop(
|
|
|
418
436
|
failedRendererSignature = null;
|
|
419
437
|
failedRendererAttempts = 0;
|
|
420
438
|
nextRendererRetryAt = 0;
|
|
421
|
-
|
|
439
|
+
maybeClearError(performance.now());
|
|
422
440
|
} catch (error) {
|
|
423
441
|
failedRendererSignature = rendererSignature;
|
|
424
442
|
failedRendererAttempts += 1;
|
|
@@ -490,15 +508,18 @@ export function createMotionGPURuntimeLoop(
|
|
|
490
508
|
uniforms: renderUniforms,
|
|
491
509
|
textures: renderTextures,
|
|
492
510
|
canvasSize,
|
|
493
|
-
|
|
494
|
-
? { pendingStorageWrites: pendingStorageWrites.splice(0) }
|
|
495
|
-
: {})
|
|
511
|
+
pendingStorageWrites: pendingStorageWrites.length > 0 ? pendingStorageWrites : undefined
|
|
496
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
|
+
}
|
|
497
518
|
}
|
|
498
519
|
|
|
499
|
-
|
|
520
|
+
maybeClearError(timestamp);
|
|
500
521
|
} catch (error) {
|
|
501
|
-
setError(error, 'render');
|
|
522
|
+
setError(error, 'render', timestamp);
|
|
502
523
|
} finally {
|
|
503
524
|
registry.endFrame();
|
|
504
525
|
}
|