wgsl-renderer 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@ A multi-pass renderer based on WebGPU and WGSL.
8
8
 
9
9
  - 🖼️ **Multi-Pass Rendering** - Support for texture rendering, post-processing effects, and other multi-pass rendering
10
10
  - ⚡ **High-Performance Rendering Loop** - Support for single-frame rendering and loop rendering modes
11
+ - 🧮 **Compute Pass Support** - Mix compute and render passes in one pipeline
11
12
  - 🛠️ **TypeScript Support** - Complete type definitions and clear API separation
12
13
  - 🎮 **Uniform System** - Built-in uniform buffer management with dynamic parameter support
13
14
 
@@ -16,7 +17,7 @@ A multi-pass renderer based on WebGPU and WGSL.
16
17
  ### Installation
17
18
 
18
19
  ```bash
19
- npm i wgls-renderer
20
+ npm i wgsl-renderer
20
21
  ```
21
22
 
22
23
  ### Add Pass
@@ -211,10 +212,100 @@ interface RenderPassOptions {
211
212
  }
212
213
  ```
213
214
 
215
+ ### renderer.addComputePass(passOptions)
216
+
217
+ Add a compute pass. Compute and render passes are executed in sequence based on insertion order.
218
+
219
+ ```ts
220
+ interface ComputePassOptions {
221
+ name: string;
222
+ shaderCode: string;
223
+ entryPoint?: string; // Default is 'cs_main'
224
+ resources?: GPUBindingResource[];
225
+ bindGroupSets?: { [setName: string]: GPUBindingResource[] };
226
+ dispatch:
227
+ | { x: number; y?: number; z?: number }
228
+ | ((context: { width: number; height: number; passName: string }) => { x: number; y?: number; z?: number });
229
+ }
230
+ ```
231
+
232
+ ```ts
233
+ const uniforms = renderer.createUniforms(4)
234
+ const output = renderer.getPassTexture('sim', {
235
+ format: 'rgba8unorm',
236
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
237
+ })
238
+
239
+ renderer.addComputePass({
240
+ name: 'sim',
241
+ shaderCode: computeWGSL,
242
+ resources: [output, uniforms.getBuffer()],
243
+ dispatch: ({ width, height }) => ({
244
+ x: Math.ceil(width / 8),
245
+ y: Math.ceil(height / 8),
246
+ z: 1,
247
+ }),
248
+ })
249
+ ```
250
+
251
+ ### renderer.createStorageTexture(options?)
252
+
253
+ Create a storage-capable texture for compute workflows.
254
+
255
+ ```ts
256
+ interface StorageTextureOptions {
257
+ width?: number;
258
+ height?: number;
259
+ format?: GPUTextureFormat; // default: 'rgba8unorm'
260
+ usage?: GPUTextureUsageFlags;
261
+ label?: string;
262
+ }
263
+ ```
264
+
265
+ ```ts
266
+ const simTex = renderer.createStorageTexture({
267
+ width: canvas.width,
268
+ height: canvas.height,
269
+ format: 'rgba16float',
270
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
271
+ })
272
+ ```
273
+
274
+ ### renderer.createPingPongTextures(options?)
275
+
276
+ Create A/B textures for iterative simulation.
277
+
278
+ ```ts
279
+ const pingpong = renderer.createPingPongTextures({
280
+ format: 'rgba16float',
281
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
282
+ })
283
+
284
+ // per frame
285
+ const readView = pingpong.getReadView()
286
+ const writeView = pingpong.getWriteView()
287
+ pingpong.swap()
288
+ ```
289
+
214
290
  ### renderer.getPassTexture(passName)
215
291
 
216
292
  Get the output texture of the specified pass. The return value is not a real texture but a placeholder that automatically binds the output texture to the shader during actual rendering.
217
293
 
294
+ ```ts
295
+ renderer.getPassTexture(
296
+ passName: string,
297
+ options?: {
298
+ format?: GPUTextureFormat;
299
+ mipmaps?: boolean;
300
+ mipLevelCount?: number;
301
+ usage?: GPUTextureUsageFlags;
302
+ },
303
+ )
304
+ ```
305
+
306
+ When a pass output is used as a compute storage texture, pass `usage` with `GPUTextureUsage.STORAGE_BINDING`.
307
+ For storage textures, use storage-compatible formats such as `rgba8unorm` or `rgba16float` (avoid `bgra8unorm`).
308
+
218
309
  ```typescript
219
310
  // Get output texture of my_pass
220
311
  const passOutputTexture = renderer.getPassTexture('my_pass')
@@ -588,4 +679,4 @@ MIT License
588
679
 
589
680
  ## 🤝 Contributing
590
681
 
591
- Issues and Pull Requests are welcome!
682
+ Issues and Pull Requests are welcome!
package/README.zh-CN.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  - 🖼️ **多Pass渲染** - 支持纹理渲染、后处理效果等自定义多通道渲染
8
8
  - ⚡ **高性能渲染循环** - 支持单帧渲染和循环渲染模式
9
+ - 🧮 **Compute 通道支持** - 可在同一管线里混合执行 compute 与 render pass
9
10
  - 🛠️ **TypeScript支持** - 完整的类型定义和清晰的API分离
10
11
  - 🎮 **Uniform系统** - 内置uniform buffer管理,支持动态参数
11
12
 
@@ -14,7 +15,7 @@
14
15
  ### 安装
15
16
 
16
17
  ```bash
17
- npm i wgls-renderer
18
+ npm i wgsl-renderer
18
19
  ```
19
20
 
20
21
  ### 添加渲染通道
@@ -227,10 +228,100 @@ interface RenderPassOptions {
227
228
  }
