@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.
Files changed (111) hide show
  1. package/README.md +99 -0
  2. package/dist/advanced.js +3 -1
  3. package/dist/core/advanced.js +3 -1
  4. package/dist/core/compute-shader.d.ts +87 -0
  5. package/dist/core/compute-shader.d.ts.map +1 -0
  6. package/dist/core/compute-shader.js +205 -0
  7. package/dist/core/compute-shader.js.map +1 -0
  8. package/dist/core/error-report.d.ts +1 -1
  9. package/dist/core/error-report.d.ts.map +1 -1
  10. package/dist/core/error-report.js +63 -0
  11. package/dist/core/error-report.js.map +1 -1
  12. package/dist/core/frame-registry.d.ts.map +1 -1
  13. package/dist/core/frame-registry.js +1 -1
  14. package/dist/core/frame-registry.js.map +1 -1
  15. package/dist/core/index.d.ts +5 -2
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +3 -1
  18. package/dist/core/material-preprocess.d.ts.map +1 -1
  19. package/dist/core/material-preprocess.js +5 -3
  20. package/dist/core/material-preprocess.js.map +1 -1
  21. package/dist/core/material.d.ts +22 -6
  22. package/dist/core/material.d.ts.map +1 -1
  23. package/dist/core/material.js +30 -3
  24. package/dist/core/material.js.map +1 -1
  25. package/dist/core/render-graph.d.ts +7 -3
  26. package/dist/core/render-graph.d.ts.map +1 -1
  27. package/dist/core/render-graph.js +22 -6
  28. package/dist/core/render-graph.js.map +1 -1
  29. package/dist/core/renderer.d.ts.map +1 -1
  30. package/dist/core/renderer.js +418 -23
  31. package/dist/core/renderer.js.map +1 -1
  32. package/dist/core/runtime-loop.d.ts +2 -2
  33. package/dist/core/runtime-loop.d.ts.map +1 -1
  34. package/dist/core/runtime-loop.js +49 -1
  35. package/dist/core/runtime-loop.js.map +1 -1
  36. package/dist/core/shader.d.ts +9 -1
  37. package/dist/core/shader.d.ts.map +1 -1
  38. package/dist/core/shader.js +21 -2
  39. package/dist/core/shader.js.map +1 -1
  40. package/dist/core/storage-buffers.d.ts +37 -0
  41. package/dist/core/storage-buffers.d.ts.map +1 -0
  42. package/dist/core/storage-buffers.js +95 -0
  43. package/dist/core/storage-buffers.js.map +1 -0
  44. package/dist/core/texture-loader.d.ts.map +1 -1
  45. package/dist/core/texture-loader.js +4 -0
  46. package/dist/core/texture-loader.js.map +1 -1
  47. package/dist/core/textures.d.ts +12 -0
  48. package/dist/core/textures.d.ts.map +1 -1
  49. package/dist/core/textures.js +7 -2
  50. package/dist/core/textures.js.map +1 -1
  51. package/dist/core/types.d.ts +146 -4
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/index.js +3 -1
  54. package/dist/passes/ComputePass.d.ts +83 -0
  55. package/dist/passes/ComputePass.d.ts.map +1 -0
  56. package/dist/passes/ComputePass.js +92 -0
  57. package/dist/passes/ComputePass.js.map +1 -0
  58. package/dist/passes/PingPongComputePass.d.ts +104 -0
  59. package/dist/passes/PingPongComputePass.d.ts.map +1 -0
  60. package/dist/passes/PingPongComputePass.js +132 -0
  61. package/dist/passes/PingPongComputePass.js.map +1 -0
  62. package/dist/passes/ShaderPass.d.ts.map +1 -1
  63. package/dist/passes/ShaderPass.js +2 -1
  64. package/dist/passes/ShaderPass.js.map +1 -1
  65. package/dist/passes/index.d.ts +2 -0
  66. package/dist/passes/index.d.ts.map +1 -1
  67. package/dist/passes/index.js +3 -1
  68. package/dist/react/FragCanvas.d.ts +2 -2
  69. package/dist/react/FragCanvas.d.ts.map +1 -1
  70. package/dist/react/FragCanvas.js.map +1 -1
  71. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
  72. package/dist/react/MotionGPUErrorOverlay.js +123 -20
  73. package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
  74. package/dist/react/advanced.js +3 -1
  75. package/dist/react/index.d.ts +5 -2
  76. package/dist/react/index.d.ts.map +1 -1
  77. package/dist/react/index.js +3 -1
  78. package/dist/svelte/FragCanvas.svelte +2 -2
  79. package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
  80. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
  81. package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
  82. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
  83. package/dist/svelte/advanced.js +3 -1
  84. package/dist/svelte/index.d.ts +5 -2
  85. package/dist/svelte/index.d.ts.map +1 -1
  86. package/dist/svelte/index.js +3 -1
  87. package/package.json +1 -1
  88. package/src/lib/core/compute-shader.ts +326 -0
  89. package/src/lib/core/error-report.ts +129 -0
  90. package/src/lib/core/frame-registry.ts +2 -1
  91. package/src/lib/core/index.ts +18 -1
  92. package/src/lib/core/material-preprocess.ts +17 -6
  93. package/src/lib/core/material.ts +101 -20
  94. package/src/lib/core/render-graph.ts +39 -9
  95. package/src/lib/core/renderer.ts +655 -41
  96. package/src/lib/core/runtime-loop.ts +82 -3
  97. package/src/lib/core/shader.ts +45 -2
  98. package/src/lib/core/storage-buffers.ts +142 -0
  99. package/src/lib/core/texture-loader.ts +6 -0
  100. package/src/lib/core/textures.ts +24 -2
  101. package/src/lib/core/types.ts +165 -4
  102. package/src/lib/passes/ComputePass.ts +136 -0
  103. package/src/lib/passes/PingPongComputePass.ts +180 -0
  104. package/src/lib/passes/ShaderPass.ts +2 -1
  105. package/src/lib/passes/index.ts +6 -0
  106. package/src/lib/react/FragCanvas.tsx +3 -3
  107. package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
  108. package/src/lib/react/index.ts +18 -1
  109. package/src/lib/svelte/FragCanvas.svelte +2 -2
  110. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
  111. package/src/lib/svelte/index.ts +18 -1
@@ -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
- const needsSwap = pass.needsSwap ?? true;
89
- const input = pass.input ?? "source";
90
- const output = pass.output ?? (needsSwap ? "target" : "source");
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 pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
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: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
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) updateTextureBinding(binding, normalizedTextureDefinitions[binding.key]?.source ?? null, "always");
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 !== pass.needsSwap || snapshot.input !== pass.input || snapshot.output !== pass.output || snapshot.clear !== pass.clear || snapshot.preserve !== pass.preserve) return false;
673
- const passClearColor = pass.clearColor;
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 passClearColor = pass.clearColor;
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: pass.needsSwap,
703
- input: pass.input,
704
- output: pass.output,
705
- clear: pass.clear,
706
- preserve: pass.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 = pass.needsSwap;
719
- snapshot.input = pass.input;
720
- snapshot.output = pass.output;
721
- snapshot.clear = pass.clear;
722
- snapshot.preserve = pass.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) if (updateTextureBinding(binding, textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null, renderMode)) bindGroupDirty = true;
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();