@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/dist/core/renderer.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { packUniformsInto } from "./uniforms.js";
|
|
2
2
|
import { getTextureMipLevelCount, normalizeTextureDefinitions, resolveTextureSize, resolveTextureUpdateMode, toTextureData } from "./textures.js";
|
|
3
|
+
import { normalizeStorageBufferDefinition } from "./storage-buffers.js";
|
|
3
4
|
import { attachShaderCompilationDiagnostics } from "./error-diagnostics.js";
|
|
4
5
|
import { buildShaderSourceWithMap, formatShaderSourceLocation } from "./shader.js";
|
|
5
6
|
import { buildRenderTargetSignature, resolveRenderTargetDefinitions } from "./render-targets.js";
|
|
6
7
|
import { planRenderGraph } from "./render-graph.js";
|
|
8
|
+
import { buildComputeShaderSource, buildPingPongComputeShaderSource, extractWorkgroupSize, storageTextureSampleScalarType } from "./compute-shader.js";
|
|
7
9
|
//#region src/lib/core/renderer.ts
|
|
8
10
|
/**
|
|
9
11
|
* Binding index for frame uniforms (`time`, `delta`, `resolution`).
|
|
@@ -28,6 +30,14 @@ function getTextureBindings(index) {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
33
|
+
* Maps WGSL scalar texture type to WebGPU sampled texture bind-group sample type.
|
|
34
|
+
*/
|
|
35
|
+
function toGpuTextureSampleType(type) {
|
|
36
|
+
if (type === "u32") return "uint";
|
|
37
|
+
if (type === "i32") return "sint";
|
|
38
|
+
return "float";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
31
41
|
* Resizes canvas backing store to match client size and DPR.
|
|
32
42
|
*/
|
|
33
43
|
function resizeCanvas(canvas, dprInput, cssSize) {
|
|
@@ -85,9 +95,11 @@ function buildPassGraphSnapshot(passes) {
|
|
|
85
95
|
for (const pass of declaredPasses) {
|
|
86
96
|
if (pass.enabled === false) continue;
|
|
87
97
|
enabledPassCount += 1;
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
98
|
+
if ("isCompute" in pass && pass.isCompute === true) continue;
|
|
99
|
+
const rp = pass;
|
|
100
|
+
const needsSwap = rp.needsSwap ?? true;
|
|
101
|
+
const input = rp.input ?? "source";
|
|
102
|
+
const output = rp.output ?? (needsSwap ? "target" : "source");
|
|
91
103
|
inputs.push(input);
|
|
92
104
|
outputs.push(output);
|
|
93
105
|
}
|
|
@@ -387,7 +399,9 @@ async function createRenderer(options) {
|
|
|
387
399
|
const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
|
|
388
400
|
const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, options.textureKeys, {
|
|
389
401
|
convertLinearToSrgb,
|
|
390
|
-
fragmentLineMap: options.fragmentLineMap
|
|
402
|
+
fragmentLineMap: options.fragmentLineMap,
|
|
403
|
+
...options.storageBufferKeys !== void 0 ? { storageBufferKeys: options.storageBufferKeys } : {},
|
|
404
|
+
...options.storageBufferDefinitions !== void 0 ? { storageBufferDefinitions: options.storageBufferDefinitions } : {}
|
|
391
405
|
});
|
|
392
406
|
const shaderModule = device.createShaderModule({ code: builtShader.code });
|
|
393
407
|
await assertCompilation(shaderModule, {
|
|
@@ -399,6 +413,10 @@ async function createRenderer(options) {
|
|
|
399
413
|
runtimeContext
|
|
400
414
|
});
|
|
401
415
|
const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
|
|
416
|
+
const storageBufferKeys = options.storageBufferKeys ?? [];
|
|
417
|
+
const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
|
|
418
|
+
const storageTextureKeys = options.storageTextureKeys ?? [];
|
|
419
|
+
const storageTextureKeySet = new Set(storageTextureKeys);
|
|
402
420
|
const textureBindings = options.textureKeys.map((key, index) => {
|
|
403
421
|
const config = normalizedTextureDefinitions[key];
|
|
404
422
|
if (!config) throw new Error(`Missing texture definition for "${key}"`);
|
|
@@ -411,7 +429,7 @@ async function createRenderer(options) {
|
|
|
411
429
|
addressModeV: config.addressModeV,
|
|
412
430
|
maxAnisotropy: config.filter === "linear" ? config.anisotropy : 1
|
|
413
431
|
});
|
|
414
|
-
const fallbackTexture = createFallbackTexture(device, config.format);
|
|
432
|
+
const fallbackTexture = createFallbackTexture(device, config.storage ? "rgba8unorm" : config.format);
|
|
415
433
|
registerInitializationCleanup(() => {
|
|
416
434
|
fallbackTexture.destroy();
|
|
417
435
|
});
|
|
@@ -442,10 +460,34 @@ async function createRenderer(options) {
|
|
|
442
460
|
lastToken: null
|
|
443
461
|
};
|
|
444
462
|
if (config.update !== void 0) runtimeBinding.defaultUpdate = config.update;
|
|
463
|
+
if (config.storage && config.width && config.height) {
|
|
464
|
+
const storageUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
|
|
465
|
+
const storageTexture = device.createTexture({
|
|
466
|
+
size: {
|
|
467
|
+
width: config.width,
|
|
468
|
+
height: config.height,
|
|
469
|
+
depthOrArrayLayers: 1
|
|
470
|
+
},
|
|
471
|
+
format: config.format,
|
|
472
|
+
usage: storageUsage
|
|
473
|
+
});
|
|
474
|
+
registerInitializationCleanup(() => {
|
|
475
|
+
storageTexture.destroy();
|
|
476
|
+
});
|
|
477
|
+
runtimeBinding.texture = storageTexture;
|
|
478
|
+
runtimeBinding.view = storageTexture.createView();
|
|
479
|
+
runtimeBinding.width = config.width;
|
|
480
|
+
runtimeBinding.height = config.height;
|
|
481
|
+
}
|
|
445
482
|
return runtimeBinding;
|
|
446
483
|
});
|
|
447
484
|
const bindGroupLayout = device.createBindGroupLayout({ entries: createBindGroupLayoutEntries(textureBindings) });
|
|
448
|
-
const
|
|
485
|
+
const fragmentStorageBindGroupLayout = storageBufferKeys.length > 0 ? device.createBindGroupLayout({ entries: storageBufferKeys.map((_, index) => ({
|
|
486
|
+
binding: index,
|
|
487
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
488
|
+
buffer: { type: "read-only-storage" }
|
|
489
|
+
})) }) : null;
|
|
490
|
+
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: fragmentStorageBindGroupLayout ? [bindGroupLayout, fragmentStorageBindGroupLayout] : [bindGroupLayout] });
|
|
449
491
|
const pipeline = device.createRenderPipeline({
|
|
450
492
|
layout: pipelineLayout,
|
|
451
493
|
vertex: {
|
|
@@ -495,6 +537,283 @@ async function createRenderer(options) {
|
|
|
495
537
|
addressModeV: "clamp-to-edge"
|
|
496
538
|
});
|
|
497
539
|
let blitBindGroupByView = /* @__PURE__ */ new WeakMap();
|
|
540
|
+
const storageBufferMap = /* @__PURE__ */ new Map();
|
|
541
|
+
const pingPongTexturePairs = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const key of storageBufferKeys) {
|
|
543
|
+
const definition = storageBufferDefinitions[key];
|
|
544
|
+
if (!definition) continue;
|
|
545
|
+
const normalized = normalizeStorageBufferDefinition(definition);
|
|
546
|
+
const buffer = device.createBuffer({
|
|
547
|
+
size: normalized.size,
|
|
548
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
549
|
+
});
|
|
550
|
+
registerInitializationCleanup(() => {
|
|
551
|
+
buffer.destroy();
|
|
552
|
+
});
|
|
553
|
+
if (definition.initialData) {
|
|
554
|
+
const data = definition.initialData;
|
|
555
|
+
device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
|
|
556
|
+
}
|
|
557
|
+
storageBufferMap.set(key, buffer);
|
|
558
|
+
}
|
|
559
|
+
const fragmentStorageBindGroup = fragmentStorageBindGroupLayout && storageBufferKeys.length > 0 ? device.createBindGroup({
|
|
560
|
+
layout: fragmentStorageBindGroupLayout,
|
|
561
|
+
entries: storageBufferKeys.map((key, index) => {
|
|
562
|
+
const buffer = storageBufferMap.get(key);
|
|
563
|
+
if (!buffer) throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
564
|
+
return {
|
|
565
|
+
binding: index,
|
|
566
|
+
resource: { buffer }
|
|
567
|
+
};
|
|
568
|
+
})
|
|
569
|
+
}) : null;
|
|
570
|
+
const ensurePingPongTexturePair = (target) => {
|
|
571
|
+
const existing = pingPongTexturePairs.get(target);
|
|
572
|
+
if (existing) return existing;
|
|
573
|
+
const config = normalizedTextureDefinitions[target];
|
|
574
|
+
if (!config || !config.storage) throw new Error(`PingPongComputePass target "${target}" must reference a texture declared with storage:true.`);
|
|
575
|
+
if (!config.width || !config.height) throw new Error(`PingPongComputePass target "${target}" requires explicit texture width and height.`);
|
|
576
|
+
const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
|
|
577
|
+
const textureA = device.createTexture({
|
|
578
|
+
size: {
|
|
579
|
+
width: config.width,
|
|
580
|
+
height: config.height,
|
|
581
|
+
depthOrArrayLayers: 1
|
|
582
|
+
},
|
|
583
|
+
format: config.format,
|
|
584
|
+
usage
|
|
585
|
+
});
|
|
586
|
+
const textureB = device.createTexture({
|
|
587
|
+
size: {
|
|
588
|
+
width: config.width,
|
|
589
|
+
height: config.height,
|
|
590
|
+
depthOrArrayLayers: 1
|
|
591
|
+
},
|
|
592
|
+
format: config.format,
|
|
593
|
+
usage
|
|
594
|
+
});
|
|
595
|
+
registerInitializationCleanup(() => {
|
|
596
|
+
textureA.destroy();
|
|
597
|
+
});
|
|
598
|
+
registerInitializationCleanup(() => {
|
|
599
|
+
textureB.destroy();
|
|
600
|
+
});
|
|
601
|
+
const sampleType = toGpuTextureSampleType(storageTextureSampleScalarType(config.format));
|
|
602
|
+
const bindGroupLayout = device.createBindGroupLayout({ entries: [{
|
|
603
|
+
binding: 0,
|
|
604
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
605
|
+
texture: {
|
|
606
|
+
sampleType,
|
|
607
|
+
viewDimension: "2d",
|
|
608
|
+
multisampled: false
|
|
609
|
+
}
|
|
610
|
+
}, {
|
|
611
|
+
binding: 1,
|
|
612
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
613
|
+
storageTexture: {
|
|
614
|
+
access: "write-only",
|
|
615
|
+
format: config.format,
|
|
616
|
+
viewDimension: "2d"
|
|
617
|
+
}
|
|
618
|
+
}] });
|
|
619
|
+
const pair = {
|
|
620
|
+
target,
|
|
621
|
+
format: config.format,
|
|
622
|
+
width: config.width,
|
|
623
|
+
height: config.height,
|
|
624
|
+
textureA,
|
|
625
|
+
viewA: textureA.createView(),
|
|
626
|
+
textureB,
|
|
627
|
+
viewB: textureB.createView(),
|
|
628
|
+
bindGroupLayout
|
|
629
|
+
};
|
|
630
|
+
pingPongTexturePairs.set(target, pair);
|
|
631
|
+
return pair;
|
|
632
|
+
};
|
|
633
|
+
const computePipelineCache = /* @__PURE__ */ new Map();
|
|
634
|
+
const buildComputePipelineEntry = (buildOptions) => {
|
|
635
|
+
const cacheKey = buildOptions.pingPongTarget && buildOptions.pingPongFormat ? `pingpong:${buildOptions.pingPongTarget}:${buildOptions.pingPongFormat}:${buildOptions.computeSource}` : `compute:${buildOptions.computeSource}`;
|
|
636
|
+
const cached = computePipelineCache.get(cacheKey);
|
|
637
|
+
if (cached) return cached;
|
|
638
|
+
const storageBufferDefs = {};
|
|
639
|
+
for (const key of storageBufferKeys) {
|
|
640
|
+
const def = storageBufferDefinitions[key];
|
|
641
|
+
if (def) {
|
|
642
|
+
const norm = normalizeStorageBufferDefinition(def);
|
|
643
|
+
storageBufferDefs[key] = {
|
|
644
|
+
type: norm.type,
|
|
645
|
+
access: norm.access
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const storageTextureDefs = {};
|
|
650
|
+
for (const key of storageTextureKeys) {
|
|
651
|
+
const texDef = options.textureDefinitions[key];
|
|
652
|
+
if (texDef?.format) storageTextureDefs[key] = { format: texDef.format };
|
|
653
|
+
}
|
|
654
|
+
const isPingPongPipeline = Boolean(buildOptions.pingPongTarget && buildOptions.pingPongFormat);
|
|
655
|
+
const shaderCode = isPingPongPipeline ? buildPingPongComputeShaderSource({
|
|
656
|
+
compute: buildOptions.computeSource,
|
|
657
|
+
uniformLayout: options.uniformLayout,
|
|
658
|
+
storageBufferKeys,
|
|
659
|
+
storageBufferDefinitions: storageBufferDefs,
|
|
660
|
+
target: buildOptions.pingPongTarget,
|
|
661
|
+
targetFormat: buildOptions.pingPongFormat
|
|
662
|
+
}) : buildComputeShaderSource({
|
|
663
|
+
compute: buildOptions.computeSource,
|
|
664
|
+
uniformLayout: options.uniformLayout,
|
|
665
|
+
storageBufferKeys,
|
|
666
|
+
storageBufferDefinitions: storageBufferDefs,
|
|
667
|
+
storageTextureKeys,
|
|
668
|
+
storageTextureDefinitions: storageTextureDefs
|
|
669
|
+
});
|
|
670
|
+
const computeShaderModule = device.createShaderModule({ code: shaderCode });
|
|
671
|
+
const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
|
|
672
|
+
const computeUniformBGL = device.createBindGroupLayout({ entries: [{
|
|
673
|
+
binding: FRAME_BINDING,
|
|
674
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
675
|
+
buffer: {
|
|
676
|
+
type: "uniform",
|
|
677
|
+
minBindingSize: 16
|
|
678
|
+
}
|
|
679
|
+
}, {
|
|
680
|
+
binding: UNIFORM_BINDING,
|
|
681
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
682
|
+
buffer: { type: "uniform" }
|
|
683
|
+
}] });
|
|
684
|
+
const storageBGLEntries = storageBufferKeys.map((key, index) => {
|
|
685
|
+
const bufferType = (storageBufferDefinitions[key]?.access ?? "read-write") === "read" ? "read-only-storage" : "storage";
|
|
686
|
+
return {
|
|
687
|
+
binding: index,
|
|
688
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
689
|
+
buffer: { type: bufferType }
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
const storageBGL = storageBGLEntries.length > 0 ? device.createBindGroupLayout({ entries: storageBGLEntries }) : null;
|
|
693
|
+
const storageTextureBGLEntries = isPingPongPipeline ? [{
|
|
694
|
+
binding: 0,
|
|
695
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
696
|
+
texture: {
|
|
697
|
+
sampleType: toGpuTextureSampleType(storageTextureSampleScalarType(buildOptions.pingPongFormat)),
|
|
698
|
+
viewDimension: "2d",
|
|
699
|
+
multisampled: false
|
|
700
|
+
}
|
|
701
|
+
}, {
|
|
702
|
+
binding: 1,
|
|
703
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
704
|
+
storageTexture: {
|
|
705
|
+
access: "write-only",
|
|
706
|
+
format: buildOptions.pingPongFormat,
|
|
707
|
+
viewDimension: "2d"
|
|
708
|
+
}
|
|
709
|
+
}] : storageTextureKeys.map((key, index) => {
|
|
710
|
+
const texDef = options.textureDefinitions[key];
|
|
711
|
+
return {
|
|
712
|
+
binding: index,
|
|
713
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
714
|
+
storageTexture: {
|
|
715
|
+
access: "write-only",
|
|
716
|
+
format: texDef?.format ?? "rgba8unorm",
|
|
717
|
+
viewDimension: "2d"
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
});
|
|
721
|
+
const storageTextureBGL = storageTextureBGLEntries.length > 0 ? device.createBindGroupLayout({ entries: storageTextureBGLEntries }) : null;
|
|
722
|
+
const bindGroupLayouts = [computeUniformBGL];
|
|
723
|
+
if (storageBGL || storageTextureBGL) bindGroupLayouts.push(storageBGL ?? device.createBindGroupLayout({ entries: [] }));
|
|
724
|
+
if (storageTextureBGL) bindGroupLayouts.push(storageTextureBGL);
|
|
725
|
+
const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
|
|
726
|
+
const entry = {
|
|
727
|
+
pipeline: device.createComputePipeline({
|
|
728
|
+
layout: computePipelineLayout,
|
|
729
|
+
compute: {
|
|
730
|
+
module: computeShaderModule,
|
|
731
|
+
entryPoint: "compute"
|
|
732
|
+
}
|
|
733
|
+
}),
|
|
734
|
+
bindGroup: device.createBindGroup({
|
|
735
|
+
layout: computeUniformBGL,
|
|
736
|
+
entries: [{
|
|
737
|
+
binding: FRAME_BINDING,
|
|
738
|
+
resource: { buffer: frameBuffer }
|
|
739
|
+
}, {
|
|
740
|
+
binding: UNIFORM_BINDING,
|
|
741
|
+
resource: { buffer: uniformBuffer }
|
|
742
|
+
}]
|
|
743
|
+
}),
|
|
744
|
+
workgroupSize,
|
|
745
|
+
computeSource: buildOptions.computeSource
|
|
746
|
+
};
|
|
747
|
+
computePipelineCache.set(cacheKey, entry);
|
|
748
|
+
return entry;
|
|
749
|
+
};
|
|
750
|
+
const getComputeStorageBindGroup = () => {
|
|
751
|
+
if (storageBufferKeys.length === 0) return null;
|
|
752
|
+
const storageBGLEntries = storageBufferKeys.map((key, index) => {
|
|
753
|
+
const bufferType = (storageBufferDefinitions[key]?.access ?? "read-write") === "read" ? "read-only-storage" : "storage";
|
|
754
|
+
return {
|
|
755
|
+
binding: index,
|
|
756
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
757
|
+
buffer: { type: bufferType }
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
const storageBGL = device.createBindGroupLayout({ entries: storageBGLEntries });
|
|
761
|
+
const storageEntries = storageBufferKeys.map((key, index) => {
|
|
762
|
+
const buffer = storageBufferMap.get(key);
|
|
763
|
+
if (!buffer) throw new Error(`Storage buffer "${key}" not allocated.`);
|
|
764
|
+
return {
|
|
765
|
+
binding: index,
|
|
766
|
+
resource: { buffer }
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
return device.createBindGroup({
|
|
770
|
+
layout: storageBGL,
|
|
771
|
+
entries: storageEntries
|
|
772
|
+
});
|
|
773
|
+
};
|
|
774
|
+
const getComputeStorageTextureBindGroup = () => {
|
|
775
|
+
if (storageTextureKeys.length === 0) return null;
|
|
776
|
+
const entries = storageTextureKeys.map((key, index) => {
|
|
777
|
+
const texDef = options.textureDefinitions[key];
|
|
778
|
+
return {
|
|
779
|
+
binding: index,
|
|
780
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
781
|
+
storageTexture: {
|
|
782
|
+
access: "write-only",
|
|
783
|
+
format: texDef?.format ?? "rgba8unorm",
|
|
784
|
+
viewDimension: "2d"
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
const bgl = device.createBindGroupLayout({ entries });
|
|
789
|
+
const bgEntries = storageTextureKeys.map((key, index) => {
|
|
790
|
+
const binding = textureBindings.find((b) => b.key === key);
|
|
791
|
+
if (!binding || !binding.texture) throw new Error(`Storage texture "${key}" not allocated.`);
|
|
792
|
+
return {
|
|
793
|
+
binding: index,
|
|
794
|
+
resource: binding.view
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
return device.createBindGroup({
|
|
798
|
+
layout: bgl,
|
|
799
|
+
entries: bgEntries
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
const getPingPongStorageTextureBindGroup = (target, readFromA) => {
|
|
803
|
+
const pair = ensurePingPongTexturePair(target);
|
|
804
|
+
const readView = readFromA ? pair.viewA : pair.viewB;
|
|
805
|
+
const writeView = readFromA ? pair.viewB : pair.viewA;
|
|
806
|
+
return device.createBindGroup({
|
|
807
|
+
layout: pair.bindGroupLayout,
|
|
808
|
+
entries: [{
|
|
809
|
+
binding: 0,
|
|
810
|
+
resource: readView
|
|
811
|
+
}, {
|
|
812
|
+
binding: 1,
|
|
813
|
+
resource: writeView
|
|
814
|
+
}]
|
|
815
|
+
});
|
|
816
|
+
};
|
|
498
817
|
const frameBuffer = device.createBuffer({
|
|
499
818
|
size: 16,
|
|
500
819
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
@@ -588,6 +907,8 @@ async function createRenderer(options) {
|
|
|
588
907
|
binding.lastToken = value;
|
|
589
908
|
return false;
|
|
590
909
|
}
|
|
910
|
+
let textureUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
|
|
911
|
+
if (storageTextureKeySet.has(binding.key)) textureUsage |= GPUTextureUsage.STORAGE_BINDING;
|
|
591
912
|
const texture = device.createTexture({
|
|
592
913
|
size: {
|
|
593
914
|
width,
|
|
@@ -596,7 +917,7 @@ async function createRenderer(options) {
|
|
|
596
917
|
},
|
|
597
918
|
format,
|
|
598
919
|
mipLevelCount,
|
|
599
|
-
usage:
|
|
920
|
+
usage: textureUsage
|
|
600
921
|
});
|
|
601
922
|
registerInitializationCleanup(() => {
|
|
602
923
|
texture.destroy();
|
|
@@ -618,7 +939,10 @@ async function createRenderer(options) {
|
|
|
618
939
|
binding.lastToken = value;
|
|
619
940
|
return true;
|
|
620
941
|
};
|
|
621
|
-
for (const binding of textureBindings)
|
|
942
|
+
for (const binding of textureBindings) {
|
|
943
|
+
if (storageTextureKeySet.has(binding.key)) continue;
|
|
944
|
+
updateTextureBinding(binding, normalizedTextureDefinitions[binding.key]?.source ?? null, "always");
|
|
945
|
+
}
|
|
622
946
|
let bindGroup = createBindGroup();
|
|
623
947
|
let sourceSlotTarget = null;
|
|
624
948
|
let targetSlotTarget = null;
|
|
@@ -667,10 +991,11 @@ async function createRenderer(options) {
|
|
|
667
991
|
if (cachedGraphPasses.length !== passes.length) return false;
|
|
668
992
|
for (let index = 0; index < passes.length; index += 1) {
|
|
669
993
|
const pass = passes[index];
|
|
994
|
+
const rp = pass;
|
|
670
995
|
const snapshot = cachedGraphPasses[index];
|
|
671
996
|
if (!pass || !snapshot || snapshot.pass !== pass) return false;
|
|
672
|
-
if (snapshot.enabled !== pass.enabled || snapshot.needsSwap !==
|
|
673
|
-
const passClearColor =
|
|
997
|
+
if (snapshot.enabled !== pass.enabled || snapshot.needsSwap !== rp.needsSwap || snapshot.input !== rp.input || snapshot.output !== rp.output || snapshot.clear !== rp.clear || snapshot.preserve !== rp.preserve) return false;
|
|
998
|
+
const passClearColor = rp.clearColor;
|
|
674
999
|
const hasPassClearColor = passClearColor !== void 0;
|
|
675
1000
|
if (snapshot.hasClearColor !== hasPassClearColor) return false;
|
|
676
1001
|
if (passClearColor) {
|
|
@@ -692,18 +1017,19 @@ async function createRenderer(options) {
|
|
|
692
1017
|
cachedGraphPasses.length = passes.length;
|
|
693
1018
|
let index = 0;
|
|
694
1019
|
for (const pass of passes) {
|
|
695
|
-
const
|
|
1020
|
+
const rp = pass;
|
|
1021
|
+
const passClearColor = rp.clearColor;
|
|
696
1022
|
const hasPassClearColor = passClearColor !== void 0;
|
|
697
1023
|
const snapshot = cachedGraphPasses[index];
|
|
698
1024
|
if (!snapshot) {
|
|
699
1025
|
cachedGraphPasses[index] = {
|
|
700
1026
|
pass,
|
|
701
1027
|
enabled: pass.enabled,
|
|
702
|
-
needsSwap:
|
|
703
|
-
input:
|
|
704
|
-
output:
|
|
705
|
-
clear:
|
|
706
|
-
preserve:
|
|
1028
|
+
needsSwap: rp.needsSwap,
|
|
1029
|
+
input: rp.input,
|
|
1030
|
+
output: rp.output,
|
|
1031
|
+
clear: rp.clear,
|
|
1032
|
+
preserve: rp.preserve,
|
|
707
1033
|
hasClearColor: hasPassClearColor,
|
|
708
1034
|
clearColor0: passClearColor?.[0] ?? 0,
|
|
709
1035
|
clearColor1: passClearColor?.[1] ?? 0,
|
|
@@ -715,11 +1041,11 @@ async function createRenderer(options) {
|
|
|
715
1041
|
}
|
|
716
1042
|
snapshot.pass = pass;
|
|
717
1043
|
snapshot.enabled = pass.enabled;
|
|
718
|
-
snapshot.needsSwap =
|
|
719
|
-
snapshot.input =
|
|
720
|
-
snapshot.output =
|
|
721
|
-
snapshot.clear =
|
|
722
|
-
snapshot.preserve =
|
|
1044
|
+
snapshot.needsSwap = rp.needsSwap;
|
|
1045
|
+
snapshot.input = rp.input;
|
|
1046
|
+
snapshot.output = rp.output;
|
|
1047
|
+
snapshot.clear = rp.clear;
|
|
1048
|
+
snapshot.preserve = rp.preserve;
|
|
723
1049
|
snapshot.hasClearColor = hasPassClearColor;
|
|
724
1050
|
snapshot.clearColor0 = passClearColor?.[0] ?? 0;
|
|
725
1051
|
snapshot.clearColor1 = passClearColor?.[1] ?? 0;
|
|
@@ -845,7 +1171,7 @@ async function createRenderer(options) {
|
|
|
845
1171
|
/**
|
|
846
1172
|
* Executes a full frame render.
|
|
847
1173
|
*/
|
|
848
|
-
const render = ({ time, delta, renderMode, uniforms, textures, canvasSize }) => {
|
|
1174
|
+
const render = ({ time, delta, renderMode, uniforms, textures, canvasSize, pendingStorageWrites }) => {
|
|
849
1175
|
if (deviceLostMessage) throw new Error(deviceLostMessage);
|
|
850
1176
|
if (uncapturedErrorMessage) {
|
|
851
1177
|
const message = uncapturedErrorMessage;
|
|
@@ -883,8 +1209,18 @@ async function createRenderer(options) {
|
|
|
883
1209
|
if (dirtyRanges.length > 0) uniformPrevious.set(uniformScratch);
|
|
884
1210
|
}
|
|
885
1211
|
let bindGroupDirty = false;
|
|
886
|
-
for (const binding of textureBindings)
|
|
1212
|
+
for (const binding of textureBindings) {
|
|
1213
|
+
if (storageTextureKeySet.has(binding.key)) continue;
|
|
1214
|
+
if (updateTextureBinding(binding, textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null, renderMode)) bindGroupDirty = true;
|
|
1215
|
+
}
|
|
887
1216
|
if (bindGroupDirty) bindGroup = createBindGroup();
|
|
1217
|
+
if (pendingStorageWrites) for (const write of pendingStorageWrites) {
|
|
1218
|
+
const buffer = storageBufferMap.get(write.name);
|
|
1219
|
+
if (buffer) {
|
|
1220
|
+
const data = write.data;
|
|
1221
|
+
device.queue.writeBuffer(buffer, write.offset, data.buffer, data.byteOffset, data.byteLength);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
888
1224
|
const commandEncoder = device.createCommandEncoder();
|
|
889
1225
|
const passes = resolvePasses();
|
|
890
1226
|
const clearColor = options.getClearColor();
|
|
@@ -909,6 +1245,49 @@ async function createRenderer(options) {
|
|
|
909
1245
|
canvas: canvasSurface
|
|
910
1246
|
} : null;
|
|
911
1247
|
const sceneOutput = slots ? slots.source : canvasSurface;
|
|
1248
|
+
if (slots) for (const step of graphPlan.steps) {
|
|
1249
|
+
if (step.kind !== "compute") continue;
|
|
1250
|
+
const computePass = step.pass;
|
|
1251
|
+
if (computePass.getCompute && computePass.resolveDispatch && computePass.getWorkgroupSize) {
|
|
1252
|
+
const computeSource = computePass.getCompute();
|
|
1253
|
+
const pingPongTarget = computePass.isPingPong && computePass.getTarget ? computePass.getTarget() : void 0;
|
|
1254
|
+
if (computePass.isPingPong && !pingPongTarget) throw new Error("PingPongComputePass must provide a target texture key.");
|
|
1255
|
+
const pingPongPair = pingPongTarget ? ensurePingPongTexturePair(pingPongTarget) : null;
|
|
1256
|
+
const pipelineEntry = buildComputePipelineEntry({
|
|
1257
|
+
computeSource,
|
|
1258
|
+
...pingPongPair ? {
|
|
1259
|
+
pingPongTarget: pingPongPair.target,
|
|
1260
|
+
pingPongFormat: pingPongPair.format
|
|
1261
|
+
} : {}
|
|
1262
|
+
});
|
|
1263
|
+
const workgroupSize = computePass.getWorkgroupSize();
|
|
1264
|
+
const storageBindGroup = getComputeStorageBindGroup();
|
|
1265
|
+
const storageTextureBindGroup = getComputeStorageTextureBindGroup();
|
|
1266
|
+
const iterations = computePass.isPingPong && computePass.getIterations ? computePass.getIterations() : 1;
|
|
1267
|
+
const currentOutput = computePass.isPingPong && computePass.getCurrentOutput ? computePass.getCurrentOutput() : null;
|
|
1268
|
+
const readFromAAtIterationZero = pingPongPair && currentOutput ? currentOutput !== `${pingPongPair.target}B` : true;
|
|
1269
|
+
for (let iter = 0; iter < iterations; iter += 1) {
|
|
1270
|
+
const dispatch = computePass.resolveDispatch({
|
|
1271
|
+
width,
|
|
1272
|
+
height,
|
|
1273
|
+
time,
|
|
1274
|
+
delta,
|
|
1275
|
+
workgroupSize
|
|
1276
|
+
});
|
|
1277
|
+
const cPass = commandEncoder.beginComputePass();
|
|
1278
|
+
cPass.setPipeline(pipelineEntry.pipeline);
|
|
1279
|
+
cPass.setBindGroup(0, pipelineEntry.bindGroup);
|
|
1280
|
+
if (storageBindGroup) cPass.setBindGroup(1, storageBindGroup);
|
|
1281
|
+
if (pingPongPair) {
|
|
1282
|
+
const readFromA = iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
|
|
1283
|
+
cPass.setBindGroup(2, getPingPongStorageTextureBindGroup(pingPongPair.target, readFromA));
|
|
1284
|
+
} else if (storageTextureBindGroup) cPass.setBindGroup(2, storageTextureBindGroup);
|
|
1285
|
+
cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
|
|
1286
|
+
cPass.end();
|
|
1287
|
+
}
|
|
1288
|
+
if (computePass.isPingPong && computePass.advanceFrame) computePass.advanceFrame();
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
912
1291
|
const scenePass = commandEncoder.beginRenderPass({ colorAttachments: [{
|
|
913
1292
|
view: sceneOutput.view,
|
|
914
1293
|
clearValue: {
|
|
@@ -922,6 +1301,7 @@ async function createRenderer(options) {
|
|
|
922
1301
|
}] });
|
|
923
1302
|
scenePass.setPipeline(pipeline);
|
|
924
1303
|
scenePass.setBindGroup(0, bindGroup);
|
|
1304
|
+
if (fragmentStorageBindGroup) scenePass.setBindGroup(1, fragmentStorageBindGroup);
|
|
925
1305
|
scenePass.draw(3);
|
|
926
1306
|
scenePass.end();
|
|
927
1307
|
if (slots) {
|
|
@@ -934,6 +1314,7 @@ async function createRenderer(options) {
|
|
|
934
1314
|
return named;
|
|
935
1315
|
};
|
|
936
1316
|
for (const step of graphPlan.steps) {
|
|
1317
|
+
if (step.kind === "compute") continue;
|
|
937
1318
|
const input = resolveStepSurface(step.input);
|
|
938
1319
|
const output = resolveStepSurface(step.output);
|
|
939
1320
|
step.pass.render({
|
|
@@ -983,11 +1364,25 @@ async function createRenderer(options) {
|
|
|
983
1364
|
initializationCleanups.length = 0;
|
|
984
1365
|
return {
|
|
985
1366
|
render,
|
|
1367
|
+
getStorageBuffer: (name) => {
|
|
1368
|
+
return storageBufferMap.get(name);
|
|
1369
|
+
},
|
|
1370
|
+
getDevice: () => {
|
|
1371
|
+
return device;
|
|
1372
|
+
},
|
|
986
1373
|
destroy: () => {
|
|
987
1374
|
isDestroyed = true;
|
|
988
1375
|
device.removeEventListener("uncapturederror", handleUncapturedError);
|
|
989
1376
|
frameBuffer.destroy();
|
|
990
1377
|
uniformBuffer.destroy();
|
|
1378
|
+
for (const buffer of storageBufferMap.values()) buffer.destroy();
|
|
1379
|
+
storageBufferMap.clear();
|
|
1380
|
+
for (const pair of pingPongTexturePairs.values()) {
|
|
1381
|
+
pair.textureA.destroy();
|
|
1382
|
+
pair.textureB.destroy();
|
|
1383
|
+
}
|
|
1384
|
+
pingPongTexturePairs.clear();
|
|
1385
|
+
computePipelineCache.clear();
|
|
991
1386
|
destroyRenderTexture(sourceSlotTarget);
|
|
992
1387
|
destroyRenderTexture(targetSlotTarget);
|
|
993
1388
|
for (const target of runtimeRenderTargets.values()) target.texture.destroy();
|