@motion-core/motion-gpu 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/dist/advanced.js +3 -1
- package/dist/core/advanced.js +3 -1
- package/dist/core/compute-shader.d.ts +87 -0
- package/dist/core/compute-shader.d.ts.map +1 -0
- package/dist/core/compute-shader.js +205 -0
- package/dist/core/compute-shader.js.map +1 -0
- package/dist/core/error-report.d.ts +1 -1
- package/dist/core/error-report.d.ts.map +1 -1
- package/dist/core/error-report.js +63 -0
- package/dist/core/error-report.js.map +1 -1
- package/dist/core/frame-registry.d.ts.map +1 -1
- package/dist/core/frame-registry.js +1 -1
- package/dist/core/frame-registry.js.map +1 -1
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/material-preprocess.d.ts.map +1 -1
- package/dist/core/material-preprocess.js +5 -3
- package/dist/core/material-preprocess.js.map +1 -1
- package/dist/core/material.d.ts +22 -6
- package/dist/core/material.d.ts.map +1 -1
- package/dist/core/material.js +30 -3
- package/dist/core/material.js.map +1 -1
- package/dist/core/render-graph.d.ts +7 -3
- package/dist/core/render-graph.d.ts.map +1 -1
- package/dist/core/render-graph.js +22 -6
- package/dist/core/render-graph.js.map +1 -1
- package/dist/core/renderer.d.ts.map +1 -1
- package/dist/core/renderer.js +418 -23
- package/dist/core/renderer.js.map +1 -1
- package/dist/core/runtime-loop.d.ts +2 -2
- package/dist/core/runtime-loop.d.ts.map +1 -1
- package/dist/core/runtime-loop.js +49 -1
- package/dist/core/runtime-loop.js.map +1 -1
- package/dist/core/shader.d.ts +9 -1
- package/dist/core/shader.d.ts.map +1 -1
- package/dist/core/shader.js +21 -2
- package/dist/core/shader.js.map +1 -1
- package/dist/core/storage-buffers.d.ts +37 -0
- package/dist/core/storage-buffers.d.ts.map +1 -0
- package/dist/core/storage-buffers.js +95 -0
- package/dist/core/storage-buffers.js.map +1 -0
- package/dist/core/texture-loader.d.ts.map +1 -1
- package/dist/core/texture-loader.js +4 -0
- package/dist/core/texture-loader.js.map +1 -1
- package/dist/core/textures.d.ts +12 -0
- package/dist/core/textures.d.ts.map +1 -1
- package/dist/core/textures.js +7 -2
- package/dist/core/textures.js.map +1 -1
- package/dist/core/types.d.ts +146 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/passes/ComputePass.d.ts +83 -0
- package/dist/passes/ComputePass.d.ts.map +1 -0
- package/dist/passes/ComputePass.js +92 -0
- package/dist/passes/ComputePass.js.map +1 -0
- package/dist/passes/PingPongComputePass.d.ts +104 -0
- package/dist/passes/PingPongComputePass.d.ts.map +1 -0
- package/dist/passes/PingPongComputePass.js +132 -0
- package/dist/passes/PingPongComputePass.js.map +1 -0
- package/dist/passes/ShaderPass.d.ts.map +1 -1
- package/dist/passes/ShaderPass.js +2 -1
- package/dist/passes/ShaderPass.js.map +1 -1
- package/dist/passes/index.d.ts +2 -0
- package/dist/passes/index.d.ts.map +1 -1
- package/dist/passes/index.js +3 -1
- package/dist/react/FragCanvas.d.ts +2 -2
- package/dist/react/FragCanvas.d.ts.map +1 -1
- package/dist/react/FragCanvas.js.map +1 -1
- package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
- package/dist/react/MotionGPUErrorOverlay.js +123 -20
- package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
- package/dist/react/advanced.js +3 -1
- package/dist/react/index.d.ts +5 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/svelte/FragCanvas.svelte +2 -2
- package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
- package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
- package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
- package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
- package/dist/svelte/advanced.js +3 -1
- package/dist/svelte/index.d.ts +5 -2
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +3 -1
- package/package.json +1 -1
- package/src/lib/core/compute-shader.ts +326 -0
- package/src/lib/core/error-report.ts +129 -0
- package/src/lib/core/frame-registry.ts +2 -1
- package/src/lib/core/index.ts +18 -1
- package/src/lib/core/material-preprocess.ts +17 -6
- package/src/lib/core/material.ts +101 -20
- package/src/lib/core/render-graph.ts +39 -9
- package/src/lib/core/renderer.ts +655 -41
- package/src/lib/core/runtime-loop.ts +82 -3
- package/src/lib/core/shader.ts +45 -2
- package/src/lib/core/storage-buffers.ts +142 -0
- package/src/lib/core/texture-loader.ts +6 -0
- package/src/lib/core/textures.ts +24 -2
- package/src/lib/core/types.ts +165 -4
- package/src/lib/passes/ComputePass.ts +136 -0
- package/src/lib/passes/PingPongComputePass.ts +180 -0
- package/src/lib/passes/ShaderPass.ts +2 -1
- package/src/lib/passes/index.ts +6 -0
- package/src/lib/react/FragCanvas.tsx +3 -3
- package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
- package/src/lib/react/index.ts +18 -1
- package/src/lib/svelte/FragCanvas.svelte +2 -2
- package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
- package/src/lib/svelte/index.ts +18 -1
package/src/lib/core/renderer.ts
CHANGED
|
@@ -17,7 +17,15 @@ import {
|
|
|
17
17
|
toTextureData
|
|
18
18
|
} from './textures.js';
|
|
19
19
|
import { packUniformsInto } from './uniforms.js';
|
|
20
|
+
import {
|
|
21
|
+
buildComputeShaderSource,
|
|
22
|
+
buildPingPongComputeShaderSource,
|
|
23
|
+
extractWorkgroupSize,
|
|
24
|
+
storageTextureSampleScalarType
|
|
25
|
+
} from './compute-shader.js';
|
|
26
|
+
import { normalizeStorageBufferDefinition } from './storage-buffers.js';
|
|
20
27
|
import type {
|
|
28
|
+
AnyPass,
|
|
21
29
|
RenderPass,
|
|
22
30
|
RenderPassInputSlot,
|
|
23
31
|
RenderPassOutputSlot,
|
|
@@ -25,6 +33,8 @@ import type {
|
|
|
25
33
|
RenderTarget,
|
|
26
34
|
Renderer,
|
|
27
35
|
RendererOptions,
|
|
36
|
+
StorageBufferAccess,
|
|
37
|
+
StorageBufferType,
|
|
28
38
|
TextureSource,
|
|
29
39
|
TextureUpdateMode,
|
|
30
40
|
TextureValue
|
|
@@ -86,11 +96,26 @@ interface RuntimeRenderTarget {
|
|
|
86
96
|
format: GPUTextureFormat;
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Runtime ping-pong storage textures for a single logical target key.
|
|
101
|
+
*/
|
|
102
|
+
interface PingPongTexturePair {
|
|
103
|
+
target: string;
|
|
104
|
+
format: GPUTextureFormat;
|
|
105
|
+
width: number;
|
|
106
|
+
height: number;
|
|
107
|
+
textureA: GPUTexture;
|
|
108
|
+
viewA: GPUTextureView;
|
|
109
|
+
textureB: GPUTexture;
|
|
110
|
+
viewB: GPUTextureView;
|
|
111
|
+
bindGroupLayout: GPUBindGroupLayout;
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
/**
|
|
90
115
|
* Cached pass properties used to validate render-graph cache correctness.
|
|
91
116
|
*/
|
|
92
117
|
interface RenderGraphPassSnapshot {
|
|
93
|
-
pass:
|
|
118
|
+
pass: AnyPass;
|
|
94
119
|
enabled: RenderPass['enabled'];
|
|
95
120
|
needsSwap: RenderPass['needsSwap'];
|
|
96
121
|
input: RenderPass['input'];
|
|
@@ -118,6 +143,19 @@ function getTextureBindings(index: number): {
|
|
|
118
143
|
};
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Maps WGSL scalar texture type to WebGPU sampled texture bind-group sample type.
|
|
148
|
+
*/
|
|
149
|
+
function toGpuTextureSampleType(type: 'f32' | 'u32' | 'i32'): GPUTextureSampleType {
|
|
150
|
+
if (type === 'u32') {
|
|
151
|
+
return 'uint';
|
|
152
|
+
}
|
|
153
|
+
if (type === 'i32') {
|
|
154
|
+
return 'sint';
|
|
155
|
+
}
|
|
156
|
+
return 'float';
|
|
157
|
+
}
|
|
158
|
+
|
|
121
159
|
/**
|
|
122
160
|
* Resizes canvas backing store to match client size and DPR.
|
|
123
161
|
*/
|
|
@@ -208,7 +246,7 @@ function toSortedUniqueStrings(values: string[]): string[] {
|
|
|
208
246
|
}
|
|
209
247
|
|
|
210
248
|
function buildPassGraphSnapshot(
|
|
211
|
-
passes:
|
|
249
|
+
passes: AnyPass[] | undefined
|
|
212
250
|
): NonNullable<ShaderCompilationRuntimeContext['passGraph']> {
|
|
213
251
|
const declaredPasses = passes ?? [];
|
|
214
252
|
let enabledPassCount = 0;
|
|
@@ -221,9 +259,13 @@ function buildPassGraphSnapshot(
|
|
|
221
259
|
}
|
|
222
260
|
|
|
223
261
|
enabledPassCount += 1;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
if ('isCompute' in pass && (pass as { isCompute?: boolean }).isCompute === true) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const rp = pass as RenderPass;
|
|
266
|
+
const needsSwap = rp.needsSwap ?? true;
|
|
267
|
+
const input = rp.input ?? 'source';
|
|
268
|
+
const output = rp.output ?? (needsSwap ? 'target' : 'source');
|
|
227
269
|
inputs.push(input);
|
|
228
270
|
outputs.push(output);
|
|
229
271
|
}
|
|
@@ -631,7 +673,13 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
631
673
|
options.textureKeys,
|
|
632
674
|
{
|
|
633
675
|
convertLinearToSrgb,
|
|
634
|
-
fragmentLineMap: options.fragmentLineMap
|
|
676
|
+
fragmentLineMap: options.fragmentLineMap,
|
|
677
|
+
...(options.storageBufferKeys !== undefined
|
|
678
|
+
? { storageBufferKeys: options.storageBufferKeys }
|
|
679
|
+
: {}),
|
|
680
|
+
...(options.storageBufferDefinitions !== undefined
|
|
681
|
+
? { storageBufferDefinitions: options.storageBufferDefinitions }
|
|
682
|
+
: {})
|
|
635
683
|
}
|
|
636
684
|
);
|
|
637
685
|
const shaderModule = device.createShaderModule({ code: builtShader.code });
|
|
@@ -650,6 +698,10 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
650
698
|
options.textureDefinitions,
|
|
651
699
|
options.textureKeys
|
|
652
700
|
);
|
|
701
|
+
const storageBufferKeys = options.storageBufferKeys ?? [];
|
|
702
|
+
const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
|
|
703
|
+
const storageTextureKeys = options.storageTextureKeys ?? [];
|
|
704
|
+
const storageTextureKeySet = new Set(storageTextureKeys);
|
|
653
705
|
const textureBindings = options.textureKeys.map((key, index): RuntimeTextureBinding => {
|
|
654
706
|
const config = normalizedTextureDefinitions[key];
|
|
655
707
|
if (!config) {
|
|
@@ -665,7 +717,11 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
665
717
|
addressModeV: config.addressModeV,
|
|
666
718
|
maxAnisotropy: config.filter === 'linear' ? config.anisotropy : 1
|
|
667
719
|
});
|
|
668
|
-
|
|
720
|
+
// Storage textures use a safe fallback format — the fallback is never
|
|
721
|
+
// sampled because storage textures are eagerly allocated with their
|
|
722
|
+
// real format/dimensions. Non-storage textures use their own format.
|
|
723
|
+
const fallbackFormat = config.storage ? 'rgba8unorm' : config.format;
|
|
724
|
+
const fallbackTexture = createFallbackTexture(device, fallbackFormat);
|
|
669
725
|
registerInitializationCleanup(() => {
|
|
670
726
|
fallbackTexture.destroy();
|
|
671
727
|
});
|
|
@@ -701,14 +757,46 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
701
757
|
runtimeBinding.defaultUpdate = config.update;
|
|
702
758
|
}
|
|
703
759
|
|
|
760
|
+
// Storage textures: eagerly create GPU texture with explicit dimensions
|
|
761
|
+
if (config.storage && config.width && config.height) {
|
|
762
|
+
const storageUsage =
|
|
763
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
764
|
+
GPUTextureUsage.STORAGE_BINDING |
|
|
765
|
+
GPUTextureUsage.COPY_DST;
|
|
766
|
+
const storageTexture = device.createTexture({
|
|
767
|
+
size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
|
|
768
|
+
format: config.format,
|
|
769
|
+
usage: storageUsage
|
|
770
|
+
});
|
|
771
|
+
registerInitializationCleanup(() => {
|
|
772
|
+
storageTexture.destroy();
|
|
773
|
+
});
|
|
774
|
+
runtimeBinding.texture = storageTexture as unknown as GPUTexture;
|
|
775
|
+
runtimeBinding.view = storageTexture.createView();
|
|
776
|
+
runtimeBinding.width = config.width;
|
|
777
|
+
runtimeBinding.height = config.height;
|
|
778
|
+
}
|
|
779
|
+
|
|
704
780
|
return runtimeBinding;
|
|
705
781
|
});
|
|
706
782
|
|
|
707
783
|
const bindGroupLayout = device.createBindGroupLayout({
|
|
708
784
|
entries: createBindGroupLayoutEntries(textureBindings)
|
|
709
785
|
});
|
|
786
|
+
const fragmentStorageBindGroupLayout =
|
|
787
|
+
storageBufferKeys.length > 0
|
|
788
|
+
? device.createBindGroupLayout({
|
|
789
|
+
entries: storageBufferKeys.map((_, index) => ({
|
|
790
|
+
binding: index,
|
|
791
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
792
|
+
buffer: { type: 'read-only-storage' as GPUBufferBindingType }
|
|
793
|
+
}))
|
|
794
|
+
})
|
|
795
|
+
: null;
|
|
710
796
|
const pipelineLayout = device.createPipelineLayout({
|
|
711
|
-
bindGroupLayouts:
|
|
797
|
+
bindGroupLayouts: fragmentStorageBindGroupLayout
|
|
798
|
+
? [bindGroupLayout, fragmentStorageBindGroupLayout]
|
|
799
|
+
: [bindGroupLayout]
|
|
712
800
|
});
|
|
713
801
|
|
|
714
802
|
const pipeline = device.createRenderPipeline({
|
|
@@ -773,6 +861,383 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
773
861
|
});
|
|
774
862
|
let blitBindGroupByView = new WeakMap<GPUTextureView, GPUBindGroup>();
|
|
775
863
|
|
|
864
|
+
// ── Storage buffer allocation ────────────────────────────────────────
|
|
865
|
+
const storageBufferMap = new Map<string, GPUBuffer>();
|
|
866
|
+
const pingPongTexturePairs = new Map<string, PingPongTexturePair>();
|
|
867
|
+
|
|
868
|
+
for (const key of storageBufferKeys) {
|
|
869
|
+
const definition = storageBufferDefinitions[key];
|
|
870
|
+
if (!definition) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const normalized = normalizeStorageBufferDefinition(definition);
|
|
874
|
+
const buffer = device.createBuffer({
|
|
875
|
+
size: normalized.size,
|
|
876
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
877
|
+
});
|
|
878
|
+
registerInitializationCleanup(() => {
|
|
879
|
+
buffer.destroy();
|
|
880
|
+
});
|
|
881
|
+
if (definition.initialData) {
|
|
882
|
+
const data = definition.initialData;
|
|
883
|
+
device.queue.writeBuffer(
|
|
884
|
+
buffer,
|
|
885
|
+
0,
|
|
886
|
+
data.buffer as ArrayBuffer,
|
|
887
|
+
data.byteOffset,
|
|
888
|
+
data.byteLength
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
storageBufferMap.set(key, buffer);
|
|
892
|
+
}
|
|
893
|
+
const fragmentStorageBindGroup =
|
|
894
|
+
fragmentStorageBindGroupLayout && storageBufferKeys.length > 0
|
|
895
|
+
? device.createBindGroup({
|
|
896
|
+
layout: fragmentStorageBindGroupLayout,
|
|
897
|
+
entries: storageBufferKeys.map((key, index) => {
|
|
898
|
+
const buffer = storageBufferMap.get(key);
|
|
899
|
+
if (!buffer) {
|
|
900
|
+
throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
901
|
+
}
|
|
902
|
+
return { binding: index, resource: { buffer } };
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
: null;
|
|
906
|
+
|
|
907
|
+
const ensurePingPongTexturePair = (target: string): PingPongTexturePair => {
|
|
908
|
+
const existing = pingPongTexturePairs.get(target);
|
|
909
|
+
if (existing) {
|
|
910
|
+
return existing;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const config = normalizedTextureDefinitions[target];
|
|
914
|
+
if (!config || !config.storage) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`PingPongComputePass target "${target}" must reference a texture declared with storage:true.`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (!config.width || !config.height) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`PingPongComputePass target "${target}" requires explicit texture width and height.`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const usage =
|
|
926
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
927
|
+
GPUTextureUsage.STORAGE_BINDING |
|
|
928
|
+
GPUTextureUsage.COPY_DST;
|
|
929
|
+
const textureA = device.createTexture({
|
|
930
|
+
size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
|
|
931
|
+
format: config.format,
|
|
932
|
+
usage
|
|
933
|
+
});
|
|
934
|
+
const textureB = device.createTexture({
|
|
935
|
+
size: { width: config.width, height: config.height, depthOrArrayLayers: 1 },
|
|
936
|
+
format: config.format,
|
|
937
|
+
usage
|
|
938
|
+
});
|
|
939
|
+
registerInitializationCleanup(() => {
|
|
940
|
+
textureA.destroy();
|
|
941
|
+
});
|
|
942
|
+
registerInitializationCleanup(() => {
|
|
943
|
+
textureB.destroy();
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
const sampleScalarType = storageTextureSampleScalarType(config.format);
|
|
947
|
+
const sampleType = toGpuTextureSampleType(sampleScalarType);
|
|
948
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
949
|
+
entries: [
|
|
950
|
+
{
|
|
951
|
+
binding: 0,
|
|
952
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
953
|
+
texture: {
|
|
954
|
+
sampleType,
|
|
955
|
+
viewDimension: '2d',
|
|
956
|
+
multisampled: false
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
binding: 1,
|
|
961
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
962
|
+
storageTexture: {
|
|
963
|
+
access: 'write-only' as GPUStorageTextureAccess,
|
|
964
|
+
format: config.format as GPUTextureFormat,
|
|
965
|
+
viewDimension: '2d'
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
const pair: PingPongTexturePair = {
|
|
972
|
+
target,
|
|
973
|
+
format: config.format as GPUTextureFormat,
|
|
974
|
+
width: config.width,
|
|
975
|
+
height: config.height,
|
|
976
|
+
textureA,
|
|
977
|
+
viewA: textureA.createView(),
|
|
978
|
+
textureB,
|
|
979
|
+
viewB: textureB.createView(),
|
|
980
|
+
bindGroupLayout
|
|
981
|
+
};
|
|
982
|
+
pingPongTexturePairs.set(target, pair);
|
|
983
|
+
return pair;
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
// ── Compute pipeline setup ──────────────────────────────────────────
|
|
987
|
+
interface ComputePipelineEntry {
|
|
988
|
+
pipeline: GPUComputePipeline;
|
|
989
|
+
bindGroup: GPUBindGroup;
|
|
990
|
+
workgroupSize: [number, number, number];
|
|
991
|
+
computeSource: string;
|
|
992
|
+
}
|
|
993
|
+
const computePipelineCache = new Map<string, ComputePipelineEntry>();
|
|
994
|
+
|
|
995
|
+
const buildComputePipelineEntry = (buildOptions: {
|
|
996
|
+
computeSource: string;
|
|
997
|
+
pingPongTarget?: string;
|
|
998
|
+
pingPongFormat?: GPUTextureFormat;
|
|
999
|
+
}): ComputePipelineEntry => {
|
|
1000
|
+
const cacheKey =
|
|
1001
|
+
buildOptions.pingPongTarget && buildOptions.pingPongFormat
|
|
1002
|
+
? `pingpong:${buildOptions.pingPongTarget}:${buildOptions.pingPongFormat}:${buildOptions.computeSource}`
|
|
1003
|
+
: `compute:${buildOptions.computeSource}`;
|
|
1004
|
+
const cached = computePipelineCache.get(cacheKey);
|
|
1005
|
+
if (cached) {
|
|
1006
|
+
return cached;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const storageBufferDefs: Record<
|
|
1010
|
+
string,
|
|
1011
|
+
{ type: StorageBufferType; access: StorageBufferAccess }
|
|
1012
|
+
> = {};
|
|
1013
|
+
for (const key of storageBufferKeys) {
|
|
1014
|
+
const def = storageBufferDefinitions[key];
|
|
1015
|
+
if (def) {
|
|
1016
|
+
const norm = normalizeStorageBufferDefinition(def);
|
|
1017
|
+
storageBufferDefs[key] = { type: norm.type, access: norm.access };
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const storageTextureDefs: Record<string, { format: GPUTextureFormat }> = {};
|
|
1021
|
+
for (const key of storageTextureKeys) {
|
|
1022
|
+
const texDef = options.textureDefinitions[key];
|
|
1023
|
+
if (texDef?.format) {
|
|
1024
|
+
storageTextureDefs[key] = { format: texDef.format };
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const isPingPongPipeline = Boolean(
|
|
1029
|
+
buildOptions.pingPongTarget && buildOptions.pingPongFormat
|
|
1030
|
+
);
|
|
1031
|
+
const shaderCode = isPingPongPipeline
|
|
1032
|
+
? buildPingPongComputeShaderSource({
|
|
1033
|
+
compute: buildOptions.computeSource,
|
|
1034
|
+
uniformLayout: options.uniformLayout,
|
|
1035
|
+
storageBufferKeys,
|
|
1036
|
+
storageBufferDefinitions: storageBufferDefs,
|
|
1037
|
+
target: buildOptions.pingPongTarget!,
|
|
1038
|
+
targetFormat: buildOptions.pingPongFormat!
|
|
1039
|
+
})
|
|
1040
|
+
: buildComputeShaderSource({
|
|
1041
|
+
compute: buildOptions.computeSource,
|
|
1042
|
+
uniformLayout: options.uniformLayout,
|
|
1043
|
+
storageBufferKeys,
|
|
1044
|
+
storageBufferDefinitions: storageBufferDefs,
|
|
1045
|
+
storageTextureKeys,
|
|
1046
|
+
storageTextureDefinitions: storageTextureDefs
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const computeShaderModule = device.createShaderModule({ code: shaderCode });
|
|
1050
|
+
const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
|
|
1051
|
+
|
|
1052
|
+
// Compute bind group layout: group(0)=uniforms, group(1)=storage buffers, group(2)=storage textures
|
|
1053
|
+
const computeUniformBGL = device.createBindGroupLayout({
|
|
1054
|
+
entries: [
|
|
1055
|
+
{
|
|
1056
|
+
binding: FRAME_BINDING,
|
|
1057
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1058
|
+
buffer: { type: 'uniform', minBindingSize: 16 }
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
binding: UNIFORM_BINDING,
|
|
1062
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1063
|
+
buffer: { type: 'uniform' }
|
|
1064
|
+
}
|
|
1065
|
+
]
|
|
1066
|
+
});
|
|
1067
|
+
|
|
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
|
+
const storageBGL =
|
|
1080
|
+
storageBGLEntries.length > 0
|
|
1081
|
+
? device.createBindGroupLayout({ entries: storageBGLEntries })
|
|
1082
|
+
: null;
|
|
1083
|
+
|
|
1084
|
+
const storageTextureBGLEntries: GPUBindGroupLayoutEntry[] = isPingPongPipeline
|
|
1085
|
+
? [
|
|
1086
|
+
{
|
|
1087
|
+
binding: 0,
|
|
1088
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1089
|
+
texture: {
|
|
1090
|
+
sampleType: toGpuTextureSampleType(
|
|
1091
|
+
storageTextureSampleScalarType(buildOptions.pingPongFormat!)
|
|
1092
|
+
),
|
|
1093
|
+
viewDimension: '2d',
|
|
1094
|
+
multisampled: false
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
binding: 1,
|
|
1099
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
1100
|
+
storageTexture: {
|
|
1101
|
+
access: 'write-only' as GPUStorageTextureAccess,
|
|
1102
|
+
format: buildOptions.pingPongFormat!,
|
|
1103
|
+
viewDimension: '2d'
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
]
|
|
1107
|
+
: storageTextureKeys.map((key, index) => {
|
|
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
|
+
});
|
|
1119
|
+
const storageTextureBGL =
|
|
1120
|
+
storageTextureBGLEntries.length > 0
|
|
1121
|
+
? device.createBindGroupLayout({ entries: storageTextureBGLEntries })
|
|
1122
|
+
: null;
|
|
1123
|
+
|
|
1124
|
+
// Bind group layout indices must match shader @group() indices:
|
|
1125
|
+
// group(0) = uniforms, group(1) = storage buffers, group(2) = storage textures.
|
|
1126
|
+
// When a group is unused, insert an empty placeholder to keep indices aligned.
|
|
1127
|
+
const bindGroupLayouts: GPUBindGroupLayout[] = [computeUniformBGL];
|
|
1128
|
+
if (storageBGL || storageTextureBGL) {
|
|
1129
|
+
bindGroupLayouts.push(storageBGL ?? device.createBindGroupLayout({ entries: [] }));
|
|
1130
|
+
}
|
|
1131
|
+
if (storageTextureBGL) {
|
|
1132
|
+
bindGroupLayouts.push(storageTextureBGL);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
|
|
1136
|
+
const pipeline = device.createComputePipeline({
|
|
1137
|
+
layout: computePipelineLayout,
|
|
1138
|
+
compute: {
|
|
1139
|
+
module: computeShaderModule,
|
|
1140
|
+
entryPoint: 'compute'
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Build uniform bind group for compute (group 0)
|
|
1145
|
+
const computeUniformBindGroup = device.createBindGroup({
|
|
1146
|
+
layout: computeUniformBGL,
|
|
1147
|
+
entries: [
|
|
1148
|
+
{ binding: FRAME_BINDING, resource: { buffer: frameBuffer } },
|
|
1149
|
+
{ binding: UNIFORM_BINDING, resource: { buffer: uniformBuffer } }
|
|
1150
|
+
]
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
const entry: ComputePipelineEntry = {
|
|
1154
|
+
pipeline,
|
|
1155
|
+
bindGroup: computeUniformBindGroup,
|
|
1156
|
+
workgroupSize,
|
|
1157
|
+
computeSource: buildOptions.computeSource
|
|
1158
|
+
};
|
|
1159
|
+
computePipelineCache.set(cacheKey, entry);
|
|
1160
|
+
return entry;
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// Helper to get the storage bind group for dispatch
|
|
1164
|
+
const getComputeStorageBindGroup = (): GPUBindGroup | null => {
|
|
1165
|
+
if (storageBufferKeys.length === 0) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
// Rebuild bind group with current storage buffers
|
|
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) => {
|
|
1182
|
+
const buffer = storageBufferMap.get(key);
|
|
1183
|
+
if (!buffer) {
|
|
1184
|
+
throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
1185
|
+
}
|
|
1186
|
+
return { binding: index, resource: { buffer } };
|
|
1187
|
+
});
|
|
1188
|
+
return device.createBindGroup({
|
|
1189
|
+
layout: storageBGL,
|
|
1190
|
+
entries: storageEntries
|
|
1191
|
+
});
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// Helper to get the storage texture bind group for compute dispatch (group 2)
|
|
1195
|
+
const getComputeStorageTextureBindGroup = (): GPUBindGroup | null => {
|
|
1196
|
+
if (storageTextureKeys.length === 0) {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
const entries: GPUBindGroupLayoutEntry[] = storageTextureKeys.map((key, index) => {
|
|
1200
|
+
const texDef = options.textureDefinitions[key];
|
|
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);
|
|
1215
|
+
if (!binding || !binding.texture) {
|
|
1216
|
+
throw new Error(`Storage texture "${key}" not allocated.`);
|
|
1217
|
+
}
|
|
1218
|
+
return { binding: index, resource: binding.view };
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
return device.createBindGroup({ layout: bgl, entries: bgEntries });
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// Helper to get ping-pong storage texture bind group for compute dispatch (group 2)
|
|
1225
|
+
const getPingPongStorageTextureBindGroup = (
|
|
1226
|
+
target: string,
|
|
1227
|
+
readFromA: boolean
|
|
1228
|
+
): GPUBindGroup => {
|
|
1229
|
+
const pair = ensurePingPongTexturePair(target);
|
|
1230
|
+
const readView = readFromA ? pair.viewA : pair.viewB;
|
|
1231
|
+
const writeView = readFromA ? pair.viewB : pair.viewA;
|
|
1232
|
+
return device.createBindGroup({
|
|
1233
|
+
layout: pair.bindGroupLayout,
|
|
1234
|
+
entries: [
|
|
1235
|
+
{ binding: 0, resource: readView },
|
|
1236
|
+
{ binding: 1, resource: writeView }
|
|
1237
|
+
]
|
|
1238
|
+
});
|
|
1239
|
+
};
|
|
1240
|
+
|
|
776
1241
|
const frameBuffer = device.createBuffer({
|
|
777
1242
|
size: 16,
|
|
778
1243
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
@@ -891,14 +1356,18 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
891
1356
|
return false;
|
|
892
1357
|
}
|
|
893
1358
|
|
|
1359
|
+
let textureUsage =
|
|
1360
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
1361
|
+
GPUTextureUsage.COPY_DST |
|
|
1362
|
+
GPUTextureUsage.RENDER_ATTACHMENT;
|
|
1363
|
+
if (storageTextureKeySet.has(binding.key)) {
|
|
1364
|
+
textureUsage |= GPUTextureUsage.STORAGE_BINDING;
|
|
1365
|
+
}
|
|
894
1366
|
const texture = device.createTexture({
|
|
895
1367
|
size: { width, height, depthOrArrayLayers: 1 },
|
|
896
1368
|
format,
|
|
897
1369
|
mipLevelCount,
|
|
898
|
-
usage:
|
|
899
|
-
GPUTextureUsage.TEXTURE_BINDING |
|
|
900
|
-
GPUTextureUsage.COPY_DST |
|
|
901
|
-
GPUTextureUsage.RENDER_ATTACHMENT
|
|
1370
|
+
usage: textureUsage
|
|
902
1371
|
});
|
|
903
1372
|
registerInitializationCleanup(() => {
|
|
904
1373
|
texture.destroy();
|
|
@@ -924,6 +1393,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
924
1393
|
};
|
|
925
1394
|
|
|
926
1395
|
for (const binding of textureBindings) {
|
|
1396
|
+
// Skip storage textures — they are eagerly allocated and not source-driven
|
|
1397
|
+
if (storageTextureKeySet.has(binding.key)) continue;
|
|
927
1398
|
const defaultSource = normalizedTextureDefinitions[binding.key]?.source ?? null;
|
|
928
1399
|
updateTextureBinding(binding, defaultSource, 'always');
|
|
929
1400
|
}
|
|
@@ -942,18 +1413,18 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
942
1413
|
let configuredWidth = 0;
|
|
943
1414
|
let configuredHeight = 0;
|
|
944
1415
|
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:
|
|
1416
|
+
const activePasses: AnyPass[] = [];
|
|
1417
|
+
const lifecyclePreviousSet = new Set<AnyPass>();
|
|
1418
|
+
const lifecycleNextSet = new Set<AnyPass>();
|
|
1419
|
+
const lifecycleUniquePasses: AnyPass[] = [];
|
|
1420
|
+
let lifecyclePassesRef: AnyPass[] | null = null;
|
|
950
1421
|
let passWidth = 0;
|
|
951
1422
|
let passHeight = 0;
|
|
952
1423
|
|
|
953
1424
|
/**
|
|
954
1425
|
* Resolves active render pass list for current frame.
|
|
955
1426
|
*/
|
|
956
|
-
const resolvePasses = ():
|
|
1427
|
+
const resolvePasses = (): AnyPass[] => {
|
|
957
1428
|
return options.getPasses?.() ?? options.passes ?? [];
|
|
958
1429
|
};
|
|
959
1430
|
|
|
@@ -968,7 +1439,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
968
1439
|
* Checks whether cached render-graph plan can be reused for this frame.
|
|
969
1440
|
*/
|
|
970
1441
|
const isGraphPlanCacheValid = (
|
|
971
|
-
passes:
|
|
1442
|
+
passes: AnyPass[],
|
|
972
1443
|
clearColor: [number, number, number, number]
|
|
973
1444
|
): boolean => {
|
|
974
1445
|
if (!cachedGraphPlan) {
|
|
@@ -994,6 +1465,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
994
1465
|
|
|
995
1466
|
for (let index = 0; index < passes.length; index += 1) {
|
|
996
1467
|
const pass = passes[index];
|
|
1468
|
+
const rp = pass as Partial<RenderPass>;
|
|
997
1469
|
const snapshot = cachedGraphPasses[index];
|
|
998
1470
|
if (!pass || !snapshot || snapshot.pass !== pass) {
|
|
999
1471
|
return false;
|
|
@@ -1001,16 +1473,16 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1001
1473
|
|
|
1002
1474
|
if (
|
|
1003
1475
|
snapshot.enabled !== pass.enabled ||
|
|
1004
|
-
snapshot.needsSwap !==
|
|
1005
|
-
snapshot.input !==
|
|
1006
|
-
snapshot.output !==
|
|
1007
|
-
snapshot.clear !==
|
|
1008
|
-
snapshot.preserve !==
|
|
1476
|
+
snapshot.needsSwap !== rp.needsSwap ||
|
|
1477
|
+
snapshot.input !== rp.input ||
|
|
1478
|
+
snapshot.output !== rp.output ||
|
|
1479
|
+
snapshot.clear !== rp.clear ||
|
|
1480
|
+
snapshot.preserve !== rp.preserve
|
|
1009
1481
|
) {
|
|
1010
1482
|
return false;
|
|
1011
1483
|
}
|
|
1012
1484
|
|
|
1013
|
-
const passClearColor =
|
|
1485
|
+
const passClearColor = rp.clearColor;
|
|
1014
1486
|
const hasPassClearColor = passClearColor !== undefined;
|
|
1015
1487
|
if (snapshot.hasClearColor !== hasPassClearColor) {
|
|
1016
1488
|
return false;
|
|
@@ -1035,7 +1507,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1035
1507
|
* Updates render-graph cache with current pass set.
|
|
1036
1508
|
*/
|
|
1037
1509
|
const updateGraphPlanCache = (
|
|
1038
|
-
passes:
|
|
1510
|
+
passes: AnyPass[],
|
|
1039
1511
|
clearColor: [number, number, number, number],
|
|
1040
1512
|
graphPlan: RenderGraphPlan
|
|
1041
1513
|
): void => {
|
|
@@ -1049,18 +1521,19 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1049
1521
|
|
|
1050
1522
|
let index = 0;
|
|
1051
1523
|
for (const pass of passes) {
|
|
1052
|
-
const
|
|
1524
|
+
const rp = pass as Partial<RenderPass>;
|
|
1525
|
+
const passClearColor = rp.clearColor;
|
|
1053
1526
|
const hasPassClearColor = passClearColor !== undefined;
|
|
1054
1527
|
const snapshot = cachedGraphPasses[index];
|
|
1055
1528
|
if (!snapshot) {
|
|
1056
1529
|
cachedGraphPasses[index] = {
|
|
1057
1530
|
pass,
|
|
1058
1531
|
enabled: pass.enabled,
|
|
1059
|
-
needsSwap:
|
|
1060
|
-
input:
|
|
1061
|
-
output:
|
|
1062
|
-
clear:
|
|
1063
|
-
preserve:
|
|
1532
|
+
needsSwap: rp.needsSwap,
|
|
1533
|
+
input: rp.input,
|
|
1534
|
+
output: rp.output,
|
|
1535
|
+
clear: rp.clear,
|
|
1536
|
+
preserve: rp.preserve,
|
|
1064
1537
|
hasClearColor: hasPassClearColor,
|
|
1065
1538
|
clearColor0: passClearColor?.[0] ?? 0,
|
|
1066
1539
|
clearColor1: passClearColor?.[1] ?? 0,
|
|
@@ -1073,11 +1546,11 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1073
1546
|
|
|
1074
1547
|
snapshot.pass = pass;
|
|
1075
1548
|
snapshot.enabled = pass.enabled;
|
|
1076
|
-
snapshot.needsSwap =
|
|
1077
|
-
snapshot.input =
|
|
1078
|
-
snapshot.output =
|
|
1079
|
-
snapshot.clear =
|
|
1080
|
-
snapshot.preserve =
|
|
1549
|
+
snapshot.needsSwap = rp.needsSwap;
|
|
1550
|
+
snapshot.input = rp.input;
|
|
1551
|
+
snapshot.output = rp.output;
|
|
1552
|
+
snapshot.clear = rp.clear;
|
|
1553
|
+
snapshot.preserve = rp.preserve;
|
|
1081
1554
|
snapshot.hasClearColor = hasPassClearColor;
|
|
1082
1555
|
snapshot.clearColor0 = passClearColor?.[0] ?? 0;
|
|
1083
1556
|
snapshot.clearColor1 = passClearColor?.[1] ?? 0;
|
|
@@ -1090,7 +1563,7 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1090
1563
|
/**
|
|
1091
1564
|
* Synchronizes pass lifecycle callbacks and resize notifications.
|
|
1092
1565
|
*/
|
|
1093
|
-
const syncPassLifecycle = (passes:
|
|
1566
|
+
const syncPassLifecycle = (passes: AnyPass[], width: number, height: number): void => {
|
|
1094
1567
|
const resized = passWidth !== width || passHeight !== height;
|
|
1095
1568
|
if (!resized && lifecyclePassesRef === passes && passes.length === activePasses.length) {
|
|
1096
1569
|
let isSameOrder = true;
|
|
@@ -1292,7 +1765,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1292
1765
|
renderMode,
|
|
1293
1766
|
uniforms,
|
|
1294
1767
|
textures,
|
|
1295
|
-
canvasSize
|
|
1768
|
+
canvasSize,
|
|
1769
|
+
pendingStorageWrites
|
|
1296
1770
|
}) => {
|
|
1297
1771
|
if (deviceLostMessage) {
|
|
1298
1772
|
throw new Error(deviceLostMessage);
|
|
@@ -1361,6 +1835,8 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1361
1835
|
|
|
1362
1836
|
let bindGroupDirty = false;
|
|
1363
1837
|
for (const binding of textureBindings) {
|
|
1838
|
+
// Storage textures are managed by compute passes, skip source-driven updates
|
|
1839
|
+
if (storageTextureKeySet.has(binding.key)) continue;
|
|
1364
1840
|
const nextTexture =
|
|
1365
1841
|
textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null;
|
|
1366
1842
|
if (updateTextureBinding(binding, nextTexture, renderMode)) {
|
|
@@ -1372,6 +1848,23 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1372
1848
|
bindGroup = createBindGroup();
|
|
1373
1849
|
}
|
|
1374
1850
|
|
|
1851
|
+
// Apply pending storage buffer writes
|
|
1852
|
+
if (pendingStorageWrites) {
|
|
1853
|
+
for (const write of pendingStorageWrites) {
|
|
1854
|
+
const buffer = storageBufferMap.get(write.name);
|
|
1855
|
+
if (buffer) {
|
|
1856
|
+
const data = write.data;
|
|
1857
|
+
device.queue.writeBuffer(
|
|
1858
|
+
buffer,
|
|
1859
|
+
write.offset,
|
|
1860
|
+
data.buffer as ArrayBuffer,
|
|
1861
|
+
data.byteOffset,
|
|
1862
|
+
data.byteLength
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1375
1868
|
const commandEncoder = device.createCommandEncoder();
|
|
1376
1869
|
const passes = resolvePasses();
|
|
1377
1870
|
const clearColor = options.getClearColor();
|
|
@@ -1402,6 +1895,98 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1402
1895
|
: null;
|
|
1403
1896
|
const sceneOutput = slots ? slots.source : canvasSurface;
|
|
1404
1897
|
|
|
1898
|
+
// Dispatch compute passes BEFORE scene render so storage textures
|
|
1899
|
+
// and buffers are up-to-date when the fragment shader samples them.
|
|
1900
|
+
if (slots) {
|
|
1901
|
+
for (const step of graphPlan.steps) {
|
|
1902
|
+
if (step.kind !== 'compute') {
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
const computePass = step.pass as {
|
|
1906
|
+
isCompute?: boolean;
|
|
1907
|
+
getCompute?: () => string;
|
|
1908
|
+
resolveDispatch?: (ctx: {
|
|
1909
|
+
width: number;
|
|
1910
|
+
height: number;
|
|
1911
|
+
time: number;
|
|
1912
|
+
delta: number;
|
|
1913
|
+
workgroupSize: [number, number, number];
|
|
1914
|
+
}) => [number, number, number];
|
|
1915
|
+
getWorkgroupSize?: () => [number, number, number];
|
|
1916
|
+
isPingPong?: boolean;
|
|
1917
|
+
getTarget?: () => string;
|
|
1918
|
+
getCurrentOutput?: () => string;
|
|
1919
|
+
getIterations?: () => number;
|
|
1920
|
+
advanceFrame?: () => void;
|
|
1921
|
+
};
|
|
1922
|
+
if (
|
|
1923
|
+
computePass.getCompute &&
|
|
1924
|
+
computePass.resolveDispatch &&
|
|
1925
|
+
computePass.getWorkgroupSize
|
|
1926
|
+
) {
|
|
1927
|
+
const computeSource = computePass.getCompute();
|
|
1928
|
+
const pingPongTarget =
|
|
1929
|
+
computePass.isPingPong && computePass.getTarget ? computePass.getTarget() : undefined;
|
|
1930
|
+
if (computePass.isPingPong && !pingPongTarget) {
|
|
1931
|
+
throw new Error('PingPongComputePass must provide a target texture key.');
|
|
1932
|
+
}
|
|
1933
|
+
const pingPongPair = pingPongTarget ? ensurePingPongTexturePair(pingPongTarget) : null;
|
|
1934
|
+
const pipelineEntry = buildComputePipelineEntry({
|
|
1935
|
+
computeSource,
|
|
1936
|
+
...(pingPongPair
|
|
1937
|
+
? {
|
|
1938
|
+
pingPongTarget: pingPongPair.target,
|
|
1939
|
+
pingPongFormat: pingPongPair.format
|
|
1940
|
+
}
|
|
1941
|
+
: {})
|
|
1942
|
+
});
|
|
1943
|
+
const workgroupSize = computePass.getWorkgroupSize();
|
|
1944
|
+
const storageBindGroup = getComputeStorageBindGroup();
|
|
1945
|
+
const storageTextureBindGroup = getComputeStorageTextureBindGroup();
|
|
1946
|
+
const iterations =
|
|
1947
|
+
computePass.isPingPong && computePass.getIterations ? computePass.getIterations() : 1;
|
|
1948
|
+
const currentOutput =
|
|
1949
|
+
computePass.isPingPong && computePass.getCurrentOutput
|
|
1950
|
+
? computePass.getCurrentOutput()
|
|
1951
|
+
: null;
|
|
1952
|
+
const readFromAAtIterationZero =
|
|
1953
|
+
pingPongPair && currentOutput ? currentOutput !== `${pingPongPair.target}B` : true;
|
|
1954
|
+
|
|
1955
|
+
for (let iter = 0; iter < iterations; iter += 1) {
|
|
1956
|
+
const dispatch = computePass.resolveDispatch({
|
|
1957
|
+
width,
|
|
1958
|
+
height,
|
|
1959
|
+
time,
|
|
1960
|
+
delta,
|
|
1961
|
+
workgroupSize
|
|
1962
|
+
});
|
|
1963
|
+
const cPass = commandEncoder.beginComputePass();
|
|
1964
|
+
cPass.setPipeline(pipelineEntry.pipeline);
|
|
1965
|
+
cPass.setBindGroup(0, pipelineEntry.bindGroup);
|
|
1966
|
+
if (storageBindGroup) {
|
|
1967
|
+
cPass.setBindGroup(1, storageBindGroup);
|
|
1968
|
+
}
|
|
1969
|
+
if (pingPongPair) {
|
|
1970
|
+
const readFromA =
|
|
1971
|
+
iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
|
|
1972
|
+
cPass.setBindGroup(
|
|
1973
|
+
2,
|
|
1974
|
+
getPingPongStorageTextureBindGroup(pingPongPair.target, readFromA)
|
|
1975
|
+
);
|
|
1976
|
+
} else if (storageTextureBindGroup) {
|
|
1977
|
+
cPass.setBindGroup(2, storageTextureBindGroup);
|
|
1978
|
+
}
|
|
1979
|
+
cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
|
|
1980
|
+
cPass.end();
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (computePass.isPingPong && computePass.advanceFrame) {
|
|
1984
|
+
computePass.advanceFrame();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1405
1990
|
const scenePass = commandEncoder.beginRenderPass({
|
|
1406
1991
|
colorAttachments: [
|
|
1407
1992
|
{
|
|
@@ -1420,6 +2005,9 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1420
2005
|
|
|
1421
2006
|
scenePass.setPipeline(pipeline);
|
|
1422
2007
|
scenePass.setBindGroup(0, bindGroup);
|
|
2008
|
+
if (fragmentStorageBindGroup) {
|
|
2009
|
+
scenePass.setBindGroup(1, fragmentStorageBindGroup);
|
|
2010
|
+
}
|
|
1423
2011
|
scenePass.draw(3);
|
|
1424
2012
|
scenePass.end();
|
|
1425
2013
|
|
|
@@ -1448,10 +2036,15 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1448
2036
|
};
|
|
1449
2037
|
|
|
1450
2038
|
for (const step of graphPlan.steps) {
|
|
2039
|
+
// Compute passes already dispatched above
|
|
2040
|
+
if (step.kind === 'compute') {
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1451
2044
|
const input = resolveStepSurface(step.input);
|
|
1452
2045
|
const output = resolveStepSurface(step.output);
|
|
1453
2046
|
|
|
1454
|
-
step.pass.render({
|
|
2047
|
+
(step.pass as RenderPass).render({
|
|
1455
2048
|
device,
|
|
1456
2049
|
commandEncoder,
|
|
1457
2050
|
source: slots.source,
|
|
@@ -1467,7 +2060,12 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1467
2060
|
clear: step.clear,
|
|
1468
2061
|
clearColor: step.clearColor,
|
|
1469
2062
|
preserve: step.preserve,
|
|
1470
|
-
beginRenderPass: (passOptions
|
|
2063
|
+
beginRenderPass: (passOptions?: {
|
|
2064
|
+
clear?: boolean;
|
|
2065
|
+
clearColor?: [number, number, number, number];
|
|
2066
|
+
preserve?: boolean;
|
|
2067
|
+
view?: GPUTextureView;
|
|
2068
|
+
}) => {
|
|
1471
2069
|
const clear = passOptions?.clear ?? step.clear;
|
|
1472
2070
|
const clearColor = passOptions?.clearColor ?? step.clearColor;
|
|
1473
2071
|
const preserve = passOptions?.preserve ?? step.preserve;
|
|
@@ -1510,11 +2108,27 @@ export async function createRenderer(options: RendererOptions): Promise<Renderer
|
|
|
1510
2108
|
initializationCleanups.length = 0;
|
|
1511
2109
|
return {
|
|
1512
2110
|
render,
|
|
2111
|
+
getStorageBuffer: (name: string): GPUBuffer | undefined => {
|
|
2112
|
+
return storageBufferMap.get(name);
|
|
2113
|
+
},
|
|
2114
|
+
getDevice: (): GPUDevice => {
|
|
2115
|
+
return device;
|
|
2116
|
+
},
|
|
1513
2117
|
destroy: () => {
|
|
1514
2118
|
isDestroyed = true;
|
|
1515
2119
|
device.removeEventListener('uncapturederror', handleUncapturedError);
|
|
1516
2120
|
frameBuffer.destroy();
|
|
1517
2121
|
uniformBuffer.destroy();
|
|
2122
|
+
for (const buffer of storageBufferMap.values()) {
|
|
2123
|
+
buffer.destroy();
|
|
2124
|
+
}
|
|
2125
|
+
storageBufferMap.clear();
|
|
2126
|
+
for (const pair of pingPongTexturePairs.values()) {
|
|
2127
|
+
pair.textureA.destroy();
|
|
2128
|
+
pair.textureB.destroy();
|
|
2129
|
+
}
|
|
2130
|
+
pingPongTexturePairs.clear();
|
|
2131
|
+
computePipelineCache.clear();
|
|
1518
2132
|
destroyRenderTexture(sourceSlotTarget);
|
|
1519
2133
|
destroyRenderTexture(targetSlotTarget);
|
|
1520
2134
|
for (const target of runtimeRenderTargets.values()) {
|