@motion-core/motion-gpu 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) 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-bindgroup-cache.d.ts +13 -0
  5. package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
  6. package/dist/core/compute-bindgroup-cache.js +45 -0
  7. package/dist/core/compute-bindgroup-cache.js.map +1 -0
  8. package/dist/core/compute-shader.d.ts +135 -0
  9. package/dist/core/compute-shader.d.ts.map +1 -0
  10. package/dist/core/compute-shader.js +238 -0
  11. package/dist/core/compute-shader.js.map +1 -0
  12. package/dist/core/error-diagnostics.d.ts +8 -1
  13. package/dist/core/error-diagnostics.d.ts.map +1 -1
  14. package/dist/core/error-diagnostics.js +7 -3
  15. package/dist/core/error-diagnostics.js.map +1 -1
  16. package/dist/core/error-report.d.ts +1 -1
  17. package/dist/core/error-report.d.ts.map +1 -1
  18. package/dist/core/error-report.js +82 -1
  19. package/dist/core/error-report.js.map +1 -1
  20. package/dist/core/frame-registry.d.ts.map +1 -1
  21. package/dist/core/frame-registry.js +1 -1
  22. package/dist/core/frame-registry.js.map +1 -1
  23. package/dist/core/index.d.ts +5 -2
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +3 -1
  26. package/dist/core/material-preprocess.d.ts.map +1 -1
  27. package/dist/core/material-preprocess.js +5 -3
  28. package/dist/core/material-preprocess.js.map +1 -1
  29. package/dist/core/material.d.ts +22 -6
  30. package/dist/core/material.d.ts.map +1 -1
  31. package/dist/core/material.js +32 -4
  32. package/dist/core/material.js.map +1 -1
  33. package/dist/core/render-graph.d.ts +7 -3
  34. package/dist/core/render-graph.d.ts.map +1 -1
  35. package/dist/core/render-graph.js +22 -6
  36. package/dist/core/render-graph.js.map +1 -1
  37. package/dist/core/renderer.d.ts.map +1 -1
  38. package/dist/core/renderer.js +489 -29
  39. package/dist/core/renderer.js.map +1 -1
  40. package/dist/core/runtime-loop.d.ts +2 -2
  41. package/dist/core/runtime-loop.d.ts.map +1 -1
  42. package/dist/core/runtime-loop.js +74 -14
  43. package/dist/core/runtime-loop.js.map +1 -1
  44. package/dist/core/shader.d.ts +16 -3
  45. package/dist/core/shader.d.ts.map +1 -1
  46. package/dist/core/shader.js +22 -2
  47. package/dist/core/shader.js.map +1 -1
  48. package/dist/core/storage-buffers.d.ts +37 -0
  49. package/dist/core/storage-buffers.d.ts.map +1 -0
  50. package/dist/core/storage-buffers.js +95 -0
  51. package/dist/core/storage-buffers.js.map +1 -0
  52. package/dist/core/texture-loader.d.ts.map +1 -1
  53. package/dist/core/texture-loader.js +4 -0
  54. package/dist/core/texture-loader.js.map +1 -1
  55. package/dist/core/textures.d.ts +16 -0
  56. package/dist/core/textures.d.ts.map +1 -1
  57. package/dist/core/textures.js +8 -2
  58. package/dist/core/textures.js.map +1 -1
  59. package/dist/core/types.d.ts +146 -4
  60. package/dist/core/types.d.ts.map +1 -1
  61. package/dist/index.js +3 -1
  62. package/dist/passes/ComputePass.d.ts +83 -0
  63. package/dist/passes/ComputePass.d.ts.map +1 -0
  64. package/dist/passes/ComputePass.js +92 -0
  65. package/dist/passes/ComputePass.js.map +1 -0
  66. package/dist/passes/PingPongComputePass.d.ts +104 -0
  67. package/dist/passes/PingPongComputePass.d.ts.map +1 -0
  68. package/dist/passes/PingPongComputePass.js +132 -0
  69. package/dist/passes/PingPongComputePass.js.map +1 -0
  70. package/dist/passes/ShaderPass.d.ts.map +1 -1
  71. package/dist/passes/ShaderPass.js +2 -1
  72. package/dist/passes/ShaderPass.js.map +1 -1
  73. package/dist/passes/index.d.ts +2 -0
  74. package/dist/passes/index.d.ts.map +1 -1
  75. package/dist/passes/index.js +3 -1
  76. package/dist/react/FragCanvas.d.ts +2 -2
  77. package/dist/react/FragCanvas.d.ts.map +1 -1
  78. package/dist/react/FragCanvas.js.map +1 -1
  79. package/dist/react/MotionGPUErrorOverlay.d.ts.map +1 -1
  80. package/dist/react/MotionGPUErrorOverlay.js +123 -20
  81. package/dist/react/MotionGPUErrorOverlay.js.map +1 -1
  82. package/dist/react/advanced.js +3 -1
  83. package/dist/react/index.d.ts +5 -2
  84. package/dist/react/index.d.ts.map +1 -1
  85. package/dist/react/index.js +3 -1
  86. package/dist/svelte/FragCanvas.svelte +2 -2
  87. package/dist/svelte/FragCanvas.svelte.d.ts +2 -2
  88. package/dist/svelte/FragCanvas.svelte.d.ts.map +1 -1
  89. package/dist/svelte/MotionGPUErrorOverlay.svelte +137 -7
  90. package/dist/svelte/MotionGPUErrorOverlay.svelte.d.ts.map +1 -1
  91. package/dist/svelte/advanced.js +3 -1
  92. package/dist/svelte/index.d.ts +5 -2
  93. package/dist/svelte/index.d.ts.map +1 -1
  94. package/dist/svelte/index.js +3 -1
  95. package/package.json +1 -1
  96. package/src/lib/core/compute-bindgroup-cache.ts +73 -0
  97. package/src/lib/core/compute-shader.ts +412 -0
  98. package/src/lib/core/error-diagnostics.ts +29 -4
  99. package/src/lib/core/error-report.ts +155 -1
  100. package/src/lib/core/frame-registry.ts +2 -1
  101. package/src/lib/core/index.ts +18 -1
  102. package/src/lib/core/material-preprocess.ts +17 -6
  103. package/src/lib/core/material.ts +103 -21
  104. package/src/lib/core/render-graph.ts +39 -9
  105. package/src/lib/core/renderer.ts +768 -48
  106. package/src/lib/core/runtime-loop.ts +116 -16
  107. package/src/lib/core/shader.ts +58 -4
  108. package/src/lib/core/storage-buffers.ts +142 -0
  109. package/src/lib/core/texture-loader.ts +6 -0
  110. package/src/lib/core/textures.ts +29 -2
  111. package/src/lib/core/types.ts +165 -4
  112. package/src/lib/passes/ComputePass.ts +136 -0
  113. package/src/lib/passes/PingPongComputePass.ts +180 -0
  114. package/src/lib/passes/ShaderPass.ts +2 -1
  115. package/src/lib/passes/index.ts +6 -0
  116. package/src/lib/react/FragCanvas.tsx +3 -3
  117. package/src/lib/react/MotionGPUErrorOverlay.tsx +137 -5
  118. package/src/lib/react/index.ts +18 -1
  119. package/src/lib/svelte/FragCanvas.svelte +2 -2
  120. package/src/lib/svelte/MotionGPUErrorOverlay.svelte +137 -7
  121. package/src/lib/svelte/index.ts +18 -1