228
229
  ```
229
230
 
231
+ ### renderer.addComputePass(passOptions)
232
+
233
+ 添加计算通道。Compute 和 Render 会按照添加顺序依次执行。
234
+
235
+ ```ts
236
+ interface ComputePassOptions {
237
+ name: string;
238
+ shaderCode: string;
239
+ entryPoint?: string; // 默认是 'cs_main'
240
+ resources?: GPUBindingResource[];
241
+ bindGroupSets?: { [setName: string]: GPUBindingResource[] };
242
+ dispatch:
243
+ | { x: number; y?: number; z?: number }
244
+ | ((context: { width: number; height: number; passName: string }) => { x: number; y?: number; z?: number });
245
+ }
246
+ ```
247
+
248
+ ```ts
249
+ const uniforms = renderer.createUniforms(4)
250
+ const output = renderer.getPassTexture('sim', {
251
+ format: 'rgba8unorm',
252
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
253
+ })
254
+
255
+ renderer.addComputePass({
256
+ name: 'sim',
257
+ shaderCode: computeWGSL,
258
+ resources: [output, uniforms.getBuffer()],
259
+ dispatch: ({ width, height }) => ({
260
+ x: Math.ceil(width / 8),
261
+ y: Math.ceil(height / 8),
262
+ z: 1,
263
+ }),
264
+ })
265
+ ```
266
+
267
+ ### renderer.createStorageTexture(options?)
268
+
269
+ 创建可用于 compute 的 storage 纹理。
270
+
271
+ ```ts
272
+ interface StorageTextureOptions {
273
+ width?: number;
274
+ height?: number;
275
+ format?: GPUTextureFormat; // 默认: 'rgba8unorm'
276
+ usage?: GPUTextureUsageFlags;
277
+ label?: string;
278
+ }
279
+ ```
280
+
281
+ ```ts
282
+ const simTex = renderer.createStorageTexture({
283
+ width: canvas.width,
284
+ height: canvas.height,
285
+ format: 'rgba16float',
286
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
287
+ })
288
+ ```
289
+
290
+ ### renderer.createPingPongTextures(options?)
291
+
292
+ 创建 A/B 双纹理,用于迭代模拟(ping-pong)。
293
+
294
+ ```ts
295
+ const pingpong = renderer.createPingPongTextures({
296
+ format: 'rgba16float',
297
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
298
+ })
299
+
300
+ // 每帧
301
+ const readView = pingpong.getReadView()
302
+ const writeView = pingpong.getWriteView()
303
+ pingpong.swap()
304
+ ```
305
+
230
306
  ### renderer.getPassTexture(passName)
231
307
 
232
308
  获取指定通道的输出纹理,返回值并不是真正的纹理,而是一个占位符,只在实际渲染时自动将输出纹理绑定到着色器。
233
309
 
310
+ ```ts
311
+ renderer.getPassTexture(
312
+ passName: string,
313
+ options?: {
314
+ format?: GPUTextureFormat;
315
+ mipmaps?: boolean;
316
+ mipLevelCount?: number;
317
+ usage?: GPUTextureUsageFlags;
318
+ },
319
+ )
320
+ ```
321
+
322
+ 如果要把输出纹理用于 compute 的 storage texture,需要在 `usage` 里包含 `GPUTextureUsage.STORAGE_BINDING`。
323
+ storage texture 请使用支持的格式(如 `rgba8unorm`、`rgba16float`),不要使用 `bgra8unorm`。
324
+
234
325
  ```typescript
235
326
  // 获取my_pass通道的输出纹理
236
327
  const passOutputTexture = renderer.getPassTexture('my_pass')
@@ -621,4 +712,4 @@ MIT License
621
712
 
622
713
  ## 🤝 贡献
623
714
 
624
- 欢迎提交Issue和Pull Request!
715
+ 欢迎提交Issue和Pull Request!
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/RenderPass.ts
3
3
  var RenderPass = class {
4
+ passType = "render";
4
5
  name;
5
6
  pipeline;
6
7
  bindGroup;
@@ -112,7 +113,8 @@ var RenderPass = class {
112
113
  if (this.bindGroups[setName]) {
113
114
  this.activeBindGroupSet = setName;
114
115
  this.bindGroup = this.bindGroups[setName];
115
- } else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
116
+ } else if (this.descriptor.bindGroupSets?.[setName]) this.activeBindGroupSet = setName;
117
+ else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
116
118
  }
117
119
  /**
118
120
  * Get the current active bind group
@@ -309,6 +311,78 @@ var RenderPass = class {
309
311
  }
310
312
  };
311
313
  //#endregion
314
+ //#region src/ComputePass.ts
315
+ var ComputePass = class {
316
+ passType = "compute";
317
+ name;
318
+ pipeline;
319
+ bindGroup;
320
+ passResources = [];
321
+ bindGroups = {};
322
+ activeBindGroupSet = "default";
323
+ enabled = true;
324
+ descriptor;
325
+ compilationInfo;
326
+ dispatch;
327
+ device;
328
+ constructor(descriptor, device, layout = "auto") {
329
+ this.device = device;
330
+ this.descriptor = descriptor;
331
+ this.name = descriptor.name;
332
+ this.dispatch = descriptor.dispatch;
333
+ const module = this.device.createShaderModule({
334
+ code: descriptor.shaderCode,
335
+ label: `Compute shader for ${descriptor.name}`
336
+ });
337
+ this.compilationInfo = module.getCompilationInfo();
338
+ const entryPoint = descriptor.entryPoint || "cs_main";
339
+ this.pipeline = this.device.createComputePipeline({
340
+ layout,
341
+ compute: {
342
+ module,
343
+ entryPoint
344
+ }
345
+ });
346
+ this.bindGroup = null;
347
+ }
348
+ updateBindGroup(newEntries) {
349
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
350
+ this.bindGroup = this.device.createBindGroup({
351
+ layout: bindGroupLayout,
352
+ entries: newEntries
353
+ });
354
+ this.bindGroups.default = this.bindGroup;
355
+ }
356
+ switchBindGroupSet(setName) {
357
+ if (this.bindGroups[setName]) {
358
+ this.activeBindGroupSet = setName;
359
+ this.bindGroup = this.bindGroups[setName];
360
+ } else if (this.descriptor.bindGroupSets?.[setName]) this.activeBindGroupSet = setName;
361
+ else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
362
+ }
363
+ getActiveBindGroup() {
364
+ return this.bindGroups[this.activeBindGroupSet] || this.bindGroup;
365
+ }
366
+ getBindGroupSets() {
367
+ return Object.keys(this.bindGroups);
368
+ }
369
+ updateBindGroupSetResources(setName, resources) {
370
+ const entries = [];
371
+ resources.forEach((resource, index) => {
372
+ if (resource) entries.push({
373
+ binding: index,
374
+ resource
375
+ });
376
+ });
377
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
378
+ this.bindGroups[setName] = this.device.createBindGroup({
379
+ layout: bindGroupLayout,
380
+ entries
381
+ });
382
+ if (this.activeBindGroupSet === setName) this.bindGroup = this.bindGroups[setName];
383
+ }
384
+ };
385
+ //#endregion
312
386
  //#region src/TextureManager.ts
313
387
  var TextureManager = class {
314
388
  textures = /* @__PURE__ */ new Map();
