wgsl-renderer 0.0.1 → 0.0.3

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/dist/cjs/index.js CHANGED
@@ -7,8 +7,12 @@ var RenderPass = class {
7
7
  vertexBuffer;
8
8
  clearColor;
9
9
  blendMode;
10
- hasOutputTexture = false;
10
+ view;
11
+ format;
12
+ passResources = [];
13
+ device;
11
14
  constructor(descriptor, device, format, layout) {
15
+ this.device = device;
12
16
  this.name = descriptor.name;
13
17
  this.clearColor = descriptor.clearColor || {
14
18
  r: 0,
@@ -16,9 +20,12 @@ var RenderPass = class {
16
20
  b: 0,
17
21
  a: 1
18
22
  };
19
- this.blendMode = descriptor.blendMode || "alpha";
20
- const module$1 = device.createShaderModule({ code: descriptor.shaderCode });
21
- this.vertexBuffer = device.createBuffer({
23
+ this.blendMode = descriptor.blendMode || "none";
24
+ this.view = descriptor.view;
25
+ this.format = descriptor.format;
26
+ const actualFormat = descriptor.format || format;
27
+ const module$1 = this.device.createShaderModule({ code: descriptor.shaderCode });
28
+ this.vertexBuffer = this.device.createBuffer({
22
29
  size: 36,
23
30
  usage: GPUBufferUsage.VERTEX,
24
31
  mappedAtCreation: true
@@ -35,11 +42,13 @@ var RenderPass = class {
35
42
  0
36
43
  ]);
37
44
  this.vertexBuffer.unmap();
38
- this.pipeline = device.createRenderPipeline({
45
+ const vertexEntryPoint = descriptor.entryPoints?.vertex || "vs_main";
46
+ const fragmentEntryPoint = descriptor.entryPoints?.fragment || "fs_main";
47
+ this.pipeline = this.device.createRenderPipeline({
39
48
  layout,
40
49
  vertex: {
41
50
  module: module$1,
42
- entryPoint: "vs_main",
51
+ entryPoint: vertexEntryPoint,
43
52
  buffers: [{
44
53
  arrayStride: 12,
45
54
  attributes: [{
@@ -51,18 +60,24 @@ var RenderPass = class {
51
60
  },
52
61
  fragment: {
53
62
  module: module$1,
54
- entryPoint: "fs_main",
63
+ entryPoint: fragmentEntryPoint,
55
64
  targets: [{
56
- format,
65
+ format: actualFormat,
57
66
  blend: this.getBlendState()
58
67
  }]
59
68
  },
60
69
  primitive: { topology: "triangle-list" }
61
70
  });
71
+ this.bindGroup = null;
72
+ }
73
+ /**
74
+ * Update bind group with new entries (e.g., after texture resize)
75
+ */
76
+ updateBindGroup(newEntries) {
62
77
  const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
63
- this.bindGroup = device.createBindGroup({
78
+ this.bindGroup = this.device.createBindGroup({
64
79
  layout: bindGroupLayout,
65
- entries: descriptor.bindGroupEntries || []
80
+ entries: newEntries
66
81
  });
67
82
  }
68
83
  getBlendState() {
@@ -126,7 +141,7 @@ var TextureManager = class {
126
141
  const texture = this.device.createTexture({
127
142
  size: [this.width, this.height],
128
143
  format: format || "bgra8unorm",
129
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
144
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
130
145
  });
131
146
  this.textures.set(name, texture);
132
147
  return texture;
@@ -136,10 +151,12 @@ var TextureManager = class {
136
151
  }
137
152
  resize(width, height) {
138
153
  if (width === this.width && height === this.height) return;
139
- this.textures.forEach((texture) => texture.destroy());
140
- this.textures.clear();
141
154
  this.width = width;
142
155
  this.height = height;
156
+ this.textures.forEach((texture) => {
157
+ texture.destroy();
158
+ });
159
+ this.textures.clear();
143
160
  }
144
161
  destroy() {
145
162
  this.textures.forEach((texture) => texture.destroy());
@@ -153,6 +170,30 @@ var TextureManager = class {
153
170
  }
154
171
  };
155
172
 
173
+ //#endregion
174
+ //#region src/PassTextureRef.ts
175
+ const PASS_TEXTURE_REF_SYMBOL = Symbol("PassTextureRef");
176
+ var PassTextureRef = class PassTextureRef {
177
+ [PASS_TEXTURE_REF_SYMBOL] = true;
178
+ passName;
179
+ constructor(passName) {
180
+ this.passName = passName;
181
+ }
182
+ static is(obj) {
183
+ return obj && typeof obj === "object" && PASS_TEXTURE_REF_SYMBOL in obj;
184
+ }
185
+ static fromGPUBindingResource(resource) {
186
+ if (this.is(resource)) return resource;
187
+ return null;
188
+ }
189
+ static create(passName) {
190
+ return new PassTextureRef(passName);
191
+ }
192
+ };
193
+ function isPassTextureRef(obj) {
194
+ return PassTextureRef.is(obj);
195
+ }
196
+
156
197
  //#endregion
157
198
  //#region src/index.ts
158
199
  var WGSLRenderer = class {
@@ -161,145 +202,122 @@ var WGSLRenderer = class {
161
202
  format;
162
203
  passes = [];
163
204
  textureManager;
164
- backgroundPassAdded = false;
165
- backgroundColor = {
166
- r: .1,
167
- g: .1,
168
- b: .1,
169
- a: 1
170
- };
171
- uniforms = /* @__PURE__ */ new Map();
172
205
  animationFrameId = null;
206
+ isResizing = false;
173
207
  constructor(canvas, options) {
174
208
  this.canvas = canvas;
209
+ this.options = options;
175
210
  if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
176
211
  this.ctx = canvas.getContext("webgpu");
177
- switch (typeof options?.backgroundColor) {
178
- case "number":
179
- const hex = options.backgroundColor;
180
- this.backgroundColor = {
181
- r: (hex >> 16 & 255) / 255,
182
- g: (hex >> 8 & 255) / 255,
183
- b: (hex & 255) / 255,
184
- a: 1
185
- };
186
- break;
187
- case "string":
188
- const m = options.backgroundColor.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i);
189
- if (m) this.backgroundColor = {
190
- r: Number.parseInt(m[1], 16) / 255,
191
- g: Number.parseInt(m[2], 16) / 255,
192
- b: Number.parseInt(m[3], 16) / 255,
193
- a: 1
194
- };
195
- break;
196
- case "object":
197
- Object.assign(this.backgroundColor, options.backgroundColor);
198
- break;
199
- }
200
212
  }
201
213
  async init() {
202
214
  this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
203
215
  this.format = navigator.gpu.getPreferredCanvasFormat();
204
- this.ctx.configure({
216
+ const config = Object.assign({
205
217
  device: this.device,
206
218
  format: this.format,
207
219
  alphaMode: "opaque"
208
- });
220
+ }, this.options?.config);
221
+ this.ctx.configure(config);
209
222
  const canvasWidth = this.canvas.width || this.canvas.clientWidth;
210
223
  const canvasHeight = this.canvas.height || this.canvas.clientHeight;
211
224
  this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
212
- this.ensureBackgroundPass();
225
+ }
226
+ async resize(width, height) {
227
+ if (this.isResizing) return;
228
+ if (this.canvas.width === width && this.canvas.height === height) return;
229
+ this.isResizing = true;
230
+ this.canvas.width = width;
231
+ this.canvas.height = height;
232
+ const future = this.device.queue.onSubmittedWorkDone();
233
+ future.catch(() => {
234
+ console.warn("GPU work submission failed during resize.");
235
+ this.isResizing = false;
236
+ });
237
+ await future;
238
+ this.textureManager.resize(width, height);
239
+ this.isResizing = false;
240
+ }
241
+ getContext() {
242
+ return this.ctx;
243
+ }
244
+ getDevice() {
245
+ return this.device;
213
246
  }
214
247
  /**
215
- * Ensure background pass is added
248
+ * Get texture reference by pass name
249
+ * Returns a PassTextureRef that will resolve to the actual texture at render time
216
250
  */
217
- ensureBackgroundPass() {
218
- if (!this.backgroundPassAdded) {
219
- const backgroundPass = new RenderPass({
220
- name: "builtin_background",
221
- shaderCode: `
222
- @vertex
223
- fn vs_main(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
224
- return vec4<f32>(p, 1.0);
225
- }
226
-
227
- @fragment
228
- fn fs_main() -> @location(0) vec4<f32> {
229
- return vec4<f32>(${this.backgroundColor.r}, ${this.backgroundColor.g}, ${this.backgroundColor.b}, ${this.backgroundColor.a});
230
- }
231
- `,
232
- blendMode: "none",
233
- clearColor: this.backgroundColor,
234
- bindGroupEntries: []
235
- }, this.device, this.format, "auto");
236
- this.passes.unshift(backgroundPass);
237
- this.backgroundPassAdded = true;
238
- this.textureManager.createTexture("pass_0_output", this.format);
239
- }
251
+ getPassTexture(passName) {
252
+ if (!this.passes.find((pass) => pass.name === passName)) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
253
+ return PassTextureRef.create(passName);
254
+ }
255
+ /**
256
+ * Resolve a PassTextureRef to actual GPUTextureView with validation
257
+ */
258
+ resolveTextureRef(ref) {
259
+ const targetPassIndex = this.passes.findIndex((pass) => pass.name === ref.passName);
260
+ if (targetPassIndex === -1) throw new Error(`Cannot find pass named '${ref.passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
261
+ const textureName = `pass_${targetPassIndex}_output`;
262
+ let texture = this.textureManager.getTexture(textureName);
263
+ if (!texture) texture = this.textureManager.createTexture(textureName, this.format);
264
+ return texture.createView();
265
+ }
266
+ /**
267
+ * Get pass by name
268
+ */
269
+ getPassByName(passName) {
270
+ return this.passes.find((pass) => pass.name === passName);
240
271
  }
241
272
  /**
242
273
  * Add a render pass to the multi-pass pipeline
243
274
  */
244
275
  addPass(descriptor) {
245
276
  const finalBindGroupEntries = [];
246
- if (this.passes.length > 0) {
247
- const previousOutput = this.getPassOutput(this.passes.length - 1);
248
- if (previousOutput) finalBindGroupEntries.push({
249
- binding: 0,
250
- resource: previousOutput.createView()
251
- });
252
- }
253
- descriptor.resources.forEach((resource, index) => {
277
+ descriptor.resources?.forEach((resource, index) => {
254
278
  finalBindGroupEntries.push({
255
- binding: index + 1,
279
+ binding: index,
256
280
  resource
257
281
  });
258
282
  });
259
- const pass = new RenderPass({
283
+ const internalDescriptor = {
260
284
  name: descriptor.name,
261
285
  shaderCode: descriptor.shaderCode,
286
+ entryPoints: descriptor.entryPoints,
262
287
  clearColor: descriptor.clearColor,
263
288
  blendMode: descriptor.blendMode,
264
- bindGroupEntries: finalBindGroupEntries
265
- }, this.device, this.format, "auto");
266
- this.passes.push(pass);
267
- const currentPassIndex = this.passes.length - 1;
268
- const textureName = `pass_${currentPassIndex}_output`;
269
- this.textureManager.createTexture(textureName, this.format);
270
- this.passes[currentPassIndex].hasOutputTexture = true;
271
- }
272
- /**
273
- * Set background color
274
- */
275
- setBackgroundColor(r, g, b, a = 1) {
276
- this.backgroundColor = {
277
- r,
278
- g,
279
- b,
280
- a
289
+ bindGroupEntries: finalBindGroupEntries,
290
+ view: descriptor.view,
291
+ format: descriptor.format
281
292
  };
293
+ const pipelineFormat = descriptor.format || this.format;
294
+ const pass = new RenderPass(internalDescriptor, this.device, pipelineFormat, "auto");
295
+ pass.passResources = descriptor.resources ?? [];
296
+ this.passes.push(pass);
282
297
  }
283
298
  /**
284
- * Force create output texture for a specific pass
285
- * This is useful when you need the output texture immediately after adding a pass
299
+ * Resolve resource to actual GPU binding resource
300
+ * Handles PassTextureRef by getting the current texture view with validation
286
301
  */
287
- createPassOutput(passIndex) {
288
- if (passIndex < 0 || passIndex >= this.passes.length) return;
289
- if (passIndex === this.passes.length - 1) return;
290
- const textureName = `pass_${passIndex}_output`;
291
- return this.textureManager.createTexture(textureName, this.format);
302
+ resolveResource(resource) {
303
+ if (isPassTextureRef(resource)) return this.resolveTextureRef(resource);
304
+ return resource;
292
305
  }
293
306
  /**
294
- * Get the output texture of a specific pass
307
+ * Update bind groups to resolve current texture references
308
+ * Call this before rendering to ensure all PassTextureRef are resolved
295
309
  */
296
- getPassOutput(passIndex) {
297
- if (passIndex < 0 || passIndex >= this.passes.length) return;
298
- if (passIndex !== 0 && passIndex === this.passes.length - 1) {
299
- if (!this.passes[passIndex]?.hasOutputTexture) return;
300
- }
301
- const textureName = `pass_${passIndex}_output`;
302
- return this.textureManager.getTexture(textureName);
310
+ updateBindGroups() {
311
+ this.passes.forEach((pass) => {
312
+ const finalBindGroupEntries = [];
313
+ pass.passResources.forEach((resource, index) => {
314
+ finalBindGroupEntries.push({
315
+ binding: index,
316
+ resource: this.resolveResource(resource)
317
+ });
318
+ });
319
+ pass.updateBindGroup(finalBindGroupEntries);
320
+ });
303
321
  }
304
322
  /**
305
323
  * Create a uniforms
@@ -312,20 +330,13 @@ var WGSLRenderer = class {
312
330
  size: values.byteLength,
313
331
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
314
332
  });
315
- const uniformID = Symbol();
316
- const uniforms = {
317
- id: uniformID,
333
+ return {
318
334
  values,
319
335
  apply: () => {
320
336
  this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
321
337
  },
322
338
  getBuffer: () => buffer
323
339
  };
324
- this.uniforms.set(uniformID, uniforms);
325
- return uniforms;
326
- }
327
- getUniformsByID(id) {
328
- return this.uniforms.get(id);
329
340
  }
330
341
  /**
331
342
  * Create a sampler
@@ -338,25 +349,7 @@ var WGSLRenderer = class {
338
349
  addressModeV: "clamp-to-edge"
339
350
  }, options));
340
351
  }
341
- /**
342
- * Create a bind group entry for texture
343
- */
344
- createTextureBinding(texture) {
345
- return texture.createView();
346
- }
347
- /**
348
- * Configure multi-pass rendering
349
- */
350
- setupMultiPass(descriptor) {
351
- this.passes = [];
352
- this.textureManager.destroy();
353
- const canvasWidth = this.canvas.width || this.canvas.clientWidth;
354
- const canvasHeight = this.canvas.height || this.canvas.clientHeight;
355
- this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
356
- descriptor.passes.forEach((passDesc) => this.addPass(passDesc));
357
- if (descriptor.output?.texture && !descriptor.output.writeToCanvas) this.textureManager.createTexture("final_output");
358
- }
359
- async loadTexture(url) {
352
+ async loadImageTexture(url) {
360
353
  const resp = fetch(url);
361
354
  resp.catch((err) => {
362
355
  console.error("Failed to load texture:", err);
@@ -381,25 +374,25 @@ var WGSLRenderer = class {
381
374
  }
382
375
  renderFrame() {
383
376
  if (this.passes.length === 0) return;
377
+ this.updateBindGroups();
384
378
  const commandEncoder = this.device.createCommandEncoder();
385
- const canvasWidth = this.canvas.width || this.canvas.clientWidth;
386
- const canvasHeight = this.canvas.height || this.canvas.clientHeight;
387
- this.textureManager.resize(canvasWidth, canvasHeight);
388
379
  for (let i = 0; i < this.passes.length; i++) {
389
380
  const pass = this.passes[i];
381
+ let loadOp = "load";
382
+ const isLast = i === this.passes.length - 1;
383
+ if (isLast) loadOp = "clear";
390
384
  let renderTarget;
391
- let loadOp = "clear";
392
- if (i === this.passes.length - 1) renderTarget = this.ctx.getCurrentTexture().createView();
385
+ if (pass.view) renderTarget = pass.view;
386
+ else if (isLast) renderTarget = this.ctx.getCurrentTexture().createView();
393
387
  else {
394
388
  const textureName = `pass_${i}_output`;
395
- const texture = this.textureManager.getTexture(textureName);
396
- if (!texture) continue;
389
+ let texture = this.textureManager.getTexture(textureName);
390
+ if (!texture) texture = this.textureManager.createTexture(textureName, "rgba16float");
397
391
  renderTarget = texture.createView();
398
- loadOp = "load";
399
392
  }
400
393
  const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
401
394
  view: renderTarget,
402
- loadOp: i === 0 ? "clear" : loadOp,
395
+ loadOp,
403
396
  storeOp: "store",
404
397
  clearValue: pass.clearColor
405
398
  }] });
@@ -412,9 +405,11 @@ var WGSLRenderer = class {
412
405
  this.device.queue.submit([commandEncoder.finish()]);
413
406
  }
414
407
  loopRender(cb) {
415
- cb?.();
416
- this.renderFrame();
417
- this.animationFrameId = requestAnimationFrame(() => this.loopRender(cb));
408
+ this.animationFrameId = requestAnimationFrame((t) => {
409
+ cb?.(t);
410
+ this.renderFrame();
411
+ this.loopRender(cb);
412
+ });
418
413
  }
419
414
  stopLoop() {
420
415
  if (this.animationFrameId !== null) {
@@ -422,11 +417,6 @@ var WGSLRenderer = class {
422
417
  this.animationFrameId = null;
423
418
  }
424
419
  }
425
- resize(width, height) {
426
- this.canvas.width = width;
427
- this.canvas.height = height;
428
- this.textureManager.resize(width, height);
429
- }
430
420
  };
431
421
  async function createWGSLRenderer(cvs, options) {
432
422
  const renderer = new WGSLRenderer(cvs, options);