@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
package/src/lib/core/renderer.ts
CHANGED
|
@@ -17,7 +17,16 @@ import {
|
|
|
17
17
|
toTextureData
|
|
18
18
|
} from './textures.js';
|
|
19
19
|
import { packUniformsInto } from './uniforms.js';
|
|
20
|
+
import {
|
|
21
|
+
buildComputeShaderSourceWithMap,
|
|
22
|
+
buildPingPongComputeShaderSourceWithMap,
|
|
23
|
+
extractWorkgroupSize,
|
|
24
|
+
storageTextureSampleScalarType
|
|
25
|
+
} from './compute-shader.js';
|
|
26
|
+
import { createComputeStorageBindGroupCache } from './compute-bindgroup-cache.js';
|
|
27
|
+
import { normalizeStorageBufferDefinition } from './storage-buffers.js';
|
|
20
28
|
import type {
|
|
29
|
+
AnyPass,
|
|
21
30
|
RenderPass,
|
|
22
31
|
RenderPassInputSlot,
|
|
23
32
|
RenderPassOutputSlot,
|
|
@@ -25,6 +34,8 @@ import type {
|
|
|
25
34
|
RenderTarget,
|
|
26
35
|
Renderer,
|
|
27
36
|
RendererOptions,
|
|
37
|
+
StorageBufferAccess,
|
|
38
|
+
StorageBufferType,
|
|
28
39
|
TextureSource,
|
|
29
40
|
TextureUpdateMode,
|
|
30
41
|
TextureValue
|
|
@@ -52,6 +63,7 @@ interface RuntimeTextureBinding {
|
|
|
52
63
|
key: string;
|
|
53
64
|
samplerBinding: number;
|
|
54
65
|
textureBinding: number;
|
|
66
|
+
fragmentVisible: boolean;
|
|
55
67
|
sampler: GPUSampler;
|
|
56
68
|
fallbackTexture: GPUTexture;
|
|
57
69
|
fallbackView: GPUTextureView;
|
|
@@ -86,11 +98,28 @@ interface RuntimeRenderTarget {
|
|
|
86
98
|
format: GPUTextureFormat;
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Runtime ping-pong storage textures for a single logical target key.
|
|
103
|
+
*/
|
|
104
|
+
interface PingPongTexturePair {
|
|
105
|
+
target: string;
|
|
106
|
+
format: GPUTextureFormat;
|
|
107
|
+
width: number;
|
|
108
|
+
height: number;
|
|
109
|
+
textureA: GPUTexture;
|
|
110
|
+
viewA: GPUTextureView;
|
|
111
|
+
textureB: GPUTexture;
|
|
112
|
+
viewB: GPUTextureView;
|
|
113
|
+
bindGroupLayout: GPUBindGroupLayout;
|
|
114
|
+
readAWriteBBindGroup: GPUBindGroup | null;
|
|
115
|
+
readBWriteABindGroup: GPUBindGroup | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
/**
|
|
90
119
|
* Cached pass properties used to validate render-graph cache correctness.
|
|
91
120
|
*/
|
|
92
121
|
interface RenderGraphPassSnapshot {
|
|
93
|
-
pass:
|
|
122
|
+
pass: AnyPass;
|
|
94
123
|
enabled: RenderPass['enabled'];
|
|
95
124
|
needsSwap: RenderPass['needsSwap'];
|
|
96
125
|
input: RenderPass['input'];
|
|
@@ -118,6 +147,19 @@ function getTextureBindings(index: number): {
|
|
|
118
147
|
};
|
|
119
148
|
}
|
|
120
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Maps WGSL scalar texture type to WebGPU sampled texture bind-group sample type.
|
|
152
|
+
*/
|
|
153
|
+
function toGpuTextureSampleType(type: 'f32' | 'u32' | 'i32'): GPUTextureSampleType {
|
|
154
|
+
if (type === 'u32') {
|
|
155
|
+
return 'uint';
|
|
156
|
+
}
|
|
157
|
+
if (type === 'i32') {
|
|
158
|
+
return 'sint';
|
|
159
|
+
}
|
|
160
|
+
return 'float';
|
|
161
|
+
}
|
|
162
|
+
|
|
121
163
|
/**
|
|
122
164
|
* Resizes canvas backing store to match client size and DPR.
|
|
123
165
|
*/
|
|
@@ -149,6 +191,7 @@ async function assertCompilation(
|
|
|
149
191
|
options?: {
|
|
150
192
|
lineMap?: ShaderLineMap;
|
|
151
193
|
fragmentSource?: string;
|
|
194
|
+
computeSource?: string;
|
|
152
195
|
includeSources?: Record<string, string>;
|
|
153
196
|
defineBlockSource?: string;
|
|
154
197
|
materialSource?: {
|
|
@@ -159,6 +202,8 @@ async function assertCompilation(
|
|
|
159
202
|
functionName?: string;
|
|
160
203
|
} | null;
|
|
161
204
|
runtimeContext?: ShaderCompilationRuntimeContext;
|
|
205
|
+
errorPrefix?: string;
|
|
206
|
+
shaderStage?: 'fragment' | 'compute';
|
|
162
207
|
}
|
|
163
208
|
): Promise<void> {
|
|
164
209
|
const info = await module.getCompilationInfo();
|
|
@@ -189,11 +234,14 @@ async function assertCompilation(
|
|
|
189
234
|
return `[${contextLabel.join(' | ')}] ${diagnostic.message}`;
|
|
190
235
|
})
|
|
191
236
|
.join('\n');
|
|
192
|
-
const
|
|
237
|
+
const prefix = options?.errorPrefix ?? 'WGSL compilation failed';
|
|
238
|
+
const error = new Error(`${prefix}:\n${summary}`);
|
|
193
239
|
throw attachShaderCompilationDiagnostics(error, {
|
|
194
240
|
kind: 'shader-compilation',
|
|
241
|
+
...(options?.shaderStage !== undefined ? { shaderStage: options.shaderStage } : {}),
|
|
195
242
|
diagnostics,
|
|
196
243
|
fragmentSource: options?.fragmentSource ?? '',
|
|
244
|
+
...(options?.computeSource !== undefined ? { computeSource: options.computeSource } : {}),
|
|
197
245
|
includeSources: options?.includeSources ?? {},
|
|
198
246
|
...(options?.defineBlockSource !== undefined
|
|
199
247
|
? { defineBlockSource: options.defineBlockSource }
|
|
@@ -207,8 +255,66 @@ function toSortedUniqueStrings(values: string[]): string[] {
|
|
|
207
255
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
208
256
|
}
|
|
209
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
|
+
|
|
210
316
|
function buildPassGraphSnapshot(
|
|
211
|
-
passes:
|
|
317
|
+
passes: AnyPass[] | undefined
|
|
212
318
|
): NonNullable<ShaderCompilationRuntimeContext['passGraph']> {
|
|
213
319
|
const declaredPasses = passes ?? [];
|
|
214
320
|
let enabledPassCount = 0;
|
|
@@ -221,9 +327,13 @@ function buildPassGraphSnapshot(
|
|
|
221
327
|
}
|
|
222
328
|
|
|
223
329
|
enabledPassCount += 1;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
330
|
+
if ('isCompute' in pass && (pass as { isCompute?: boolean }).isCompute === true) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const rp = pass as RenderPass;
|
|
334
|
+
const needsSwap = rp.needsSwap ?? true;
|
|
335
|
+
const input = rp.input ?? 'source';
|
|
336
|
+
const output = rp.output ?? (needsSwap ? 'target' : 'source');
|
|
227
337
|
inputs.push(input);
|
|
228
338
|
outputs.push(output);
|
|
229
339
|
}
|
|
@@ -625,13 +735,22 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
625
735
|
try {
|
|
626
736
|
const runtimeContext = buildShaderCompilationRuntimeContext(options);
|
|
627
737
|
const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
|
|
738
|
+
const fragmentTextureKeys = options.textureKeys.filter(
|
|
739
|
+
(key) => options.textureDefinitions[key]?.fragmentVisible !== false
|
|
740
|
+
);
|
|
628
741
|
const builtShader = buildShaderSourceWithMap(
|
|
629
742
|
options.fragmentWgsl,
|
|
630
743
|
options.uniformLayout,
|
|
631
|
-
|
|
744
|
+
fragmentTextureKeys,
|
|
632
745
|
{
|
|
633
746
|
convertLinearToSrgb,
|
|
634
|
-
fragmentLineMap: options.fragmentLineMap
|
|
747
|
+
fragmentLineMap: options.fragmentLineMap,
|
|
748
|
+
...(options.storageBufferKeys !== undefined
|
|
749
|
+
? { storageBufferKeys: options.storageBufferKeys }
|
|
750
|
+
: {}),
|
|
751
|
+
...(options.storageBufferDefinitions !== undefined
|
|
752
|
+
? { storageBufferDefinitions: options.storageBufferDefinitions }
|
|
753
|
+
: {})
|
|
635
754
|
}
|
|
636
755
|
);
|
|
637
756
|
const shaderModule = device.createShaderModule({ code: builtShader.code });
|
|
@@ -650,13 +769,22 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
650
769
|
options.textureDefinitions,
|
|
651
770
|
options.textureKeys
|
|
652
771
|
);
|
|
653
|
-
const
|
|
772
|
+
const storageBufferKeys = options.storageBufferKeys ?? [];
|
|
773
|
+
const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
|
|
774
|
+
const storageTextureKeys = options.storageTextureKeys ?? [];
|
|
775
|
+
const storageTextureKeySet = new Set(storageTextureKeys);
|
|
776
|
+
const fragmentTextureIndexByKey = new Map(
|
|
777
|
+
fragmentTextureKeys.map((key, index) => [key, index] as const)
|
|
778
|
+
);
|
|
779
|
+
const textureBindings = options.textureKeys.map((key): RuntimeTextureBinding => {
|
|
654
780
|
const config = normalizedTextureDefinitions[key];
|
|
655
781
|
if (!config) {
|
|
656
782
|
throw new Error(`Missing texture definition for "${key}"`);
|
|
657
783
|
}
|
|
658
784
|
|
|
659
|
-
const
|
|
785
|
+
const fragmentTextureIndex = fragmentTextureIndexByKey.get(key);
|
|
786
|
+
const fragmentVisible = fragmentTextureIndex !== undefined;
|
|
787
|
+
const { samplerBinding, textureBinding } = getTextureBindings(fragmentTextureIndex ?? 0);
|
|
660
788
|
const sampler = device.createSampler({
|
|
661
789
|
magFilter: config.filter,
|
|
662
790
|
minFilter: config.filter,
|
|
@@ -665,7 +793,11 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
665
793
|
addressModeV: config.addressModeV,
|
|
666
794
|
maxAnisotropy: config.filter === 'linear' ? config.anisotropy : 1
|
|
667
795
|
});
|
|
668
|
-
|
|
796
|
+
// Storage textures use a safe fallback format — the fallback is never
|
|
797
|
+
// sampled because storage textures are eagerly allocated with their
|
|
798
|
+
// real format/dimensions. Non-storage textures use their own format.
|
|
799
|
+
const fallbackFormat = config.storage ? 'rgba8unorm' : config.format;
|
|
800
|
+
const fallbackTexture = createFallbackTexture(device, fallbackFormat);
|
|
669
801
|
registerInitializationCleanup(() => {
|
|
670
802
|
fallbackTexture.destroy();
|
|
671
803
|
});
|
|
@@ -675,6 +807,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
675
807
|
key,
|
|
676
808
|
samplerBinding,
|
|
677
809
|
textureBinding,
|
|
810
|
+
fragmentVisible,
|
|
678
811
|
sampler,
|
|
679
812
|
fallbackTexture,
|
|
680
813
|
fallbackView,
|
|
@@ -701,14 +834,86 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
701
834
|
runtimeBinding.defaultUpdate = config.update;
|
|
702
835
|
}
|
|
703
836
|
|
|
837
|
+
// Storage textures: eagerly create GPU texture with explicit dimensions
|
|
838
|
+
if (config.storage && config.width && config.height) {
|
|
839
|
+
const storageUsage =
|
|
840
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
841
|
+
GPUTextureUsage.STORAGE_BINDING |
|
|
842
|
+
GPUTextureUsage.COPY_DST;
|
|
843
|
+
const storageTexture = device.createTexture({
|
|
844
|
+
size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
|
|
845
|
+
format: config.format,
|
|
846
|
+
usage: storageUsage
|
|
847
|
+
});
|
|
848
|
+
registerInitializationCleanup(() => {
|
|
849
|
+
storageTexture.destroy();
|
|
850
|
+
});
|
|
851
|
+
runtimeBinding.texture = storageTexture as unknown as GPUTexture;
|
|
852
|
+
runtimeBinding.view = storageTexture.createView();
|
|
853
|
+
runtimeBinding.width = config.width;
|
|
854
|
+
runtimeBinding.height = config.height;
|
|
855
|
+
}
|
|
856
|
+
|
|
704
857
|
return runtimeBinding;
|
|
705
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);
|
|
706
899
|
|
|
707
900
|
const bindGroupLayout = device.createBindGroupLayout({
|
|
708
|
-
entries: createBindGroupLayoutEntries(
|
|
901
|
+
entries: createBindGroupLayoutEntries(fragmentTextureBindings)
|
|
709
902
|
});
|
|
903
|
+
const fragmentStorageBindGroupLayout =
|
|
904
|
+
storageBufferKeys.length > 0
|
|
905
|
+
? device.createBindGroupLayout({
|
|
906
|
+
entries: storageBufferKeys.map((_, index) => ({
|
|
907
|
+
binding: index,
|
|
908
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
909
|
+
buffer: { type: 'read-only-storage' as GPUBufferBindingType }
|
|
910
|
+
}))
|
|
911
|
+
})
|
|
912
|
+
: null;
|
|
710
913
|
const pipelineLayout = device.createPipelineLayout({
|
|
711
|
-
bindGroupLayouts:
|
|
914
|
+
bindGroupLayouts: fragmentStorageBindGroupLayout
|
|
915
|
+
? [bindGroupLayout, fragmentStorageBindGroupLayout]
|
|
916
|
+
: [bindGroupLayout]
|
|
712
917
|
});
|
|
713
918
|
|
|
714
919
|
const pipeline = device.createRenderPipeline({
|
|
@@ -773,6 +978,372 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
773
978
|
});
|
|
774
979
|
let blitBindGroupByView = new WeakMap<GPUTextureView, GPUBindGroup>();
|
|
775
980
|
|
|
981
|
+
// ── Storage buffer allocation ────────────────────────────────────────
|
|
982
|
+
const storageBufferMap = new Map<string, GPUBuffer>();
|
|
983
|
+
const pingPongTexturePairs = new Map<string, PingPongTexturePair>();
|
|
984
|
+
|
|
985
|
+
for (const key of storageBufferKeys) {
|
|
986
|
+
const definition = storageBufferDefinitions[key];
|
|
987
|
+
if (!definition) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const normalized = normalizeStorageBufferDefinition(definition);
|
|
991
|
+
const buffer = device.createBuffer({
|
|
992
|
+
size: normalized.size,
|
|
993
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
994
|
+
});
|
|
995
|
+
registerInitializationCleanup(() => {
|
|
996
|
+
buffer.destroy();
|
|
997
|
+
});
|
|
998
|
+
if (definition.initialData) {
|
|
999
|
+
const data = definition.initialData;
|
|
1000
|
+
device.queue.writeBuffer(
|
|
1001
|
+
buffer,
|
|
1002
|
+
0,
|
|
1003
|
+
data.buffer as ArrayBuffer,
|
|
1004
|
+
data.byteOffset,
|
|
1005
|
+
data.byteLength
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
storageBufferMap.set(key, buffer);
|
|
1009
|
+
}
|
|
1010
|
+
const fragmentStorageBindGroup =
|
|
1011
|
+
fragmentStorageBindGroupLayout && storageBufferKeys.length > 0
|
|
1012
|
+
? device.createBindGroup({
|
|
1013
|
+
layout: fragmentStorageBindGroupLayout,
|
|
1014
|
+
entries: storageBufferKeys.map((key, index) => {
|
|
1015
|
+
const buffer = storageBufferMap.get(key);
|
|
1016
|
+
if (!buffer) {
|
|
1017
|
+
throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
1018
|
+
}
|
|
1019
|
+
return { binding: index, resource: { buffer } };
|
|
1020
|
+
})
|
|
1021
|
+
})
|
|
1022
|
+
: null;
|
|
1023
|
+
|
|
1024
|
+
const ensurePingPongTexturePair = (target: string): PingPongTexturePair => {
|
|
1025
|
+
const existing = pingPongTexturePairs.get(target);
|
|
1026
|
+
if (existing) {
|
|
1027
|
+
return existing;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const config = normalizedTextureDefinitions[target];
|
|
1031
|
+
if (!config || !config.storage) {
|
|
1032
|
+
throw new Error(
|
|
1033
|
+
`PingPongComputePass target "${target}" must reference a texture declared with storage:true.`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
if (!config.width || !config.height) {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
`PingPongComputePass target "${target}" requires explicit texture width and height.`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const usage =
|
|
1043
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
1044
|
+
GPUTextureUsage.STORAGE_BINDING |
|
|
1045
|
+
GPUTextureUsage.COPY_DST;
|
|
1046
|
+
const textureA = device.createTexture({
|
|
1047
|
+
size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
|
|
1048
|
+
format: config.format,
|
|
1049
|
+
usage
|
|
1050
|
+
});
|
|
1051
|
+
const textureB = device.createTexture({
|
|
1052
|
+
size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
|
|
1053
|
+
format: config.format,
|
|
1054
|
+
usage
|
|
1055
|
+
});
|
|
1056
|
+
registerInitializationCleanup(() => {
|
|
1057
|
+
textureA.destroy();
|
|
1058
|
+
});
|
|
1059
|
+
registerInitializationCleanup(() => {
|
|
1060
|
+
textureB.destroy();
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
const sampleScalarType = storageTextureSampleScalarType(config.format);
|
|
1064
|
+
const sampleType = toGpuTextureSampleType(sampleScalarType);
|
|
1065
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
1066
|
+
entries: [
|
|
1067
|
+
{
|
|
1068
|
+
binding: 0,
|
|
1069
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1070
|
+
texture: {
|
|
1071
|
+
sampleType,
|
|
1072
|
+
viewDimension: '2d',
|
|
1073
|
+
multisampled: false
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
binding: 1,
|
|
1078
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1079
|
+
storageTexture: {
|
|
1080
|
+
access: 'write-only' as GPUStorageTextureAccess,
|
|
1081
|
+
format: config.format as GPUTextureFormat,
|
|
1082
|
+
viewDimension: '2d'
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
]
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
const pair: PingPongTexturePair = {
|
|
1089
|
+
target,
|
|
1090
|
+
format: config.format as GPUTextureFormat,
|
|
1091
|
+
width: config.width,
|
|
1092
|
+
height: config.height,
|
|
1093
|
+
textureA,
|
|
1094
|
+
viewA: textureA.createView(),
|
|
1095
|
+
textureB,
|
|
1096
|
+
viewB: textureB.createView(),
|
|
1097
|
+
bindGroupLayout,
|
|
1098
|
+
readAWriteBBindGroup: null,
|
|
1099
|
+
readBWriteABindGroup: null
|
|
1100
|
+
};
|
|
1101
|
+
pingPongTexturePairs.set(target, pair);
|
|
1102
|
+
return pair;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// ── Compute pipeline setup ──────────────────────────────────────────
|
|
1106
|
+
interface ComputePipelineEntry {
|
|
1107
|
+
pipeline: GPUComputePipeline;
|
|
1108
|
+
bindGroup: GPUBindGroup;
|
|
1109
|
+
workgroupSize: [number, number, number];
|
|
1110
|
+
computeSource: string;
|
|
1111
|
+
}
|
|
1112
|
+
const computePipelineCache = new Map<string, ComputePipelineEntry>();
|
|
1113
|
+
|
|
1114
|
+
const buildComputePipelineEntry = (buildOptions: {
|
|
1115
|
+
computeSource: string;
|
|
1116
|
+
pingPongTarget?: string;
|
|
1117
|
+
pingPongFormat?: GPUTextureFormat;
|
|
1118
|
+
}): ComputePipelineEntry => {
|
|
1119
|
+
const cacheKey =
|
|
1120
|
+
buildOptions.pingPongTarget && buildOptions.pingPongFormat
|
|
1121
|
+
? `pingpong:${buildOptions.pingPongTarget}:${buildOptions.pingPongFormat}:${buildOptions.computeSource}`
|
|
1122
|
+
: `compute:${buildOptions.computeSource}`;
|
|
1123
|
+
const cached = computePipelineCache.get(cacheKey);
|
|
1124
|
+
if (cached) {
|
|
1125
|
+
return cached;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const storageBufferDefs: Record<
|
|
1129
|
+
string,
|
|
1130
|
+
{ type: StorageBufferType; access: StorageBufferAccess }
|
|
1131
|
+
> = {};
|
|
1132
|
+
for (const key of storageBufferKeys) {
|
|
1133
|
+
const def = storageBufferDefinitions[key];
|
|
1134
|
+
if (def) {
|
|
1135
|
+
const norm = normalizeStorageBufferDefinition(def);
|
|
1136
|
+
storageBufferDefs[key] = { type: norm.type, access: norm.access };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const storageTextureDefs: Record<string, { format: GPUTextureFormat }> = {};
|
|
1140
|
+
for (const key of storageTextureKeys) {
|
|
1141
|
+
const texDef = options.textureDefinitions[key];
|
|
1142
|
+
if (texDef?.format) {
|
|
1143
|
+
storageTextureDefs[key] = { format: texDef.format };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const isPingPongPipeline = Boolean(
|
|
1148
|
+
buildOptions.pingPongTarget && buildOptions.pingPongFormat
|
|
1149
|
+
);
|
|
1150
|
+
const builtComputeShader = isPingPongPipeline
|
|
1151
|
+
? buildPingPongComputeShaderSourceWithMap({
|
|
1152
|
+
compute: buildOptions.computeSource,
|
|
1153
|
+
uniformLayout: options.uniformLayout,
|
|
1154
|
+
storageBufferKeys,
|
|
1155
|
+
storageBufferDefinitions: storageBufferDefs,
|
|
1156
|
+
target: buildOptions.pingPongTarget!,
|
|
1157
|
+
targetFormat: buildOptions.pingPongFormat!
|
|
1158
|
+
})
|
|
1159
|
+
: buildComputeShaderSourceWithMap({
|
|
1160
|
+
compute: buildOptions.computeSource,
|
|
1161
|
+
uniformLayout: options.uniformLayout,
|
|
1162
|
+
storageBufferKeys,
|
|
1163
|
+
storageBufferDefinitions: storageBufferDefs,
|
|
1164
|
+
storageTextureKeys,
|
|
1165
|
+
storageTextureDefinitions: storageTextureDefs
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
const computeShaderModule = device.createShaderModule({ code: builtComputeShader.code });
|
|
1169
|
+
const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
|
|
1170
|
+
|
|
1171
|
+
// Compute bind group layout: group(0)=uniforms, group(1)=storage buffers, group(2)=storage textures
|
|
1172
|
+
const computeUniformBGL = device.createBindGroupLayout({
|
|
1173
|
+
entries: [
|
|
1174
|
+
{
|
|
1175
|
+
binding: FRAME_BINDING,
|
|
1176
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1177
|
+
buffer: { type: 'uniform', minBindingSize: 16 }
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
binding: UNIFORM_BINDING,
|
|
1181
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1182
|
+
buffer: { type: 'uniform' }
|
|
1183
|
+
}
|
|
1184
|
+
]
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
const storageBGL =
|
|
1188
|
+
computeStorageBufferLayoutEntries.length > 0
|
|
1189
|
+
? device.createBindGroupLayout({ entries: computeStorageBufferLayoutEntries })
|
|
1190
|
+
: null;
|
|
1191
|
+
|
|
1192
|
+
const storageTextureBGLEntries: GPUBindGroupLayoutEntry[] = isPingPongPipeline
|
|
1193
|
+
? [
|
|
1194
|
+
{
|
|
1195
|
+
binding: 0,
|
|
1196
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1197
|
+
texture: {
|
|
1198
|
+
sampleType: toGpuTextureSampleType(
|
|
1199
|
+
storageTextureSampleScalarType(buildOptions.pingPongFormat!)
|
|
1200
|
+
),
|
|
1201
|
+
viewDimension: '2d',
|
|
1202
|
+
multisampled: false
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
binding: 1,
|
|
1207
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1208
|
+
storageTexture: {
|
|
1209
|
+
access: 'write-only' as GPUStorageTextureAccess,
|
|
1210
|
+
format: buildOptions.pingPongFormat!,
|
|
1211
|
+
viewDimension: '2d'
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
]
|
|
1215
|
+
: computeStorageTextureLayoutEntries;
|
|
1216
|
+
const storageTextureBGL =
|
|
1217
|
+
storageTextureBGLEntries.length > 0
|
|
1218
|
+
? device.createBindGroupLayout({ entries: storageTextureBGLEntries })
|
|
1219
|
+
: null;
|
|
1220
|
+
|
|
1221
|
+
// Bind group layout indices must match shader @group() indices:
|
|
1222
|
+
// group(0) = uniforms, group(1) = storage buffers, group(2) = storage textures.
|
|
1223
|
+
// When a group is unused, insert an empty placeholder to keep indices aligned.
|
|
1224
|
+
const bindGroupLayouts: GPUBindGroupLayout[] = [computeUniformBGL];
|
|
1225
|
+
if (storageBGL || storageTextureBGL) {
|
|
1226
|
+
bindGroupLayouts.push(storageBGL ?? device.createBindGroupLayout({ entries: [] }));
|
|
1227
|
+
}
|
|
1228
|
+
if (storageTextureBGL) {
|
|
1229
|
+
bindGroupLayouts.push(storageTextureBGL);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
|
|
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
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Build uniform bind group for compute (group 0)
|
|
1252
|
+
const computeUniformBindGroup = device.createBindGroup({
|
|
1253
|
+
layout: computeUniformBGL,
|
|
1254
|
+
entries: [
|
|
1255
|
+
{ binding: FRAME_BINDING, resource: { buffer: frameBuffer } },
|
|
1256
|
+
{ binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
|
|
1257
|
+
]
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
const entry: ComputePipelineEntry = {
|
|
1261
|
+
pipeline,
|
|
1262
|
+
bindGroup: computeUniformBindGroup,
|
|
1263
|
+
workgroupSize,
|
|
1264
|
+
computeSource: buildOptions.computeSource
|
|
1265
|
+
};
|
|
1266
|
+
computePipelineCache.set(cacheKey, entry);
|
|
1267
|
+
return entry;
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
// Helper to get the storage bind group for dispatch
|
|
1271
|
+
const getComputeStorageBindGroup = (): GPUBindGroup | null => {
|
|
1272
|
+
if (computeStorageBufferLayoutEntries.length === 0) {
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
const resources: GPUBuffer[] = storageBufferKeys.map((key) => {
|
|
1276
|
+
const buffer = storageBufferMap.get(key);
|
|
1277
|
+
if (!buffer) {
|
|
1278
|
+
throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
1279
|
+
}
|
|
1280
|
+
return buffer;
|
|
1281
|
+
});
|
|
1282
|
+
const storageEntries: GPUBindGroupEntry[] = resources.map((buffer, index) => {
|
|
1283
|
+
return { binding: index, resource: { buffer } };
|
|
1284
|
+
});
|
|
1285
|
+
return computeStorageBufferBindGroupCache.getOrCreate({
|
|
1286
|
+
topologyKey: computeStorageBufferTopologyKey,
|
|
1287
|
+
layoutEntries: computeStorageBufferLayoutEntries,
|
|
1288
|
+
bindGroupEntries: storageEntries,
|
|
1289
|
+
resourceRefs: resources
|
|
1290
|
+
});
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
// Helper to get the storage texture bind group for compute dispatch (group 2)
|
|
1294
|
+
const getComputeStorageTextureBindGroup = (): GPUBindGroup | null => {
|
|
1295
|
+
if (computeStorageTextureLayoutEntries.length === 0) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
const resources: GPUTextureView[] = storageTextureKeys.map((key) => {
|
|
1299
|
+
const binding = textureBindingByKey.get(key);
|
|
1300
|
+
if (!binding || !binding.texture) {
|
|
1301
|
+
throw new Error(`Storage texture "${key}" not allocated.`);
|
|
1302
|
+
}
|
|
1303
|
+
return binding.view;
|
|
1304
|
+
});
|
|
1305
|
+
const bgEntries: GPUBindGroupEntry[] = resources.map((view, index) => {
|
|
1306
|
+
return { binding: index, resource: view };
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
return computeStorageTextureBindGroupCache.getOrCreate({
|
|
1310
|
+
topologyKey: computeStorageTextureTopologyKey,
|
|
1311
|
+
layoutEntries: computeStorageTextureLayoutEntries,
|
|
1312
|
+
bindGroupEntries: bgEntries,
|
|
1313
|
+
resourceRefs: resources
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// Helper to get ping-pong storage texture bind group for compute dispatch (group 2)
|
|
1318
|
+
const getPingPongStorageTextureBindGroup = (
|
|
1319
|
+
target: string,
|
|
1320
|
+
readFromA: boolean
|
|
1321
|
+
): GPUBindGroup => {
|
|
1322
|
+
const pair = ensurePingPongTexturePair(target);
|
|
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;
|
|
1345
|
+
};
|
|
1346
|
+
|
|
776
1347
|
const frameBuffer = device.createBuffer({
|
|
777
1348
|
size: 16,
|
|
778
1349
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
@@ -802,7 +1373,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
802
1373
|
{ binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
|
|
803
1374
|
];
|
|
804
1375
|
|
|
805
|
-
for (const binding of
|
|
1376
|
+
for (const binding of fragmentTextureBindings) {
|
|
806
1377
|
entries.push({
|
|
807
1378
|
binding: binding.samplerBinding,
|
|
808
1379
|
resource: binding.sampler
|
|
@@ -891,14 +1462,18 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
891
1462
|
return false;
|
|
892
1463
|
}
|
|
893
1464
|
|
|
1465
|
+
let textureUsage =
|
|
1466
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
1467
|
+
GPUTextureUsage.COPY_DST |
|
|
1468
|
+
GPUTextureUsage.RENDER_ATTACHMENT;
|
|
1469
|
+
if (storageTextureKeySet.has(binding.key)) {
|
|
1470
|
+
textureUsage |= GPUTextureUsage.STORAGE_BINDING;
|
|
1471
|
+
}
|
|
894
1472
|
const texture = device.createTexture({
|
|
895
1473
|
size: { width, height, depthOrArrayLayers: 1 },
|
|
896
1474
|
format,
|
|
897
1475
|
mipLevelCount,
|
|
898
|
-
usage:
|
|
899
|
-
GPUTextureUsage.TEXTURE_BINDING |
|
|
900
|
-
GPUTextureUsage.COPY_DST |
|
|
901
|
-
GPUTextureUsage.RENDER_ATTACHMENT
|
|
1476
|
+
usage: textureUsage
|
|
902
1477
|
});
|
|
903
1478
|
registerInitializationCleanup(() => {
|
|
904
1479
|
texture.destroy();
|
|
@@ -924,6 +1499,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
924
1499
|
};
|
|
925
1500
|
|
|
926
1501
|
for (const binding of textureBindings) {
|
|
1502
|
+
// Skip storage textures — they are eagerly allocated and not source-driven
|
|
1503
|
+
if (storageTextureKeySet.has(binding.key)) continue;
|
|
927
1504
|
const defaultSource = normalizedTextureDefinitions[binding.key]?.source ?? null;
|
|
928
1505
|
updateTextureBinding(binding, defaultSource, 'always');
|
|
929
1506
|
}
|
|
@@ -942,18 +1519,18 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
942
1519
|
let configuredWidth = 0;
|
|
943
1520
|
let configuredHeight = 0;
|
|
944
1521
|
const runtimeRenderTargets = new Map<string, RuntimeRenderTarget>();
|
|
945
|
-
const activePasses:
|
|
946
|
-
const lifecyclePreviousSet = new Set<
|
|
947
|
-
const lifecycleNextSet = new Set<
|
|
948
|
-
const lifecycleUniquePasses:
|
|
949
|
-
let lifecyclePassesRef:
|
|
1522
|
+
const activePasses: AnyPass[] = [];
|
|
1523
|
+
const lifecyclePreviousSet = new Set<AnyPass>();
|
|
1524
|
+
const lifecycleNextSet = new Set<AnyPass>();
|
|
1525
|
+
const lifecycleUniquePasses: AnyPass[] = [];
|
|
1526
|
+
let lifecyclePassesRef: AnyPass[] | null = null;
|
|
950
1527
|
let passWidth = 0;
|
|
951
1528
|
let passHeight = 0;
|
|
952
1529
|
|
|
953
1530
|
/**
|
|
954
1531
|
* Resolves active render pass list for current frame.
|
|
955
1532
|
*/
|
|
956
|
-
const resolvePasses = ():
|
|
1533
|
+
const resolvePasses = (): AnyPass[] => {
|
|
957
1534
|
return options.getPasses?.() ?? options.passes ?? [];
|
|
958
1535
|
};
|
|
959
1536
|
|
|
@@ -968,7 +1545,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
968
1545
|
* Checks whether cached render-graph plan can be reused for this frame.
|
|
969
1546
|
*/
|
|
970
1547
|
const isGraphPlanCacheValid = (
|
|
971
|
-
passes:
|
|
1548
|
+
passes: AnyPass[],
|
|
972
1549
|
clearColor: [number, number, number, number]
|
|
973
1550
|
): boolean => {
|
|
974
1551
|
if (!cachedGraphPlan) {
|
|
@@ -994,6 +1571,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
994
1571
|
|
|
995
1572
|
for (let index = 0; index < passes.length; index += 1) {
|
|
996
1573
|
const pass = passes[index];
|
|
1574
|
+
const rp = pass as Partial<RenderPass>;
|
|
997
1575
|
const snapshot = cachedGraphPasses[index];
|
|
998
1576
|
if (!pass || !snapshot || snapshot.pass !== pass) {
|
|
999
1577
|
return false;
|
|
@@ -1001,16 +1579,16 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1001
1579
|
|
|
1002
1580
|
if (
|
|
1003
1581
|
snapshot.enabled !== pass.enabled ||
|
|
1004
|
-
snapshot.needsSwap !==
|
|
1005
|
-
snapshot.input !==
|
|
1006
|
-
snapshot.output !==
|
|
1007
|
-
snapshot.clear !==
|
|
1008
|
-
snapshot.preserve !==
|
|
1582
|
+
snapshot.needsSwap !== rp.needsSwap ||
|
|
1583
|
+
snapshot.input !== rp.input ||
|
|
1584
|
+
snapshot.output !== rp.output ||
|
|
1585
|
+
snapshot.clear !== rp.clear ||
|
|
1586
|
+
snapshot.preserve !== rp.preserve
|
|
1009
1587
|
) {
|
|
1010
1588
|
return false;
|
|
1011
1589
|
}
|
|
1012
1590
|
|
|
1013
|
-
const passClearColor =
|
|
1591
|
+
const passClearColor = rp.clearColor;
|
|
1014
1592
|
const hasPassClearColor = passClearColor !== undefined;
|
|
1015
1593
|
if (snapshot.hasClearColor !== hasPassClearColor) {
|
|
1016
1594
|
return false;
|
|
@@ -1035,7 +1613,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1035
1613
|
* Updates render-graph cache with current pass set.
|
|
1036
1614
|
*/
|
|
1037
1615
|
const updateGraphPlanCache = (
|
|
1038
|
-
passes:
|
|
1616
|
+
passes: AnyPass[],
|
|
1039
1617
|
clearColor: [number, number, number, number],
|
|
1040
1618
|
graphPlan: RenderGraphPlan
|
|
1041
1619
|
): void => {
|
|
@@ -1049,18 +1627,19 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1049
1627
|
|
|
1050
1628
|
let index = 0;
|
|
1051
1629
|
for (const pass of passes) {
|
|
1052
|
-
const
|
|
1630
|
+
const rp = pass as Partial<RenderPass>;
|
|
1631
|
+
const passClearColor = rp.clearColor;
|
|
1053
1632
|
const hasPassClearColor = passClearColor !== undefined;
|
|
1054
1633
|
const snapshot = cachedGraphPasses[index];
|
|
1055
1634
|
if (!snapshot) {
|
|
1056
1635
|
cachedGraphPasses[index] = {
|
|
1057
1636
|
pass,
|
|
1058
1637
|
enabled: pass.enabled,
|
|
1059
|
-
needsSwap:
|
|
1060
|
-
input:
|
|
1061
|
-
output:
|
|
1062
|
-
clear:
|
|
1063
|
-
preserve:
|
|
1638
|
+
needsSwap: rp.needsSwap,
|
|
1639
|
+
input: rp.input,
|
|
1640
|
+
output: rp.output,
|
|
1641
|
+
clear: rp.clear,
|
|
1642
|
+
preserve: rp.preserve,
|
|
1064
1643
|
hasClearColor: hasPassClearColor,
|
|
1065
1644
|
clearColor0: passClearColor?.[0] ?? 0,
|
|
1066
1645
|
clearColor1: passClearColor?.[1] ?? 0,
|
|
@@ -1073,11 +1652,11 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1073
1652
|
|
|
1074
1653
|
snapshot.pass = pass;
|
|
1075
1654
|
snapshot.enabled = pass.enabled;
|
|
1076
|
-
snapshot.needsSwap =
|
|
1077
|
-
snapshot.input =
|
|
1078
|
-
snapshot.output =
|
|
1079
|
-
snapshot.clear =
|
|
1080
|
-
snapshot.preserve =
|
|
1655
|
+
snapshot.needsSwap = rp.needsSwap;
|
|
1656
|
+
snapshot.input = rp.input;
|
|
1657
|
+
snapshot.output = rp.output;
|
|
1658
|
+
snapshot.clear = rp.clear;
|
|
1659
|
+
snapshot.preserve = rp.preserve;
|
|
1081
1660
|
snapshot.hasClearColor = hasPassClearColor;
|
|
1082
1661
|
snapshot.clearColor0 = passClearColor?.[0] ?? 0;
|
|
1083
1662
|
snapshot.clearColor1 = passClearColor?.[1] ?? 0;
|
|
@@ -1090,7 +1669,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1090
1669
|
/**
|
|
1091
1670
|
* Synchronizes pass lifecycle callbacks and resize notifications.
|
|
1092
1671
|
*/
|
|
1093
|
-
const syncPassLifecycle = (passes:
|
|
1672
|
+
const syncPassLifecycle = (passes: AnyPass[], width: number, height: number): void => {
|
|
1094
1673
|
const resized = passWidth !== width || passHeight !== height;
|
|
1095
1674
|
if (!resized && lifecyclePassesRef === passes && passes.length === activePasses.length) {
|
|
1096
1675
|
let isSameOrder = true;
|
|
@@ -1292,7 +1871,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1292
1871
|
renderMode,
|
|
1293
1872
|
uniforms,
|
|
1294
1873
|
textures,
|
|
1295
|
-
canvasSize
|
|
1874
|
+
canvasSize,
|
|
1875
|
+
pendingStorageWrites
|
|
1296
1876
|
}) => {
|
|
1297
1877
|
if (deviceLostMessage) {
|
|
1298
1878
|
throw new Error(deviceLostMessage);
|
|
@@ -1361,9 +1941,11 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1361
1941
|
|
|
1362
1942
|
let bindGroupDirty = false;
|
|
1363
1943
|
for (const binding of textureBindings) {
|
|
1944
|
+
// Storage textures are managed by compute passes, skip source-driven updates
|
|
1945
|
+
if (storageTextureKeySet.has(binding.key)) continue;
|
|
1364
1946
|
const nextTexture =
|
|
1365
1947
|
textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
|
|
1366
|
-
if (updateTextureBinding(binding, nextTexture, renderMode)) {
|
|
1948
|
+
if (updateTextureBinding(binding, nextTexture, renderMode) && binding.fragmentVisible) {
|
|
1367
1949
|
bindGroupDirty = true;
|
|
1368
1950
|
}
|
|
1369
1951
|
}
|
|
@@ -1372,6 +1954,23 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1372
1954
|
bindGroup = createBindGroup();
|
|
1373
1955
|
}
|
|
1374
1956
|
|
|
1957
|
+
// Apply pending storage buffer writes
|
|
1958
|
+
if (pendingStorageWrites) {
|
|
1959
|
+
for (const write of pendingStorageWrites) {
|
|
1960
|
+
const buffer = storageBufferMap.get(write.name);
|
|
1961
|
+
if (buffer) {
|
|
1962
|
+
const data = write.data;
|
|
1963
|
+
device.queue.writeBuffer(
|
|
1964
|
+
buffer,
|
|
1965
|
+
write.offset,
|
|
1966
|
+
data.buffer as ArrayBuffer,
|
|
1967
|
+
data.byteOffset,
|
|
1968
|
+
data.byteLength
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1375
1974
|
const commandEncoder = device.createCommandEncoder();
|
|
1376
1975
|
const passes = resolvePasses();
|
|
1377
1976
|
const clearColor = options.getClearColor();
|
|
@@ -1402,6 +2001,98 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1402
2001
|
: null;
|
|
1403
2002
|
const sceneOutput = slots ? slots.source : canvasSurface;
|
|
1404
2003
|
|
|
2004
|
+
// Dispatch compute passes BEFORE scene render so storage textures
|
|
2005
|
+
// and buffers are up-to-date when the fragment shader samples them.
|
|
2006
|
+
if (slots) {
|
|
2007
|
+
for (const step of graphPlan.steps) {
|
|
2008
|
+
if (step.kind !== 'compute') {
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
const computePass = step.pass as {
|
|
2012
|
+
isCompute?: boolean;
|
|
2013
|
+
getCompute?: () => string;
|
|
2014
|
+
resolveDispatch?: (ctx: {
|
|
2015
|
+
width: number;
|
|
2016
|
+
height: number;
|
|
2017
|
+
time: number;
|
|
2018
|
+
delta: number;
|
|
2019
|
+
workgroupSize: [number, number, number];
|
|
2020
|
+
}) => [number, number, number];
|
|
2021
|
+
getWorkgroupSize?: () => [number, number, number];
|
|
2022
|
+
isPingPong?: boolean;
|
|
2023
|
+
getTarget?: () => string;
|
|
2024
|
+
getCurrentOutput?: () => string;
|
|
2025
|
+
getIterations?: () => number;
|
|
2026
|
+
advanceFrame?: () => void;
|
|
2027
|
+
};
|
|
2028
|
+
if (
|
|
2029
|
+
computePass.getCompute &&
|
|
2030
|
+
computePass.resolveDispatch &&
|
|
2031
|
+
computePass.getWorkgroupSize
|
|
2032
|
+
) {
|
|
2033
|
+
const computeSource = computePass.getCompute();
|
|
2034
|
+
const pingPongTarget =
|
|
2035
|
+
computePass.isPingPong && computePass.getTarget ? computePass.getTarget() : undefined;
|
|
2036
|
+
if (computePass.isPingPong && !pingPongTarget) {
|
|
2037
|
+
throw new Error('PingPongComputePass must provide a target texture key.');
|
|
2038
|
+
}
|
|
2039
|
+
const pingPongPair = pingPongTarget ? ensurePingPongTexturePair(pingPongTarget) : null;
|
|
2040
|
+
const pipelineEntry = buildComputePipelineEntry({
|
|
2041
|
+
computeSource,
|
|
2042
|
+
...(pingPongPair
|
|
2043
|
+
? {
|
|
2044
|
+
pingPongTarget: pingPongPair.target,
|
|
2045
|
+
pingPongFormat: pingPongPair.format
|
|
2046
|
+
}
|
|
2047
|
+
: {})
|
|
2048
|
+
});
|
|
2049
|
+
const workgroupSize = computePass.getWorkgroupSize();
|
|
2050
|
+
const storageBindGroup = getComputeStorageBindGroup();
|
|
2051
|
+
const storageTextureBindGroup = getComputeStorageTextureBindGroup();
|
|
2052
|
+
const iterations =
|
|
2053
|
+
computePass.isPingPong && computePass.getIterations ? computePass.getIterations() : 1;
|
|
2054
|
+
const currentOutput =
|
|
2055
|
+
computePass.isPingPong && computePass.getCurrentOutput
|
|
2056
|
+
? computePass.getCurrentOutput()
|
|
2057
|
+
: null;
|
|
2058
|
+
const readFromAAtIterationZero =
|
|
2059
|
+
pingPongPair && currentOutput ? currentOutput !== `${pingPongPair.target}B` : true;
|
|
2060
|
+
|
|
2061
|
+
for (let iter = 0; iter < iterations; iter += 1) {
|
|
2062
|
+
const dispatch = computePass.resolveDispatch({
|
|
2063
|
+
width,
|
|
2064
|
+
height,
|
|
2065
|
+
time,
|
|
2066
|
+
delta,
|
|
2067
|
+
workgroupSize
|
|
2068
|
+
});
|
|
2069
|
+
const cPass = commandEncoder.beginComputePass();
|
|
2070
|
+
cPass.setPipeline(pipelineEntry.pipeline);
|
|
2071
|
+
cPass.setBindGroup(0, pipelineEntry.bindGroup);
|
|
2072
|
+
if (storageBindGroup) {
|
|
2073
|
+
cPass.setBindGroup(1, storageBindGroup);
|
|
2074
|
+
}
|
|
2075
|
+
if (pingPongPair) {
|
|
2076
|
+
const readFromA =
|
|
2077
|
+
iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
|
|
2078
|
+
cPass.setBindGroup(
|
|
2079
|
+
2,
|
|
2080
|
+
getPingPongStorageTextureBindGroup(pingPongPair.target, readFromA)
|
|
2081
|
+
);
|
|
2082
|
+
} else if (storageTextureBindGroup) {
|
|
2083
|
+
cPass.setBindGroup(2, storageTextureBindGroup);
|
|
2084
|
+
}
|
|
2085
|
+
cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
|
|
2086
|
+
cPass.end();
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
if (computePass.isPingPong && computePass.advanceFrame) {
|
|
2090
|
+
computePass.advanceFrame();
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1405
2096
|
const scenePass = commandEncoder.beginRenderPass({
|
|
1406
2097
|
colorAttachments: [
|
|
1407
2098
|
{
|
|
@@ -1420,6 +2111,9 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1420
2111
|
|
|
1421
2112
|
scenePass.setPipeline(pipeline);
|
|
1422
2113
|
scenePass.setBindGroup(0, bindGroup);
|
|
2114
|
+
if (fragmentStorageBindGroup) {
|
|
2115
|
+
scenePass.setBindGroup(1, fragmentStorageBindGroup);
|
|
2116
|
+
}
|
|
1423
2117
|
scenePass.draw(3);
|
|
1424
2118
|
scenePass.end();
|
|
1425
2119
|
|
|
@@ -1448,10 +2142,15 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1448
2142
|
};
|
|
1449
2143
|
|
|
1450
2144
|
for (const step of graphPlan.steps) {
|
|
2145
|
+
// Compute passes already dispatched above
|
|
2146
|
+
if (step.kind === 'compute') {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
1451
2150
|
const input = resolveStepSurface(step.input);
|
|
1452
2151
|
const output = resolveStepSurface(step.output);
|
|
1453
2152
|
|
|
1454
|
-
step.pass.render({
|
|
2153
|
+
(step.pass as RenderPass).render({
|
|
1455
2154
|
device,
|
|
1456
2155
|
commandEncoder,
|
|
1457
2156
|
source: slots.source,
|
|
@@ -1467,7 +2166,12 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1467
2166
|
clear: step.clear,
|
|
1468
2167
|
clearColor: step.clearColor,
|
|
1469
2168
|
preserve: step.preserve,
|
|
1470
|
-
beginRenderPass: (passOptions
|
|
2169
|
+
beginRenderPass: (passOptions?: {
|
|
2170
|
+
clear?: boolean;
|
|
2171
|
+
clearColor?: [number, number, number, number];
|
|
2172
|
+
preserve?: boolean;
|
|
2173
|
+
view?: GPUTextureView;
|
|
2174
|
+
}) => {
|
|
1471
2175
|
const clear = passOptions?.clear ?? step.clear;
|
|
1472
2176
|
const clearColor = passOptions?.clearColor ?? step.clearColor;
|
|
1473
2177
|
const preserve = passOptions?.preserve ?? step.preserve;
|
|
@@ -1510,11 +2214,27 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1510
2214
|
initializationCleanups.length = 0;
|
|
1511
2215
|
return {
|
|
1512
2216
|
render,
|
|
2217
|
+
getStorageBuffer: (name: string): GPUBuffer | undefined => {
|
|
2218
|
+
return storageBufferMap.get(name);
|
|
2219
|
+
},
|
|
2220
|
+
getDevice: (): GPUDevice => {
|
|
2221
|
+
return device;
|
|
2222
|
+
},
|
|
1513
2223
|
destroy: () => {
|
|
1514
2224
|
isDestroyed = true;
|
|
1515
2225
|
device.removeEventListener('uncapturederror', handleUncapturedError);
|
|
1516
2226
|
frameBuffer.destroy();
|
|
1517
2227
|
uniformBuffer.destroy();
|
|
2228
|
+
for (const buffer of storageBufferMap.values()) {
|
|
2229
|
+
buffer.destroy();
|
|
2230
|
+
}
|
|
2231
|
+
storageBufferMap.clear();
|
|
2232
|
+
for (const pair of pingPongTexturePairs.values()) {
|
|
2233
|
+
pair.textureA.destroy();
|
|
2234
|
+
pair.textureB.destroy();
|
|
2235
|
+
}
|
|
2236
|
+
pingPongTexturePairs.clear();
|
|
2237
|
+
computePipelineCache.clear();
|
|
1518
2238
|
destroyRenderTexture(sourceSlotTarget);
|
|
1519
2239
|
destroyRenderTexture(targetSlotTarget);
|
|
1520
2240
|
for (const target of runtimeRenderTargets.values()) {
|