@@ -322,12 +396,12 @@ var TextureManager = class {
322
396
  this.width = width;
323
397
  this.height = height;
324
398
  }
325
- createTexture(name, format, mipLevelCount) {
399
+ createTexture(name, format, mipLevelCount, usage) {
326
400
  if (this.textures.has(name)) this.textures.get(name).destroy();
327
401
  const texture = this.device.createTexture({
328
402
  size: [this.width, this.height],
329
403
  format: format || "bgra8unorm",
330
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
404
+ usage: usage ?? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
331
405
  mipLevelCount: mipLevelCount || 1
332
406
  });
333
407
  this.textures.set(name, texture);
@@ -470,6 +544,9 @@ var WGSLRenderer = class {
470
544
  renderMode = "normal";
471
545
  readBuffer = null;
472
546
  hasClearedCanvasThisFrame = false;
547
+ getPassOutputTextureName(passName) {
548
+ return `pass_${passName}_output`;
549
+ }
473
550
  constructor(canvas, options) {
474
551
  this.canvas = canvas;
475
552
  this.options = options;
@@ -542,12 +619,14 @@ var WGSLRenderer = class {
542
619
  resolveTextureRef(ref) {
543
620
  const targetPass = this.passes.find((pass) => pass.name === ref.passName);
544
621
  if (!targetPass) throw new Error(`Cannot find pass named '${ref.passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
545
- if (targetPass.view) return targetPass.view;
546
- const textureName = `pass_${this.passes.indexOf(targetPass)}_output`;
622
+ if (targetPass.passType === "render" && targetPass.view) return targetPass.view;
623
+ const textureName = this.getPassOutputTextureName(targetPass.name);
547
624
  let texture = this.textureManager.getTexture(textureName);
548
625
  if (!texture) {
549
- const format = this.passes.find((pass) => pass.name === ref.passName)?.format || this.format;
550
- texture = this.textureManager.createTexture(textureName, format, ref.options?.mipLevelCount);
626
+ const targetPass = this.passes.find((pass) => pass.name === ref.passName);
627
+ const format = ref.options?.format ?? (targetPass?.passType === "render" ? targetPass.format || this.format : this.format);
628
+ const usage = ref.options?.usage ?? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
629
+ texture = this.textureManager.createTexture(textureName, format, ref.options?.mipLevelCount, usage);
551
630
  }
552
631
  return texture.createView({
553
632
  baseMipLevel: 0,
@@ -568,7 +647,7 @@ var WGSLRenderer = class {
568
647
  getPassTextureRaw(passName) {
569
648
  const targetPass = this.passes.find((pass) => pass.name === passName);
570
649
  if (!targetPass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
571
- const textureName = `pass_${this.passes.indexOf(targetPass)}_output`;
650
+ const textureName = this.getPassOutputTextureName(targetPass.name);
572
651
  const texture = this.textureManager.getTexture(textureName);
573
652
  if (texture) return texture;
574
653
  return null;
@@ -675,6 +754,28 @@ var WGSLRenderer = class {
675
754
  const pipelineFormat = descriptor.format || this.format;
676
755
  return new RenderPass(internalDescriptor, this.device, pipelineFormat);
677
756
  }
757
+ createComputePass(descriptor) {
758
+ const finalBindGroupEntries = [];
759
+ descriptor.resources?.forEach((resource, index) => {
760
+ finalBindGroupEntries.push({
761
+ binding: index,
762
+ resource
763
+ });
764
+ });
765
+ let bindGroupSetsCopy = void 0;
766
+ if (descriptor.bindGroupSets) {
767
+ bindGroupSetsCopy = {};
768
+ for (const [setName, resources] of Object.entries(descriptor.bindGroupSets)) bindGroupSetsCopy[setName] = [...resources];
769
+ }
770
+ return new ComputePass({
771
+ name: descriptor.name,
772
+ shaderCode: descriptor.shaderCode,
773
+ entryPoint: descriptor.entryPoint,
774
+ bindGroupEntries: finalBindGroupEntries,
775
+ bindGroupSets: bindGroupSetsCopy,
776
+ dispatch: descriptor.dispatch
777
+ }, this.device);
778
+ }
678
779
  /**
679
780
  * Add a render pass to the multi-pass pipeline
680
781
  */
@@ -683,6 +784,11 @@ var WGSLRenderer = class {
683
784
  pass.passResources = descriptor.resources ?? [];
684
785
  this.passes.push(pass);
685
786
  }
787
+ addComputePass(descriptor) {
788
+ const pass = this.createComputePass(descriptor);
789
+ pass.passResources = descriptor.resources ?? [];
790
+ this.passes.push(pass);
791
+ }
686
792
  /**
687
793
  * Validate all shader compilations
688
794
  * Call this after adding all passes to check for shader errors
@@ -695,7 +801,8 @@ var WGSLRenderer = class {
695
801
  for (const msg of info.messages) errMsg += `[WGSL ${msg.type}] Shader compilation failed for pass ${pass.name} (${msg.lineNum}:${msg.linePos}): ${msg.message}\n`;
696
802
  return Promise.reject({
697
803
  messages: info.messages,
698
- cause: errMsg
804
+ cause: errMsg,
805
+ pass: pass.name
699
806
  });
700
807
  }
701
808
  }));
@@ -721,6 +828,16 @@ var WGSLRenderer = class {
721
828
  });
722
829
  this.passes.splice(i, 0, ...newPasses);
723
830
  }
831
+ insertComputePassesTo(passName, descriptors) {
832
+ const i = this.passes.findIndex((p) => p.name === passName);
833
+ if (i === -1) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
834
+ const newPasses = descriptors.map((desc) => {
835
+ const pass = this.createComputePass(desc);
836
+ pass.passResources = desc.resources ?? [];
837
+ return pass;
838
+ });
839
+ this.passes.splice(i, 0, ...newPasses);
840
+ }
724
841
  /**
725
842
  * Resolve resource to actual GPU binding resource
726
843
  * Handles PassTextureRef by getting the current texture view with validation
@@ -743,6 +860,14 @@ var WGSLRenderer = class {
743
860
  });
744
861
  });
745
862
  pass.updateBindGroup(finalBindGroupEntries);
863
+ const bindGroupSets = pass.descriptor.bindGroupSets;
864
+ if (bindGroupSets) for (const [setName, resources] of Object.entries(bindGroupSets)) {
865
+ const resolvedResources = resources.map((resource) => {
866
+ if (resource) return this.resolveResource(resource);
867
+ return resource;
868
+ });
869
+ pass.updateBindGroupSetResources(setName, resolvedResources);
870
+ }
746
871
  });
747
872
  }
748
873
  /**
@@ -819,16 +944,36 @@ var WGSLRenderer = class {
819
944
  if (this.passes.length === 0) return;
820
945
  const enabledPasses = this.getEnabledPasses();
821
946
  if (enabledPasses.length === 0) return;
947
+ const enabledRenderPasses = enabledPasses.filter((pass) => pass.passType === "render");
948
+ if (enabledRenderPasses.length === 0) return;
822
949
  this.hasClearedCanvasThisFrame = false;
823
950
  this.updateBindGroups();
824
951
  const commandEncoder = this.device.createCommandEncoder();
825
952
  const canvasWidth = this.canvas.width || this.canvas.clientWidth;
826
953
  const canvasHeight = this.canvas.height || this.canvas.clientHeight;
954
+ const lastRenderPass = enabledRenderPasses[enabledRenderPasses.length - 1];
827
955
  for (let i = 0; i < enabledPasses.length; i++) {
828
956
  const pass = enabledPasses[i];
957
+ if (pass.passType === "compute") {
958
+ const computePass = commandEncoder.beginComputePass();
959
+ computePass.setPipeline(pass.pipeline);
960
+ const activeBindGroup = pass.getActiveBindGroup();
961
+ if (activeBindGroup) computePass.setBindGroup(0, activeBindGroup);
962
+ const dispatch = typeof pass.dispatch === "function" ? pass.dispatch({
963
+ width: canvasWidth,
964
+ height: canvasHeight,
965
+ passName: pass.name
966
+ }) : pass.dispatch;
967
+ const x = Math.max(1, Math.floor(dispatch.x));
968
+ const y = Math.max(1, Math.floor(dispatch.y ?? 1));
969
+ const z = Math.max(1, Math.floor(dispatch.z ?? 1));
970
+ computePass.dispatchWorkgroups(x, y, z);
971
+ computePass.end();
972
+ continue;
973
+ }
829
974
  let loadOp = "load";
830
975
  const isFirst = i === 0;
831
- const isLastPass = i === enabledPasses.length - 1;
976
+ const isLastPass = pass.name === lastRenderPass.name;
832
977
  if (isFirst) loadOp = "clear";
833
978
  let renderTargetView;
834
979
  let isRenderingToCanvas = false;
@@ -845,7 +990,7 @@ var WGSLRenderer = class {
845
990
  renderTargetView = this.ctx.getCurrentTexture().createView();
846
991
  isRenderingToCanvas = true;
847
992
  } else {
848
- const textureName = `pass_${i}_output`;
993
+ const textureName = this.getPassOutputTextureName(pass.name);
849
994
  let texture = this.textureManager.getTexture(textureName);
850
995
  if (!texture) texture = this.textureManager.createTexture(textureName, pass.format || this.format);
851
996
  renderTargetView = texture.createView();
@@ -927,6 +1072,55 @@ var WGSLRenderer = class {
927
1072
  }
928
1073
  return pixelData;
929
1074
  }
1075
+ /**
1076
+ * Create a storage-capable texture (useful for compute passes)
1077
+ */
1078
+ createStorageTexture(options) {
1079
+ const width = options?.width ?? (this.canvas.width || this.canvas.clientWidth);
1080
+ const height = options?.height ?? (this.canvas.height || this.canvas.clientHeight);
1081
+ const format = options?.format ?? "rgba8unorm";
1082
+ const usage = options?.usage ?? GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC;
1083
+ return this.device.createTexture({
1084
+ label: options?.label,
1085
+ size: [
1086
+ width,
1087
+ height,
1088
+ 1
1089
+ ],
1090
+ format,
1091
+ usage
1092
+ });
1093
+ }
1094
+ /**
1095
+ * Create ping-pong textures and a swap helper for iterative compute simulation
1096
+ */
1097
+ createPingPongTextures(options) {
1098
+ const textureA = this.createStorageTexture({
1099
+ ...options,
1100
+ label: options?.label ? `${options.label}_A` : "pingpong_A"
1101
+ });
1102
+ const textureB = this.createStorageTexture({
1103
+ ...options,
1104
+ label: options?.label ? `${options.label}_B` : "pingpong_B"
1105
+ });
1106
+ let read = textureA;
1107
+ let write = textureB;
1108
+ return {
1109
+ get read() {
1110
+ return read;
1111
+ },
1112
+ get write() {
1113
+ return write;
1114
+ },
1115
+ getReadView: () => read.createView(),
1116
+ getWriteView: () => write.createView(),
1117
+ swap: () => {
1118
+ const nextRead = write;
1119
+ write = read;
1120
+ read = nextRead;
1121
+ }
1122
+ };
1123
+ }
930
1124
  };
931
1125
  async function createWGSLRenderer(cvs, options) {
932
1126
  const renderer = new WGSLRenderer(cvs, options);
@@ -1,28 +1,19 @@
1
1
  //#region src/PassTextureRef.d.ts
2
2
  declare const PASS_TEXTURE_REF_SYMBOL: unique symbol;
3
+ interface PassTextureOptions {
4
+ format?: GPUTextureFormat;
5
+ mipmaps?: boolean;
6
+ usage?: GPUTextureUsageFlags;
7
+ mipLevelCount?: number;
8
+ }
3
9
  declare class PassTextureRef {
4
10
  readonly [PASS_TEXTURE_REF_SYMBOL] = true;
5
11
  readonly passName: string;
6
- readonly options?: {
7
- format?: GPUTextureFormat;
8
- mipmaps?: boolean;
9
- usage?: GPUTextureUsageFlags;
10
- mipLevelCount?: number;
11
- };
12
- constructor(passName: string, options?: {
13
- format?: GPUTextureFormat;
14
- mipmaps?: boolean;
15
- usage?: GPUTextureUsageFlags;
16
- mipLevelCount?: number;
17
- });
12
+ readonly options?: PassTextureOptions;
13
+ constructor(passName: string, options?: PassTextureOptions);
18
14
  static is(obj: any): obj is PassTextureRef;
19
15
  static fromGPUBindingResource(resource: GPUBindingResource): PassTextureRef | null;
20
- static create(passName: string, options?: {
21
- format?: GPUTextureFormat;
22
- mipmaps?: boolean;
23
- usage?: GPUTextureUsageFlags;
24
- mipLevelCount?: number;
25
- }): PassTextureRef;
16
+ static create(passName: string, options?: PassTextureOptions): PassTextureRef;
26
17
  }
27
18
  /**
28
19
  * Create a texture view suitable for sampling (not for render attachments)
@@ -82,6 +73,7 @@ interface InternalRenderPassDescriptor {
82
73
  renderToCanvas?: boolean;
83
74
  }
84
75
  declare class RenderPass {
76
+ readonly passType = "render";
85
77
  name: string;
86
78
  pipeline: GPURenderPipeline;
87
79
  bindGroup: GPUBindGroup | null;
@@ -140,6 +132,66 @@ declare class RenderPass {
140
132
  private getBlendState;
141
133
  }
142
134
  //#endregion
135
+ //#region src/ComputePass.d.ts
136
+ interface ComputeDispatchSize {
137
+ x: number;
138
+ y?: number;
139
+ z?: number;
140
+ }
141
+ type ComputeDispatchResolver = (context: {
142
+ width: number;
143
+ height: number;
144
+ passName: string;
145
+ }) => ComputeDispatchSize;
146
+ interface ComputePassOptions {
147
+ name: string;
148
+ shaderCode: string;
149
+ entryPoint?: string;
150
+ resources?: BindingResource[];
151
+ bindGroupSets?: {
152
+ [setName: string]: BindingResource[];
153
+ };
154
+ dispatch: ComputeDispatchResolver | ComputeDispatchSize;
155
+ }
156
+ interface InternalComputePassDescriptor {
157
+ name: string;
158
+ shaderCode: string;
159
+ entryPoint?: string;
160
+ bindGroupEntries: {
161
+ binding: number;
162
+ resource: BindingResource;
163
+ }[];
164
+ bindGroupSets?: {
165
+ [setName: string]: BindingResource[];
166
+ };
167
+ dispatch: ComputePassOptions['dispatch'];
168
+ }
169
+ declare class ComputePass {
170
+ readonly passType = "compute";
171
+ name: string;
172
+ pipeline: GPUComputePipeline;
173
+ bindGroup: GPUBindGroup | null;
174
+ passResources: BindingResource[];
175
+ bindGroups: {
176
+ [setName: string]: GPUBindGroup;
177
+ };
178
+ activeBindGroupSet: string;
179
+ enabled: boolean;
180
+ descriptor: InternalComputePassDescriptor;
181
+ compilationInfo: Promise<GPUCompilationInfo>;
182
+ dispatch: ComputePassOptions['dispatch'];
183
+ private device;
184
+ constructor(descriptor: InternalComputePassDescriptor, device: GPUDevice, layout?: GPUPipelineLayout | 'auto');
185
+ updateBindGroup(newEntries: {
186
+ binding: number;
187
+ resource: GPUBindingResource;
188
+ }[]): void;
189
+ switchBindGroupSet(setName: string): void;
190
+ getActiveBindGroup(): GPUBindGroup | null;
191
+ getBindGroupSets(): string[];
192
+ updateBindGroupSetResources(setName: string, resources: BindingResource[]): void;
193
+ }
194
+ //#endregion
143
195
  //#region src/index.d.ts
144
196
  declare enum RenderMode {
145
197
  NORMAL = "normal",
@@ -158,6 +210,20 @@ interface Uniforms {
158
210
  (): GPUBuffer;
159
211
  };
160
212
  }
213
+ interface StorageTextureOptions {
214
+ width?: number;
215
+ height?: number;
216
+ format?: GPUTextureFormat;
217
+ usage?: GPUTextureUsageFlags;
218
+ label?: string;
219
+ }
220
+ interface PingPongTextureSet {
221
+ read: GPUTexture;
222
+ write: GPUTexture;
223
+ getReadView: () => GPUTextureView;
224
+ getWriteView: () => GPUTextureView;
225
+ swap: () => void;
226
+ }
161
227
  declare class WGSLRenderer {
162
228
  canvas: HTMLCanvasElement;
163
229
  options?: WGSLRendererOptions | undefined;
@@ -171,6 +237,7 @@ declare class WGSLRenderer {
171
237
  private renderMode;
172
238
  private readBuffer;
173
239
  private hasClearedCanvasThisFrame;
240
+ private getPassOutputTextureName;
174
241
  constructor(canvas: HTMLCanvasElement, options?: WGSLRendererOptions | undefined);
175
242
  init(): Promise<void>;
176
243
  resize(width: number, height: number): Promise<void>;
@@ -192,12 +259,7 @@ declare class WGSLRenderer {
192
259
  * @param passName Name of the pass to reference
193
260
  * @param options Optional texture creation options for when the texture needs to be created
194
261
  */
195
- getPassTexture(passName: string, options?: {
196
- format?: GPUTextureFormat;
197
- mipmaps?: boolean;
198
- usage?: GPUTextureUsageFlags;
199
- mipLevelCount?: number;
200
- }): PassTextureRef;
262
+ getPassTexture(passName: string, options?: PassTextureOptions): PassTextureRef;
201
263
  /**
202
264
  * Resolve a PassTextureRef to actual GPUTextureView with validation
203
265
  */
@@ -205,7 +267,7 @@ declare class WGSLRenderer {
205
267
  /**
206
268
  * Get pass by name
207
269
  */
208
- getPassByName(passName: string): RenderPass | undefined;
270
+ getPassByName(passName: string): ComputePass | RenderPass | undefined;
209
271
  /**
210
272
  * Get the raw GPUTexture for a pass (useful for creating custom views)
211
273
  * Note: The returned texture should not be used directly as a render attachment
@@ -231,15 +293,15 @@ declare class WGSLRenderer {
231
293
  /**
232
294
  * Get all passes (enabled and disabled)
233
295
  */
234
- getAllPasses(): RenderPass[];
296
+ getAllPasses(): Array<ComputePass | RenderPass>;
235
297
  /**
236
298
  * Get only enabled passes
237
299
  */
238
- getEnabledPasses(): RenderPass[];
300
+ getEnabledPasses(): Array<ComputePass | RenderPass>;
239
301
  /**
240
302
  * Set the entire passes array (replaces existing passes)
241
303
  */
242
- setPasses(passes: RenderPass[]): void;
304
+ setPasses(passes: Array<ComputePass | RenderPass>): void;
243
305
  /**
244
306
  * Switch bind group set for a specific pass
245
307
  */
@@ -250,10 +312,12 @@ declare class WGSLRenderer {
250
312
  */
251
313
  updateBindGroupSetResources(passName: string, setName: string, resources: BindingResource[]): void;
252
314
  private createPass;
315
+ private createComputePass;
253
316
  /**
254
317
  * Add a render pass to the multi-pass pipeline
255
318
  */
256
319
  addPass(descriptor: RenderPassOptions): void;
320
+ addComputePass(descriptor: ComputePassOptions): void;
257
321
  /**
258
322
  * Validate all shader compilations
259
323
  * Call this after adding all passes to check for shader errors
@@ -261,6 +325,7 @@ declare class WGSLRenderer {
261
325
  */
262
326
  validateShaders(): Promise<void>;
263
327
  insertPassesTo(passName: string, descriptors: RenderPassOptions[]): void;
328
+ insertComputePassesTo(passName: string, descriptors: ComputePassOptions[]): void;
264
329
  /**
265
330
  * Resolve resource to actual GPU binding resource
266
331
  * Handles PassTextureRef by getting the current texture view with validation
@@ -302,7 +367,15 @@ declare class WGSLRenderer {
302
367
  * @returns Uint8Array格式的RGBA像素数据
303
368
  */
304
369
  captureFrameFast(): Promise<Uint8Array>;
370
+ /**
371
+ * Create a storage-capable texture (useful for compute passes)
372
+ */
373
+ createStorageTexture(options?: StorageTextureOptions): GPUTexture;
374
+ /**
375
+ * Create ping-pong textures and a swap helper for iterative compute simulation
376
+ */
377
+ createPingPongTextures(options?: StorageTextureOptions): PingPongTextureSet;
305
378
  }
306
379
  declare function createWGSLRenderer(cvs: HTMLCanvasElement, options?: WGSLRendererOptions): Promise<WGSLRenderer>;
307
380
  //#endregion
308
- export { BindingResource, PassTextureRef, RenderMode, RenderPassOptions, Uniforms, WGSLRenderer, createSamplingView, createWGSLRenderer };
381
+ export { BindingResource, type ComputePassOptions, PassTextureRef, type PingPongTextureSet, RenderMode, RenderPassOptions, type StorageTextureOptions, type Uniforms, WGSLRenderer, createSamplingView, createWGSLRenderer };
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  //#region src/RenderPass.ts
2
2
  var RenderPass = class {
3
+ passType = "render";
3
4
  name;
4
5
  pipeline;
5
6
  bindGroup;
@@ -111,7 +112,8 @@ var RenderPass = class {
111
112
  if (this.bindGroups[setName]) {
112
113
  this.activeBindGroupSet = setName;
113
114
  this.bindGroup = this.bindGroups[setName];
114
- } else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
115
+ } else if (this.descriptor.bindGroupSets?.[setName]) this.activeBindGroupSet = setName;
116
+ else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
115
117
  }
116
118
  /**
117
119
  * Get the current active bind group
@@ -308,6 +310,78 @@ var RenderPass = class {
308
310
  }
309
311
  };
310
312
  //#endregion
313
+ //#region src/ComputePass.ts
314
+ var ComputePass = class {
315
+ passType = "compute";
316
+ name;
317
+ pipeline;
318
+ bindGroup;
319
+ passResources = [];
320
+ bindGroups = {};
321
+ activeBindGroupSet = "default";
322
+ enabled = true;
323
+ descriptor;
324
+ compilationInfo;
325
+ dispatch;
326
+ device;
327
+ constructor(descriptor, device, layout = "auto") {
328
+ this.device = device;
329
+ this.descriptor = descriptor;
330
+ this.name = descriptor.name;
331
+ this.dispatch = descriptor.dispatch;
332
+ const module = this.device.createShaderModule({
333
+ code: descriptor.shaderCode,
334
+ label: `Compute shader for ${descriptor.name}`
335
+ });
336
+ this.compilationInfo = module.getCompilationInfo();
337
+ const entryPoint = descriptor.entryPoint || "cs_main";
338
+ this.pipeline = this.device.createComputePipeline({
339
+ layout,
340
+ compute: {
341
+ module,
342
+ entryPoint
343
+ }
344
+ });
345
+ this.bindGroup = null;
346
+ }
347
+ updateBindGroup(newEntries) {
348
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
349
+ this.bindGroup = this.device.createBindGroup({
350
+ layout: bindGroupLayout,
351
+ entries: newEntries
352
+ });
353
+ this.bindGroups.default = this.bindGroup;
354
+ }
355
+ switchBindGroupSet(setName) {
356
+ if (this.bindGroups[setName]) {
357
+ this.activeBindGroupSet = setName;
358
+ this.bindGroup = this.bindGroups[setName];
359
+ } else if (this.descriptor.bindGroupSets?.[setName]) this.activeBindGroupSet = setName;
360
+ else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
361
+ }
362
+ getActiveBindGroup() {
363
+ return this.bindGroups[this.activeBindGroupSet] || this.bindGroup;
364
+ }
365
+ getBindGroupSets() {
366
+ return Object.keys(this.bindGroups);
367
+ }
368
+ updateBindGroupSetResources(setName, resources) {
369
+ const entries = [];
370
+ resources.forEach((resource, index) => {
371
+ if (resource) entries.push({
372
+ binding: index,
373
+ resource
374
+ });
375
+ });
376
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
377
+ this.bindGroups[setName] = this.device.createBindGroup({
378
+ layout: bindGroupLayout,
379
+ entries
380
+ });
381
+ if (this.activeBindGroupSet === setName) this.bindGroup = this.bindGroups[setName];
382
+ }
383
+ };
384
+ //#endregion
311
385
  //#region src/TextureManager.ts
312
386
  var TextureManager = class {
313
387
  textures = /* @__PURE__ */ new Map();
@@ -321,12 +395,12 @@ var TextureManager = class {
321
395
  this.width = width;
322
396
  this.height = height;
323
397
  }
324
- createTexture(name, format, mipLevelCount) {
398
+ createTexture(name, format, mipLevelCount, usage) {
325
399
  if (this.textures.has(name)) this.textures.get(name).destroy();
326
400
  const texture = this.device.createTexture({
327
401
  size: [this.width, this.height],
328
402
  format: format || "bgra8unorm",
329
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
403
+ usage: usage ?? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
330
404
  mipLevelCount: mipLevelCount || 1
331
405
  });
332
406
  this.textures.set(name, texture);
@@ -469,6 +543,9 @@ var WGSLRenderer = class {
469
543
  renderMode = "normal";
470
544
  readBuffer = null;
471
545
  hasClearedCanvasThisFrame = false;
546
+ getPassOutputTextureName(passName) {
547
+ return `pass_${passName}_output`;
548
+ }
472
549
  constructor(canvas, options) {
473
550
  this.canvas = canvas;
474
551
  this.options = options;
@@ -541,12 +618,14 @@ var WGSLRenderer = class {
541
618
  resolveTextureRef(ref) {
542
619
  const targetPass = this.passes.find((pass) => pass.name === ref.passName);
543
620
  if (!targetPass) throw new Error(`Cannot find pass named '${ref.passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
544
- if (targetPass.view) return targetPass.view;
545
- const textureName = `pass_${this.passes.indexOf(targetPass)}_output`;
621
+ if (targetPass.passType === "render" && targetPass.view) return targetPass.view;
622
+ const textureName = this.getPassOutputTextureName(targetPass.name);
546
623
  let texture = this.textureManager.getTexture(textureName);
547
624
  if (!texture) {
548
- const format = this.passes.find((pass) => pass.name === ref.passName)?.format || this.format;
549
- texture = this.textureManager.createTexture(textureName, format, ref.options?.mipLevelCount);
625
+ const targetPass = this.passes.find((pass) => pass.name === ref.passName);
626
+ const format = ref.options?.format ?? (targetPass?.passType === "render" ? targetPass.format || this.format : this.format);
627
+ const usage = ref.options?.usage ?? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
628
+ texture = this.textureManager.createTexture(textureName, format, ref.options?.mipLevelCount, usage);
550
629
  }
551
630
  return texture.createView({
552
631
  baseMipLevel: 0,
@@ -567,7 +646,7 @@ var WGSLRenderer = class {
567
646
  getPassTextureRaw(passName) {
568
647
  const targetPass = this.passes.find((pass) => pass.name === passName);
569
648
  if (!targetPass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
570
- const textureName = `pass_${this.passes.indexOf(targetPass)}_output`;
649
+ const textureName = this.getPassOutputTextureName(targetPass.name);
571
650
  const texture = this.textureManager.getTexture(textureName);
572
651
  if (texture) return texture;
573
652
  return null;
@@ -674,6 +753,28 @@ var WGSLRenderer = class {
674
753
  const pipelineFormat = descriptor.format || this.format;
675
754
  return new RenderPass(internalDescriptor, this.device, pipelineFormat);
676
755
  }
756
+ createComputePass(descriptor) {
757
+ const finalBindGroupEntries = [];
758
+ descriptor.resources?.forEach((resource, index) => {
759
+ finalBindGroupEntries.push({
760
+ binding: index,
761
+ resource
762
+ });
763
+ });
764
+ let bindGroupSetsCopy = void 0;
765
+ if (descriptor.bindGroupSets) {
766
+ bindGroupSetsCopy = {};
767
+ for (const [setName, resources] of Object.entries(descriptor.bindGroupSets)) bindGroupSetsCopy[setName] = [...resources];
768
+ }
769
+ return new ComputePass({
770
+ name: descriptor.name,
771
+ shaderCode: descriptor.shaderCode,
772
+ entryPoint: descriptor.entryPoint,
773
+ bindGroupEntries: finalBindGroupEntries,
774
+ bindGroupSets: bindGroupSetsCopy,
775
+ dispatch: descriptor.dispatch
776
+ }, this.device);
777
+ }
677
778
  /**
678
779
  * Add a render pass to the multi-pass pipeline
679
780
  */
@@ -682,6 +783,11 @@ var WGSLRenderer = class {
682
783
  pass.passResources = descriptor.resources ?? [];
683
784
  this.passes.push(pass);
684
785
  }
786
+ addComputePass(descriptor) {
787
+ const pass = this.createComputePass(descriptor);
788
+ pass.passResources = descriptor.resources ?? [];
789
+ this.passes.push(pass);
790
+ }
685
791
  /**
686
792
  * Validate all shader compilations
687
793
  * Call this after adding all passes to check for shader errors
@@ -694,7 +800,8 @@ var WGSLRenderer = class {
694
800
  for (const msg of info.messages) errMsg += `[WGSL ${msg.type}] Shader compilation failed for pass ${pass.name} (${msg.lineNum}:${msg.linePos}): ${msg.message}\n`;
695
801
  return Promise.reject({
696
802
  messages: info.messages,
697
- cause: errMsg
803
+ cause: errMsg,
804
+ pass: pass.name
698
805
  });
699
806
  }
700
807
  }));
@@ -720,6 +827,16 @@ var WGSLRenderer = class {
720
827
  });
721
828
  this.passes.splice(i, 0, ...newPasses);
722
829
  }
830
+ insertComputePassesTo(passName, descriptors) {
831
+ const i = this.passes.findIndex((p) => p.name === passName);
832
+ if (i === -1) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
833
+ const newPasses = descriptors.map((desc) => {
834
+ const pass = this.createComputePass(desc);
835
+ pass.passResources = desc.resources ?? [];
836
+ return pass;
837
+ });
838
+ this.passes.splice(i, 0, ...newPasses);
839
+ }
723
840
  /**
724
841
  * Resolve resource to actual GPU binding resource
725
842
  * Handles PassTextureRef by getting the current texture view with validation
@@ -742,6 +859,14 @@ var WGSLRenderer = class {
742
859
  });
743
860
  });
744
861
  pass.updateBindGroup(finalBindGroupEntries);
862
+ const bindGroupSets = pass.descriptor.bindGroupSets;
863
+ if (bindGroupSets) for (const [setName, resources] of Object.entries(bindGroupSets)) {
864
+ const resolvedResources = resources.map((resource) => {
865
+ if (resource) return this.resolveResource(resource);
866
+ return resource;
867
+ });
868
+ pass.updateBindGroupSetResources(setName, resolvedResources);
869
+ }
745
870
  });
746
871
  }
747
872
  /**
@@ -818,16 +943,36 @@ var WGSLRenderer = class {
818
943
  if (this.passes.length === 0) return;
819
944
  const enabledPasses = this.getEnabledPasses();
820
945
  if (enabledPasses.length === 0) return;
946
+ const enabledRenderPasses = enabledPasses.filter((pass) => pass.passType === "render");
947
+ if (enabledRenderPasses.length === 0) return;
821
948
  this.hasClearedCanvasThisFrame = false;
822
949
  this.updateBindGroups();
823
950
  const commandEncoder = this.device.createCommandEncoder();
824
951
  const canvasWidth = this.canvas.width || this.canvas.clientWidth;
825
952
  const canvasHeight = this.canvas.height || this.canvas.clientHeight;
953
+ const lastRenderPass = enabledRenderPasses[enabledRenderPasses.length - 1];
826
954
  for (let i = 0; i < enabledPasses.length; i++) {
827
955
  const pass = enabledPasses[i];
956
+ if (pass.passType === "compute") {
957
+ const computePass = commandEncoder.beginComputePass();
958
+ computePass.setPipeline(pass.pipeline);
959
+ const activeBindGroup = pass.getActiveBindGroup();
960
+ if (activeBindGroup) computePass.setBindGroup(0, activeBindGroup);
961
+ const dispatch = typeof pass.dispatch === "function" ? pass.dispatch({
962
+ width: canvasWidth,
963
+ height: canvasHeight,
964
+ passName: pass.name
965
+ }) : pass.dispatch;
966
+ const x = Math.max(1, Math.floor(dispatch.x));
967
+ const y = Math.max(1, Math.floor(dispatch.y ?? 1));
968
+ const z = Math.max(1, Math.floor(dispatch.z ?? 1));
969
+ computePass.dispatchWorkgroups(x, y, z);
970
+ computePass.end();
971
+ continue;
972
+ }
828
973
  let loadOp = "load";
829
974
  const isFirst = i === 0;
830
- const isLastPass = i === enabledPasses.length - 1;
975
+ const isLastPass = pass.name === lastRenderPass.name;
831
976
  if (isFirst) loadOp = "clear";
832
977
  let renderTargetView;
833
978
  let isRenderingToCanvas = false;
@@ -844,7 +989,7 @@ var WGSLRenderer = class {
844
989
  renderTargetView = this.ctx.getCurrentTexture().createView();
845
990
  isRenderingToCanvas = true;
846
991
  } else {
847
- const textureName = `pass_${i}_output`;
992
+ const textureName = this.getPassOutputTextureName(pass.name);
848
993
  let texture = this.textureManager.getTexture(textureName);
849
994
  if (!texture) texture = this.textureManager.createTexture(textureName, pass.format || this.format);
850
995
  renderTargetView = texture.createView();
@@ -926,6 +1071,55 @@ var WGSLRenderer = class {
926
1071
  }
927
1072
  return pixelData;
928
1073
  }
1074
+ /**
1075
+ * Create a storage-capable texture (useful for compute passes)
1076
+ */
1077
+ createStorageTexture(options) {
1078
+ const width = options?.width ?? (this.canvas.width || this.canvas.clientWidth);
1079
+ const height = options?.height ?? (this.canvas.height || this.canvas.clientHeight);
1080
+ const format = options?.format ?? "rgba8unorm";
1081
+ const usage = options?.usage ?? GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC;
1082
+ return this.device.createTexture({
1083
+ label: options?.label,
1084
+ size: [
1085
+ width,
1086
+ height,
1087
+ 1
1088
+ ],
1089
+ format,
1090
+ usage
1091
+ });
1092
+ }
1093
+ /**
1094
+ * Create ping-pong textures and a swap helper for iterative compute simulation
1095
+ */
1096
+ createPingPongTextures(options) {
1097
+ const textureA = this.createStorageTexture({
1098
+ ...options,
1099
+ label: options?.label ? `${options.label}_A` : "pingpong_A"
1100
+ });
1101
+ const textureB = this.createStorageTexture({
1102
+ ...options,
1103
+ label: options?.label ? `${options.label}_B` : "pingpong_B"
1104
+ });
1105
+ let read = textureA;
1106
+ let write = textureB;
1107
+ return {
1108
+ get read() {
1109
+ return read;
1110
+ },
1111
+ get write() {
1112
+ return write;
1113
+ },
1114
+ getReadView: () => read.createView(),
1115
+ getWriteView: () => write.createView(),
1116
+ swap: () => {
1117
+ const nextRead = write;
1118
+ write = read;
1119
+ read = nextRead;
1120
+ }
1121
+ };
1122
+ }
929
1123
  };
930
1124
  async function createWGSLRenderer(cvs, options) {
931
1125
  const renderer = new WGSLRenderer(cvs, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wgsl-renderer",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A multi-pass renderer based on WebGPU and WGSL.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -34,13 +34,13 @@
34
34
  "dist"
35
35
  ],
36
36
  "devDependencies": {
37
- "@taiyuuki/eslint-config": "^1.6.6",
37
+ "@taiyuuki/eslint-config": "^1.6.9",
38
38
  "@types/node": "^25.5.2",
39
39
  "@webgpu/types": "^0.1.69",
40
40
  "eslint": "^10.2.0",
41
41
  "rimraf": "^6.1.3",
42
- "rolldown": "1.0.0-rc.17",
43
- "rolldown-plugin-dts": "^0.23.2",
42
+ "rolldown": "1.0.0-rc.18",
43
+ "rolldown-plugin-dts": "^0.24.1",
44
44
  "typescript": "^6.0.2"
45
45
  },
46
46
  "scripts": {