@@ -1,9 +1,12 @@
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 { buildComputeShaderSourceWithMap, buildPingPongComputeShaderSourceWithMap, extractWorkgroupSize, storageTextureSampleScalarType } from "./compute-shader.js";
9
+ import { createComputeStorageBindGroupCache } from "./compute-bindgroup-cache.js";
7
10
  //#region src/lib/core/renderer.ts
8
11
  /**
9
12
  * Binding index for frame uniforms (`time`, `delta`, `resolution`).
@@ -28,6 +31,14 @@ function getTextureBindings(index) {
28
31
  };
29
32
  }
30
33
  /**
34
+ * Maps WGSL scalar texture type to WebGPU sampled texture bind-group sample type.
35
+ */
36
+ function toGpuTextureSampleType(type) {
37
+ if (type === "u32") return "uint";
38
+ if (type === "i32") return "sint";
39
+ return "float";
40
+ }
41
+ /**
31
42
  * Resizes canvas backing store to match client size and DPR.
32
43
  */
33
44
  function resizeCanvas(canvas, dprInput, cssSize) {
@@ -64,10 +75,13 @@ async function assertCompilation(module, options) {
64
75
  if (contextLabel.length === 0) return diagnostic.message;
65
76
  return `[${contextLabel.join(" | ")}] ${diagnostic.message}`;
66
77
  }).join("\n");
