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 +93 -2
- package/README.zh-CN.md +93 -2
- package/dist/cjs/index.js +205 -11
- package/dist/esm/index.d.ts +102 -29
- package/dist/esm/index.js +205 -11
- package/package.json +4 -4
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
550
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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);
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
549
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
43
|
-
"rolldown-plugin-dts": "^0.
|
|
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": {
|