67
- throw attachShaderCompilationDiagnostics(/* @__PURE__ */ new Error(`WGSL compilation failed:\n${summary}`), {
78
+ const prefix = options?.errorPrefix ?? "WGSL compilation failed";
79
+ throw attachShaderCompilationDiagnostics(/* @__PURE__ */ new Error(`${prefix}:\n${summary}`), {
68
80
  kind: "shader-compilation",
81
+ ...options?.shaderStage !== void 0 ? { shaderStage: options.shaderStage } : {},
69
82
  diagnostics,
70
83
  fragmentSource: options?.fragmentSource ?? "",
84
+ ...options?.computeSource !== void 0 ? { computeSource: options.computeSource } : {},
71
85
  includeSources: options?.includeSources ?? {},
72
86
  ...options?.defineBlockSource !== void 0 ? { defineBlockSource: options.defineBlockSource } : {},
73
87
  materialSource: options?.materialSource ?? null,
@@ -77,6 +91,41 @@ async function assertCompilation(module, options) {
77
91
  function toSortedUniqueStrings(values) {
78
92
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
79
93
  }
94
+ function extractGeneratedLineFromComputeError(message) {
95
+ const lineMatch = message.match(/\bline\s+(\d+)\b/i);
96
+ if (lineMatch) {
97
+ const parsed = Number.parseInt(lineMatch[1] ?? "", 10);
98
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
99
+ }
100
+ const colonMatch = message.match(/:(\d+):\d+/);
101
+ if (colonMatch) {
102
+ const parsed = Number.parseInt(colonMatch[1] ?? "", 10);
103
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
104
+ }
105
+ return null;
106
+ }
107
+ function toComputeCompilationError(input) {
108
+ const baseError = input.error instanceof Error ? input.error : new Error(String(input.error ?? "Unknown error"));
109
+ const generatedLine = extractGeneratedLineFromComputeError(baseError.message) ?? 0;
110
+ const sourceLocation = generatedLine > 0 ? input.lineMap[generatedLine] ?? null : null;
111
+ const diagnostics = [{
112
+ generatedLine,
113
+ message: baseError.message,
114
+ sourceLocation
115
+ }];
116
+ const contextLabel = [formatShaderSourceLocation(sourceLocation), generatedLine > 0 ? `generated WGSL line ${generatedLine}` : null].filter((value) => Boolean(value));
117
+ const summary = contextLabel.length > 0 ? `[${contextLabel.join(" | ")}] ${baseError.message}` : baseError.message;
118
+ return attachShaderCompilationDiagnostics(/* @__PURE__ */ new Error(`Compute shader compilation failed:\n${summary}`), {
119
+ kind: "shader-compilation",
120
+ shaderStage: "compute",
121
+ diagnostics,
122
+ fragmentSource: "",
123
+ computeSource: input.computeSource,
124
+ includeSources: {},
125
+ materialSource: null,
126
+ runtimeContext: input.runtimeContext
127
+ });
128
+ }
80
129
  function buildPassGraphSnapshot(passes) {
81
130
  const declaredPasses = passes ?? [];
82
131
  let enabledPassCount = 0;
@@ -85,9 +134,11 @@ function buildPassGraphSnapshot(passes) {
85
134
  for (const pass of declaredPasses) {
86
135
  if (pass.enabled === false) continue;
87
136
  enabledPassCount += 1;
88
- const needsSwap = pass.needsSwap ?? true;
89
- const input = pass.input ?? "source";
90
- const output = pass.output ?? (needsSwap ? "target" : "source");
137
+ if ("isCompute" in pass && pass.isCompute === true) continue;
138
+ const rp = pass;
139
+ const needsSwap = rp.needsSwap ?? true;
140
+ const input = rp.input ?? "source";
141
+ const output = rp.output ?? (needsSwap ? "target" : "source");
91
142
  inputs.push(input);
92
143
  outputs.push(output);
93
144
  }
@@ -385,9 +436,12 @@ async function createRenderer(options) {
385
436
  try {
386
437
  const runtimeContext = buildShaderCompilationRuntimeContext(options);
387
438
  const convertLinearToSrgb = shouldConvertLinearToSrgb(options.outputColorSpace, format);
388
- const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, options.textureKeys, {
439
+ const fragmentTextureKeys = options.textureKeys.filter((key) => options.textureDefinitions[key]?.fragmentVisible !== false);
440
+ const builtShader = buildShaderSourceWithMap(options.fragmentWgsl, options.uniformLayout, fragmentTextureKeys, {
389
441
  convertLinearToSrgb,
390
- fragmentLineMap: options.fragmentLineMap
442
+ fragmentLineMap: options.fragmentLineMap,
443
+ ...options.storageBufferKeys !== void 0 ? { storageBufferKeys: options.storageBufferKeys } : {},
444
+ ...options.storageBufferDefinitions !== void 0 ? { storageBufferDefinitions: options.storageBufferDefinitions } : {}
391
445
  });
392
446
  const shaderModule = device.createShaderModule({ code: builtShader.code });
393
447
  await assertCompilation(shaderModule, {
@@ -399,10 +453,17 @@ async function createRenderer(options) {
399
453
  runtimeContext
400
454
  });
401
455
  const normalizedTextureDefinitions = normalizeTextureDefinitions(options.textureDefinitions, options.textureKeys);
402
- const textureBindings = options.textureKeys.map((key, index) => {
456
+ const storageBufferKeys = options.storageBufferKeys ?? [];
457
+ const storageBufferDefinitions = options.storageBufferDefinitions ?? {};
458
+ const storageTextureKeys = options.storageTextureKeys ?? [];
459
+ const storageTextureKeySet = new Set(storageTextureKeys);
460
+ const fragmentTextureIndexByKey = new Map(fragmentTextureKeys.map((key, index) => [key, index]));
461
+ const textureBindings = options.textureKeys.map((key) => {
403
462
  const config = normalizedTextureDefinitions[key];
404
463
  if (!config) throw new Error(`Missing texture definition for "${key}"`);
405
- const { samplerBinding, textureBinding } = getTextureBindings(index);
464
+ const fragmentTextureIndex = fragmentTextureIndexByKey.get(key);
465
+ const fragmentVisible = fragmentTextureIndex !== void 0;
466
+ const { samplerBinding, textureBinding } = getTextureBindings(fragmentTextureIndex ?? 0);
406
467
  const sampler = device.createSampler({
407
468
  magFilter: config.filter,
408
469
  minFilter: config.filter,
@@ -411,7 +472,7 @@ async function createRenderer(options) {
411
472
  addressModeV: config.addressModeV,
412
473
  maxAnisotropy: config.filter === "linear" ? config.anisotropy : 1
413
474
  });
414
- const fallbackTexture = createFallbackTexture(device, config.format);
475
+ const fallbackTexture = createFallbackTexture(device, config.storage ? "rgba8unorm" : config.format);
415
476
  registerInitializationCleanup(() => {
416
477
  fallbackTexture.destroy();
417
478
  });
@@ -420,6 +481,7 @@ async function createRenderer(options) {
420
481
  key,
421
482
  samplerBinding,
422
483
  textureBinding,
484
+ fragmentVisible,
423
485
  sampler,
424
486
  fallbackTexture,
425
487
  fallbackView,
@@ -442,10 +504,60 @@ async function createRenderer(options) {
442
504
  lastToken: null
443
505
  };
444
506
  if (config.update !== void 0) runtimeBinding.defaultUpdate = config.update;
507
+ if (config.storage && config.width && config.height) {
508
+ const storageUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
509
+ const storageTexture = device.createTexture({
510
+ size: {
511
+ width: config.width,
512
+ height: config.height,
513
+ depthOrArrayLayers: 1
514
+ },
515
+ format: config.format,
516
+ usage: storageUsage
517
+ });
518
+ registerInitializationCleanup(() => {
519
+ storageTexture.destroy();
520
+ });
521
+ runtimeBinding.texture = storageTexture;
522
+ runtimeBinding.view = storageTexture.createView();
523
+ runtimeBinding.width = config.width;
524
+ runtimeBinding.height = config.height;
525
+ }
445
526
  return runtimeBinding;
446
527
  });
447
- const bindGroupLayout = device.createBindGroupLayout({ entries: createBindGroupLayoutEntries(textureBindings) });
448
- const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
528
+ const textureBindingByKey = new Map(textureBindings.map((binding) => [binding.key, binding]));
529
+ const fragmentTextureBindings = textureBindings.filter((binding) => binding.fragmentVisible);
530
+ const computeStorageBufferLayoutEntries = storageBufferKeys.map((key, index) => {
531
+ const bufferType = (storageBufferDefinitions[key]?.access ?? "read-write") === "read" ? "read-only-storage" : "storage";
532
+ return {
533
+ binding: index,
534
+ visibility: GPUShaderStage.COMPUTE,
535
+ buffer: { type: bufferType }
536
+ };
537
+ });
538
+ const computeStorageBufferTopologyKey = storageBufferKeys.map((key) => `${key}:${storageBufferDefinitions[key]?.access ?? "read-write"}`).join("|");
539
+ const computeStorageTextureLayoutEntries = storageTextureKeys.map((key, index) => {
540
+ const config = normalizedTextureDefinitions[key];
541
+ return {
542
+ binding: index,
543
+ visibility: GPUShaderStage.COMPUTE,
544
+ storageTexture: {
545
+ access: "write-only",
546
+ format: config?.format ?? "rgba8unorm",
547
+ viewDimension: "2d"
548
+ }
549
+ };
550
+ });
551
+ const computeStorageTextureTopologyKey = storageTextureKeys.map((key) => `${key}:${normalizedTextureDefinitions[key]?.format ?? "rgba8unorm"}`).join("|");
552
+ const computeStorageBufferBindGroupCache = createComputeStorageBindGroupCache(device);
553
+ const computeStorageTextureBindGroupCache = createComputeStorageBindGroupCache(device);
554
+ const bindGroupLayout = device.createBindGroupLayout({ entries: createBindGroupLayoutEntries(fragmentTextureBindings) });
555
+ const fragmentStorageBindGroupLayout = storageBufferKeys.length > 0 ? device.createBindGroupLayout({ entries: storageBufferKeys.map((_, index) => ({
556
+ binding: index,
557
+ visibility: GPUShaderStage.FRAGMENT,
558
+ buffer: { type: "read-only-storage" }
559
+ })) }) : null;
560
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: fragmentStorageBindGroupLayout ? [bindGroupLayout, fragmentStorageBindGroupLayout] : [bindGroupLayout] });
449
561
  const pipeline = device.createRenderPipeline({
450
562
  layout: pipelineLayout,
451
563
  vertex: {
@@ -495,6 +607,278 @@ async function createRenderer(options) {
495
607
  addressModeV: "clamp-to-edge"
496
608
  });
497
609
  let blitBindGroupByView = /* @__PURE__ */ new WeakMap();
610
+ const storageBufferMap = /* @__PURE__ */ new Map();
611
+ const pingPongTexturePairs = /* @__PURE__ */ new Map();
612
+ for (const key of storageBufferKeys) {
613
+ const definition = storageBufferDefinitions[key];
614
+ if (!definition) continue;
615
+ const normalized = normalizeStorageBufferDefinition(definition);
616
+ const buffer = device.createBuffer({
617
+ size: normalized.size,
618
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
619
+ });
620
+ registerInitializationCleanup(() => {
621
+ buffer.destroy();
622
+ });
623
+ if (definition.initialData) {
624
+ const data = definition.initialData;
625
+ device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
626
+ }
627
+ storageBufferMap.set(key, buffer);
628
+ }
629
+ const fragmentStorageBindGroup = fragmentStorageBindGroupLayout && storageBufferKeys.length > 0 ? device.createBindGroup({
630
+ layout: fragmentStorageBindGroupLayout,
631
+ entries: storageBufferKeys.map((key, index) => {
632
+ const buffer = storageBufferMap.get(key);
633
+ if (!buffer) throw new Error(`Storage buffer "${key}" not allocated.`);
634
+ return {
635
+ binding: index,
636
+ resource: { buffer }
637
+ };
638
+ })
639
+ }) : null;
640
+ const ensurePingPongTexturePair = (target) => {
641
+ const existing = pingPongTexturePairs.get(target);
642
+ if (existing) return existing;
643
+ const config = normalizedTextureDefinitions[target];
644
+ if (!config || !config.storage) throw new Error(`PingPongComputePass target "${target}" must reference a texture declared with storage:true.`);
645
+ if (!config.width || !config.height) throw new Error(`PingPongComputePass target "${target}" requires explicit texture width and height.`);
646
+ const usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST;
647
+ const textureA = device.createTexture({
648
+ size: {
649
+ width: config.width,
650
+ height: config.height,
651
+ depthOrArrayLayers: 1
652
+ },
653
+ format: config.format,
654
+ usage
655
+ });
656
+ const textureB = device.createTexture({
657
+ size: {
658
+ width: config.width,
659
+ height: config.height,
660
+ depthOrArrayLayers: 1
661
+ },
662
+ format: config.format,
663
+ usage
664
+ });
665
+ registerInitializationCleanup(() => {
666
+ textureA.destroy();
667
+ });
668
+ registerInitializationCleanup(() => {
669
+ textureB.destroy();
670
+ });
671
+ const sampleType = toGpuTextureSampleType(storageTextureSampleScalarType(config.format));
672
+ const bindGroupLayout = device.createBindGroupLayout({ entries: [{
673
+ binding: 0,
674
+ visibility: GPUShaderStage.COMPUTE,
675
+ texture: {
676
+ sampleType,
677
+ viewDimension: "2d",
678
+ multisampled: false
679
+ }
680
+ }, {
681
+ binding: 1,
682
+ visibility: GPUShaderStage.COMPUTE,
683
+ storageTexture: {
684
+ access: "write-only",
685
+ format: config.format,
686
+ viewDimension: "2d"
687
+ }
688
+ }] });
689
+ const pair = {
690
+ target,
691
+ format: config.format,
692
+ width: config.width,
693
+ height: config.height,
694
+ textureA,
695
+ viewA: textureA.createView(),
696
+ textureB,
697
+ viewB: textureB.createView(),
698
+ bindGroupLayout,
699
+ readAWriteBBindGroup: null,
700
+ readBWriteABindGroup: null
701
+ };
702
+ pingPongTexturePairs.set(target, pair);
703
+ return pair;
704
+ };
705
+ const computePipelineCache = /* @__PURE__ */ new Map();
706
+ const buildComputePipelineEntry = (buildOptions) => {
707
+ const cacheKey = buildOptions.pingPongTarget && buildOptions.pingPongFormat ? `pingpong:${buildOptions.pingPongTarget}:${buildOptions.pingPongFormat}:${buildOptions.computeSource}` : `compute:${buildOptions.computeSource}`;
708
+ const cached = computePipelineCache.get(cacheKey);
709
+ if (cached) return cached;
710
+ const storageBufferDefs = {};
711
+ for (const key of storageBufferKeys) {
712
+ const def = storageBufferDefinitions[key];
713
+ if (def) {
714
+ const norm = normalizeStorageBufferDefinition(def);
715
+ storageBufferDefs[key] = {
716
+ type: norm.type,
717
+ access: norm.access
718
+ };
719
+ }
720
+ }
721
+ const storageTextureDefs = {};
722
+ for (const key of storageTextureKeys) {
723
+ const texDef = options.textureDefinitions[key];
724
+ if (texDef?.format) storageTextureDefs[key] = { format: texDef.format };
725
+ }
726
+ const isPingPongPipeline = Boolean(buildOptions.pingPongTarget && buildOptions.pingPongFormat);
727
+ const builtComputeShader = isPingPongPipeline ? buildPingPongComputeShaderSourceWithMap({
728
+ compute: buildOptions.computeSource,
729
+ uniformLayout: options.uniformLayout,
730
+ storageBufferKeys,
731
+ storageBufferDefinitions: storageBufferDefs,
732
+ target: buildOptions.pingPongTarget,
733
+ targetFormat: buildOptions.pingPongFormat
734
+ }) : buildComputeShaderSourceWithMap({
735
+ compute: buildOptions.computeSource,
736
+ uniformLayout: options.uniformLayout,
737
+ storageBufferKeys,
738
+ storageBufferDefinitions: storageBufferDefs,
739
+ storageTextureKeys,
740
+ storageTextureDefinitions: storageTextureDefs
741
+ });
742
+ const computeShaderModule = device.createShaderModule({ code: builtComputeShader.code });
743
+ const workgroupSize = extractWorkgroupSize(buildOptions.computeSource);
744
+ const computeUniformBGL = device.createBindGroupLayout({ entries: [{
745
+ binding: FRAME_BINDING,
746
+ visibility: GPUShaderStage.COMPUTE,
747
+ buffer: {
748
+ type: "uniform",
749
+ minBindingSize: 16
750
+ }
751
+ }, {
752
+ binding: UNIFORM_BINDING,
753
+ visibility: GPUShaderStage.COMPUTE,
754
+ buffer: { type: "uniform" }
755
+ }] });
756
+ const storageBGL = computeStorageBufferLayoutEntries.length > 0 ? device.createBindGroupLayout({ entries: computeStorageBufferLayoutEntries }) : null;
757
+ const storageTextureBGLEntries = isPingPongPipeline ? [{
758
+ binding: 0,
759
+ visibility: GPUShaderStage.COMPUTE,
760
+ texture: {
761
+ sampleType: toGpuTextureSampleType(storageTextureSampleScalarType(buildOptions.pingPongFormat)),
762
+ viewDimension: "2d",
763
+ multisampled: false
764
+ }
765
+ }, {
766
+ binding: 1,
767
+ visibility: GPUShaderStage.COMPUTE,
768
+ storageTexture: {
769
+ access: "write-only",
770
+ format: buildOptions.pingPongFormat,
771
+ viewDimension: "2d"
772
+ }
773
+ }] : computeStorageTextureLayoutEntries;
774
+ const storageTextureBGL = storageTextureBGLEntries.length > 0 ? device.createBindGroupLayout({ entries: storageTextureBGLEntries }) : null;
775
+ const bindGroupLayouts = [computeUniformBGL];
776
+ if (storageBGL || storageTextureBGL) bindGroupLayouts.push(storageBGL ?? device.createBindGroupLayout({ entries: [] }));
777
+ if (storageTextureBGL) bindGroupLayouts.push(storageTextureBGL);
778
+ const computePipelineLayout = device.createPipelineLayout({ bindGroupLayouts });
779
+ let pipeline;
780
+ try {
781
+ pipeline = device.createComputePipeline({
782
+ layout: computePipelineLayout,
783
+ compute: {
784
+ module: computeShaderModule,
785
+ entryPoint: "compute"
786
+ }
787
+ });
788
+ } catch (error) {
789
+ throw toComputeCompilationError({
790
+ error,
791
+ lineMap: builtComputeShader.lineMap,
792
+ computeSource: buildOptions.computeSource,
793
+ runtimeContext
794
+ });
795
+ }
796
+ const computeUniformBindGroup = device.createBindGroup({
797
+ layout: computeUniformBGL,
798
+ entries: [{
799
+ binding: FRAME_BINDING,
800
+ resource: { buffer: frameBuffer }
801
+ }, {
802
+ binding: UNIFORM_BINDING,
803
+ resource: { buffer: uniformBuffer }
804
+ }]
805
+ });
806
+ const entry = {
807
+ pipeline,
808
+ bindGroup: computeUniformBindGroup,
809
+ workgroupSize,
810
+ computeSource: buildOptions.computeSource
811
+ };
812
+ computePipelineCache.set(cacheKey, entry);
813
+ return entry;
814
+ };
815
+ const getComputeStorageBindGroup = () => {
816
+ if (computeStorageBufferLayoutEntries.length === 0) return null;
817
+ const resources = storageBufferKeys.map((key) => {
818
+ const buffer = storageBufferMap.get(key);
819
+ if (!buffer) throw new Error(`Storage buffer "${key}" not allocated.`);
820
+ return buffer;
821
+ });
822
+ const storageEntries = resources.map((buffer, index) => {
823
+ return {
824
+ binding: index,
825
+ resource: { buffer }
826
+ };
827
+ });
828
+ return computeStorageBufferBindGroupCache.getOrCreate({
829
+ topologyKey: computeStorageBufferTopologyKey,
830
+ layoutEntries: computeStorageBufferLayoutEntries,
831
+ bindGroupEntries: storageEntries,
832
+ resourceRefs: resources
833
+ });
834
+ };
835
+ const getComputeStorageTextureBindGroup = () => {
836
+ if (computeStorageTextureLayoutEntries.length === 0) return null;
837
+ const resources = storageTextureKeys.map((key) => {
838
+ const binding = textureBindingByKey.get(key);
839
+ if (!binding || !binding.texture) throw new Error(`Storage texture "${key}" not allocated.`);
840
+ return binding.view;
841
+ });
842
+ const bgEntries = resources.map((view, index) => {
843
+ return {
844
+ binding: index,
845
+ resource: view
846
+ };
847
+ });
848
+ return computeStorageTextureBindGroupCache.getOrCreate({
849
+ topologyKey: computeStorageTextureTopologyKey,
850
+ layoutEntries: computeStorageTextureLayoutEntries,
851
+ bindGroupEntries: bgEntries,
852
+ resourceRefs: resources
853
+ });
854
+ };
855
+ const getPingPongStorageTextureBindGroup = (target, readFromA) => {
856
+ const pair = ensurePingPongTexturePair(target);
857
+ if (readFromA) {
858
+ if (!pair.readAWriteBBindGroup) pair.readAWriteBBindGroup = device.createBindGroup({
859
+ layout: pair.bindGroupLayout,
860
+ entries: [{
861
+ binding: 0,
862
+ resource: pair.viewA
863
+ }, {
864
+ binding: 1,
865
+ resource: pair.viewB
866
+ }]
867
+ });
868
+ return pair.readAWriteBBindGroup;
869
+ }
870
+ if (!pair.readBWriteABindGroup) pair.readBWriteABindGroup = device.createBindGroup({
871
+ layout: pair.bindGroupLayout,
872
+ entries: [{
873
+ binding: 0,
874
+ resource: pair.viewB
875
+ }, {
876
+ binding: 1,
877
+ resource: pair.viewA
878
+ }]
879
+ });
880
+ return pair.readBWriteABindGroup;
881
+ };
498
882
  const frameBuffer = device.createBuffer({
499
883
  size: 16,
500
884
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
@@ -524,7 +908,7 @@ async function createRenderer(options) {
524
908
  binding: UNIFORM_BINDING,
525
909
  resource: { buffer: uniformBuffer }
526
910
  }];
527
- for (const binding of textureBindings) {
911
+ for (const binding of fragmentTextureBindings) {
528
912
  entries.push({
529
913
  binding: binding.samplerBinding,
530
914
  resource: binding.sampler
@@ -588,6 +972,8 @@ async function createRenderer(options) {
588
972
  binding.lastToken = value;
589
973
  return false;
590
974
  }
975
+ let textureUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
976
+ if (storageTextureKeySet.has(binding.key)) textureUsage |= GPUTextureUsage.STORAGE_BINDING;
591
977
  const texture = device.createTexture({
592
978
  size: {
593
979
  width,
@@ -596,7 +982,7 @@ async function createRenderer(options) {
596
982
  },
597
983
  format,
598
984
  mipLevelCount,
599
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
985
+ usage: textureUsage
600
986
  });
601
987
  registerInitializationCleanup(() => {
602
988
  texture.destroy();
@@ -618,7 +1004,10 @@ async function createRenderer(options) {
618
1004
  binding.lastToken = value;
619
1005
  return true;
620
1006
  };
621
- for (const binding of textureBindings) updateTextureBinding(binding, normalizedTextureDefinitions[binding.key]?.source ?? null, "always");
1007
+ for (const binding of textureBindings) {
1008
+ if (storageTextureKeySet.has(binding.key)) continue;
1009
+ updateTextureBinding(binding, normalizedTextureDefinitions[binding.key]?.source ?? null, "always");
1010
+ }
622
1011
  let bindGroup = createBindGroup();
623
1012
  let sourceSlotTarget = null;
624
1013
  let targetSlotTarget = null;
@@ -667,10 +1056,11 @@ async function createRenderer(options) {
667
1056
  if (cachedGraphPasses.length !== passes.length) return false;
668
1057
  for (let index = 0; index < passes.length; index += 1) {
669
1058
  const pass = passes[index];
1059
+ const rp = pass;
670
1060
  const snapshot = cachedGraphPasses[index];
671
1061
  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;
1062
+ 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;
1063
+ const passClearColor = rp.clearColor;
674
1064
  const hasPassClearColor = passClearColor !== void 0;
675
1065
  if (snapshot.hasClearColor !== hasPassClearColor) return false;
676
1066
  if (passClearColor) {
@@ -692,18 +1082,19 @@ async function createRenderer(options) {
692
1082
  cachedGraphPasses.length = passes.length;
693
1083
  let index = 0;
694
1084
  for (const pass of passes) {
695
- const passClearColor = pass.clearColor;
1085
+ const rp = pass;
1086
+ const passClearColor = rp.clearColor;
696
1087
  const hasPassClearColor = passClearColor !== void 0;
697
1088
  const snapshot = cachedGraphPasses[index];
698
1089
  if (!snapshot) {
699
1090
  cachedGraphPasses[index] = {
700
1091
  pass,
701
1092
  enabled: pass.enabled,
702
- needsSwap: pass.needsSwap,
703
- input: pass.input,
704
- output: pass.output,
705
- clear: pass.clear,
706
- preserve: pass.preserve,
1093
+ needsSwap: rp.needsSwap,
1094
+ input: rp.input,
1095
+ output: rp.output,
1096
+ clear: rp.clear,
1097
+ preserve: rp.preserve,
707
1098
  hasClearColor: hasPassClearColor,
708
1099
  clearColor0: passClearColor?.[0] ?? 0,
709
1100
  clearColor1: passClearColor?.[1] ?? 0,
@@ -715,11 +1106,11 @@ async function createRenderer(options) {
715
1106
  }
716
1107
  snapshot.pass = pass;
717
1108
  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;
1109
+ snapshot.needsSwap = rp.needsSwap;
1110
+ snapshot.input = rp.input;
1111
+ snapshot.output = rp.output;
1112
+ snapshot.clear = rp.clear;
1113
+ snapshot.preserve = rp.preserve;
723
1114
  snapshot.hasClearColor = hasPassClearColor;
724
1115
  snapshot.clearColor0 = passClearColor?.[0] ?? 0;
725
1116
  snapshot.clearColor1 = passClearColor?.[1] ?? 0;
@@ -845,7 +1236,7 @@ async function createRenderer(options) {
845
1236
  /**
846
1237
  * Executes a full frame render.
847
1238
  */
848
- const render = ({ time, delta, renderMode, uniforms, textures, canvasSize }) => {
1239
+ const render = ({ time, delta, renderMode, uniforms, textures, canvasSize, pendingStorageWrites }) => {
849
1240
  if (deviceLostMessage) throw new Error(deviceLostMessage);
850
1241
  if (uncapturedErrorMessage) {
851
1242
  const message = uncapturedErrorMessage;
@@ -883,8 +1274,18 @@ async function createRenderer(options) {
883
1274
  if (dirtyRanges.length > 0) uniformPrevious.set(uniformScratch);
884
1275
  }
885
1276
  let bindGroupDirty = false;
886
- for (const binding of textureBindings) if (updateTextureBinding(binding, textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null, renderMode)) bindGroupDirty = true;
1277
+ for (const binding of textureBindings) {
1278
+ if (storageTextureKeySet.has(binding.key)) continue;
1279
+ if (updateTextureBinding(binding, textures[binding.key] ?? normalizedTextureDefinitions[binding.key]?.source ?? null, renderMode) && binding.fragmentVisible) bindGroupDirty = true;
1280
+ }
887
1281
  if (bindGroupDirty) bindGroup = createBindGroup();
1282
+ if (pendingStorageWrites) for (const write of pendingStorageWrites) {
1283
+ const buffer = storageBufferMap.get(write.name);
1284
+ if (buffer) {
1285
+ const data = write.data;
1286
+ device.queue.writeBuffer(buffer, write.offset, data.buffer, data.byteOffset, data.byteLength);
1287
+ }
1288
+ }
888
1289
  const commandEncoder = device.createCommandEncoder();
889
1290
  const passes = resolvePasses();
890
1291
  const clearColor = options.getClearColor();
@@ -909,6 +1310,49 @@ async function createRenderer(options) {
909
1310
  canvas: canvasSurface
910
1311
  } : null;
911
1312
  const sceneOutput = slots ? slots.source : canvasSurface;
1313
+ if (slots) for (const step of graphPlan.steps) {
1314
+ if (step.kind !== "compute") continue;
1315
+ const computePass = step.pass;
1316
+ if (computePass.getCompute && computePass.resolveDispatch && computePass.getWorkgroupSize) {
1317
+ const computeSource = computePass.getCompute();
1318
+ const pingPongTarget = computePass.isPingPong && computePass.getTarget ? computePass.getTarget() : void 0;
1319
+ if (computePass.isPingPong && !pingPongTarget) throw new Error("PingPongComputePass must provide a target texture key.");
1320
+ const pingPongPair = pingPongTarget ? ensurePingPongTexturePair(pingPongTarget) : null;
1321
+ const pipelineEntry = buildComputePipelineEntry({
1322
+ computeSource,
1323
+ ...pingPongPair ? {
1324
+ pingPongTarget: pingPongPair.target,
1325
+ pingPongFormat: pingPongPair.format
1326
+ } : {}
1327
+ });
1328
+ const workgroupSize = computePass.getWorkgroupSize();
1329
+ const storageBindGroup = getComputeStorageBindGroup();
1330
+ const storageTextureBindGroup = getComputeStorageTextureBindGroup();
1331
+ const iterations = computePass.isPingPong && computePass.getIterations ? computePass.getIterations() : 1;
1332
+ const currentOutput = computePass.isPingPong && computePass.getCurrentOutput ? computePass.getCurrentOutput() : null;
1333
+ const readFromAAtIterationZero = pingPongPair && currentOutput ? currentOutput !== `${pingPongPair.target}B` : true;
1334
+ for (let iter = 0; iter < iterations; iter += 1) {
1335
+ const dispatch = computePass.resolveDispatch({
1336
+ width,
1337
+ height,
1338
+ time,
1339
+ delta,
1340
+ workgroupSize
1341
+ });
1342
+ const cPass = commandEncoder.beginComputePass();
1343
+ cPass.setPipeline(pipelineEntry.pipeline);
1344
+ cPass.setBindGroup(0, pipelineEntry.bindGroup);
1345
+ if (storageBindGroup) cPass.setBindGroup(1, storageBindGroup);
1346
+ if (pingPongPair) {
1347
+ const readFromA = iter % 2 === 0 ? readFromAAtIterationZero : !readFromAAtIterationZero;
1348
+ cPass.setBindGroup(2, getPingPongStorageTextureBindGroup(pingPongPair.target, readFromA));
1349
+ } else if (storageTextureBindGroup) cPass.setBindGroup(2, storageTextureBindGroup);
1350
+ cPass.dispatchWorkgroups(dispatch[0], dispatch[1], dispatch[2]);
1351
+ cPass.end();
1352
+ }
1353
+ if (computePass.isPingPong && computePass.advanceFrame) computePass.advanceFrame();
1354
+ }
1355
+ }
912
1356
  const scenePass = commandEncoder.beginRenderPass({ colorAttachments: [{
913
1357
  view: sceneOutput.view,
914
1358
  clearValue: {
@@ -922,6 +1366,7 @@ async function createRenderer(options) {
922
1366
  }] });
923
1367
  scenePass.setPipeline(pipeline);
924
1368
  scenePass.setBindGroup(0, bindGroup);
1369
+ if (fragmentStorageBindGroup) scenePass.setBindGroup(1, fragmentStorageBindGroup);
925
1370
  scenePass.draw(3);
926
1371
  scenePass.end();
927
1372
  if (slots) {
@@ -934,6 +1379,7 @@ async function createRenderer(options) {
934
1379
  return named;
935
1380
  };
936
1381
  for (const step of graphPlan.steps) {
1382
+ if (step.kind === "compute") continue;
937
1383
  const input = resolveStepSurface(step.input);
938
1384
  const output = resolveStepSurface(step.output);
939
1385
  step.pass.render({
@@ -983,11 +1429,25 @@ async function createRenderer(options) {
983
1429
  initializationCleanups.length = 0;
984
1430
  return {
985
1431
  render,
1432
+ getStorageBuffer: (name) => {
1433
+ return storageBufferMap.get(name);
1434
+ },
1435
+ getDevice: () => {
1436
+ return device;
1437
+ },
986
1438
  destroy: () => {
987
1439
  isDestroyed = true;
988
1440
  device.removeEventListener("uncapturederror", handleUncapturedError);
989
1441
  frameBuffer.destroy();
990
1442
  uniformBuffer.destroy();
1443
+ for (const buffer of storageBufferMap.values()) buffer.destroy();
1444
+ storageBufferMap.clear();
1445
+ for (const pair of pingPongTexturePairs.values()) {
1446
+ pair.textureA.destroy();
1447
+ pair.textureB.destroy();
1448
+ }
1449
+ pingPongTexturePairs.clear();
1450
+ computePipelineCache.clear();
991
1451
  destroyRenderTexture(sourceSlotTarget);
992
1452
  destroyRenderTexture(targetSlotTarget);
993
1453
  for (const target of runtimeRenderTargets.values()) target.texture.destroy();