wgsl-renderer 0.0.1 → 0.0.2

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
@@ -8,7 +8,12 @@ var RenderPass = class {
8
8
  clearColor;
9
9
  blendMode;
10
10
  hasOutputTexture = false;
11
+ passResources = [];
12
+ device;
13
+ originalBindGroupEntries;
11
14
  constructor(descriptor, device, format, layout) {
15
+ this.device = device;
16
+ this.originalBindGroupEntries = [...descriptor.bindGroupEntries || []];
12
17
  this.name = descriptor.name;
13
18
  this.clearColor = descriptor.clearColor || {
14
19
  r: 0,
@@ -17,8 +22,8 @@ var RenderPass = class {
17
22
  a: 1
18
23
  };
19
24
  this.blendMode = descriptor.blendMode || "alpha";
20
- const module$1 = device.createShaderModule({ code: descriptor.shaderCode });
21
- this.vertexBuffer = device.createBuffer({
25
+ const module$1 = this.device.createShaderModule({ code: descriptor.shaderCode });
26
+ this.vertexBuffer = this.device.createBuffer({
22
27
  size: 36,
23
28
  usage: GPUBufferUsage.VERTEX,
24
29
  mappedAtCreation: true
@@ -35,7 +40,7 @@ var RenderPass = class {
35
40
  0
36
41
  ]);
37
42
  this.vertexBuffer.unmap();
38
- this.pipeline = device.createRenderPipeline({
43
+ this.pipeline = this.device.createRenderPipeline({
39
44
  layout,
40
45
  vertex: {
41
46
  module: module$1,
@@ -60,9 +65,20 @@ var RenderPass = class {
60
65
  primitive: { topology: "triangle-list" }
61
66
  });
62
67
  const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
63
- this.bindGroup = device.createBindGroup({
68
+ this.bindGroup = this.device.createBindGroup({
69
+ layout: bindGroupLayout,
70
+ entries: this.originalBindGroupEntries
71
+ });
72
+ }
73
+ /**
74
+ * Update bind group with new entries (e.g., after texture resize)
75
+ */
76
+ updateBindGroup(newEntries) {
77
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
78
+ this.originalBindGroupEntries = [...newEntries];
79
+ this.bindGroup = this.device.createBindGroup({
64
80
  layout: bindGroupLayout,
65
- entries: descriptor.bindGroupEntries || []
81
+ entries: newEntries
66
82
  });
67
83
  }
68
84
  getBlendState() {
@@ -116,13 +132,17 @@ var TextureManager = class {
116
132
  device;
117
133
  width;
118
134
  height;
135
+ oldTextures = [];
119
136
  constructor(device, width, height) {
120
137
  this.device = device;
121
138
  this.width = width;
122
139
  this.height = height;
123
140
  }
124
141
  createTexture(name, format) {
125
- if (this.textures.has(name)) this.textures.get(name).destroy();
142
+ if (this.textures.has(name)) {
143
+ const oldTexture = this.textures.get(name);
144
+ this.oldTextures.push(oldTexture);
145
+ }
126
146
  const texture = this.device.createTexture({
127
147
  size: [this.width, this.height],
128
148
  format: format || "bgra8unorm",
@@ -136,14 +156,40 @@ var TextureManager = class {
136
156
  }
137
157
  resize(width, height) {
138
158
  if (width === this.width && height === this.height) return;
139
- this.textures.forEach((texture) => texture.destroy());
140
- this.textures.clear();
141
159
  this.width = width;
142
160
  this.height = height;
161
+ this.textures.forEach((texture) => {
162
+ this.oldTextures.push(texture);
163
+ });
164
+ this.textures.clear();
165
+ }
166
+ /**
167
+ * Recreate a specific texture with current dimensions
168
+ */
169
+ recreateTexture(name, format) {
170
+ const texture = this.device.createTexture({
171
+ size: [this.width, this.height],
172
+ format: format || "bgra8unorm",
173
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
174
+ });
175
+ this.textures.set(name, texture);
176
+ return texture;
177
+ }
178
+ /**
179
+ * Clean up old textures that are no longer needed
180
+ * Call this after ensuring GPU work is complete
181
+ */
182
+ cleanupOldTextures() {
183
+ this.oldTextures.forEach((texture) => {
184
+ texture.destroy();
185
+ });
186
+ this.oldTextures.length = 0;
143
187
  }
144
188
  destroy() {
145
189
  this.textures.forEach((texture) => texture.destroy());
146
190
  this.textures.clear();
191
+ this.oldTextures.forEach((texture) => texture.destroy());
192
+ this.oldTextures.length = 0;
147
193
  }
148
194
  getPixelSize() {
149
195
  return {
@@ -161,89 +207,93 @@ var WGSLRenderer = class {
161
207
  format;
162
208
  passes = [];
163
209
  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
210
  animationFrameId = null;
211
+ isResizing = false;
173
212
  constructor(canvas, options) {
174
213
  this.canvas = canvas;
214
+ this.options = options;
175
215
  if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
176
216
  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
217
  }
201
218
  async init() {
202
219
  this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
203
220
  this.format = navigator.gpu.getPreferredCanvasFormat();
204
- this.ctx.configure({
221
+ const config = Object.assign({
205
222
  device: this.device,
206
223
  format: this.format,
207
224
  alphaMode: "opaque"
208
- });
225
+ }, this.options?.config);
226
+ this.ctx.configure(config);
209
227
  const canvasWidth = this.canvas.width || this.canvas.clientWidth;
210
228
  const canvasHeight = this.canvas.height || this.canvas.clientHeight;
211
229
  this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
212
- this.ensureBackgroundPass();
230
+ }
231
+ async resize(width, height) {
232
+ if (this.isResizing) return;
233
+ const currentSize = this.textureManager.getPixelSize();
234
+ if (currentSize.width === width && currentSize.height === height) return;
235
+ this.isResizing = true;
236
+ this.canvas.width = width;
237
+ this.canvas.height = height;
238
+ const future = this.device.queue.onSubmittedWorkDone();
239
+ future.catch(() => {
240
+ this.isResizing = false;
241
+ });
242
+ await future;
243
+ this.textureManager.cleanupOldTextures();
244
+ this.textureManager.resize(width, height);
245
+ this.updateAllBindGroups();
246
+ this.isResizing = false;
247
+ }
248
+ getContext() {
249
+ return this.ctx;
250
+ }
251
+ getDevice() {
252
+ return this.device;
213
253
  }
214
254
  /**
215
- * Ensure background pass is added
255
+ * Update bind groups for all passes after texture resize
216
256
  */
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
- }
257
+ updateAllBindGroups() {
258
+ this.recreatePassTextures();
259
+ this.passes.forEach((pass, index) => {
260
+ const finalBindGroupEntries = [];
261
+ if (index >= 1) {
262
+ const previousOutputTextureName = `pass_${index - 1}_output`;
263
+ let previousOutput = this.textureManager.getTexture(previousOutputTextureName);
264
+ if (!previousOutput) previousOutput = this.textureManager.recreateTexture(previousOutputTextureName, this.format);
265
+ finalBindGroupEntries.push({
266
+ binding: 0,
267
+ resource: previousOutput.createView()
268
+ });
269
+ }
270
+ if (pass.passResources) pass.passResources.forEach((resource, resourceIndex) => {
271
+ finalBindGroupEntries.push({
272
+ binding: resourceIndex + 1,
273
+ resource
274
+ });
275
+ });
276
+ pass.updateBindGroup(finalBindGroupEntries);
277
+ });
278
+ }
279
+ /**
280
+ * Recreate all pass output textures after resize
281
+ */
282
+ recreatePassTextures() {
283
+ this.passes.forEach((pass, index) => {
284
+ if (index < this.passes.length - 1 || pass.hasOutputTexture) {
285
+ const textureName = `pass_${index}_output`;
286
+ this.textureManager.recreateTexture(textureName, this.format);
287
+ }
288
+ });
240
289
  }
241
290
  /**
242
291
  * Add a render pass to the multi-pass pipeline
243
292
  */
244
293
  addPass(descriptor) {
245
294
  const finalBindGroupEntries = [];
246
- if (this.passes.length > 0) {
295
+ const firstPass = this.passes.length === 0;
296
+ if (!firstPass) {
247
297
  const previousOutput = this.getPassOutput(this.passes.length - 1);
248
298
  if (previousOutput) finalBindGroupEntries.push({
249
299
  binding: 0,
@@ -252,7 +302,7 @@ var WGSLRenderer = class {
252
302
  }
253
303
  descriptor.resources.forEach((resource, index) => {
254
304
  finalBindGroupEntries.push({
255
- binding: index + 1,
305
+ binding: index + (firstPass ? 0 : 1),
256
306
  resource
257
307
  });
258
308
  });
@@ -263,6 +313,7 @@ var WGSLRenderer = class {
263
313
  blendMode: descriptor.blendMode,
264
314
  bindGroupEntries: finalBindGroupEntries
265
315
  }, this.device, this.format, "auto");
316
+ pass.passResources = [...descriptor.resources];
266
317
  this.passes.push(pass);
267
318
  const currentPassIndex = this.passes.length - 1;
268
319
  const textureName = `pass_${currentPassIndex}_output`;
@@ -270,32 +321,11 @@ var WGSLRenderer = class {
270
321
  this.passes[currentPassIndex].hasOutputTexture = true;
271
322
  }
272
323
  /**
273
- * Set background color
274
- */
275
- setBackgroundColor(r, g, b, a = 1) {
276
- this.backgroundColor = {
277
- r,
278
- g,
279
- b,
280
- a
281
- };
282
- }
283
- /**
284
- * Force create output texture for a specific pass
285
- * This is useful when you need the output texture immediately after adding a pass
286
- */
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);
292
- }
293
- /**
294
324
  * Get the output texture of a specific pass
295
325
  */
296
326
  getPassOutput(passIndex) {
297
327
  if (passIndex < 0 || passIndex >= this.passes.length) return;
298
- if (passIndex !== 0 && passIndex === this.passes.length - 1) {
328
+ if (passIndex === this.passes.length - 1) {
299
329
  if (!this.passes[passIndex]?.hasOutputTexture) return;
300
330
  }
301
331
  const textureName = `pass_${passIndex}_output`;
@@ -312,20 +342,13 @@ var WGSLRenderer = class {
312
342
  size: values.byteLength,
313
343
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
314
344
  });
315
- const uniformID = Symbol();
316
- const uniforms = {
317
- id: uniformID,
345
+ return {
318
346
  values,
319
347
  apply: () => {
320
348
  this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
321
349
  },
322
350
  getBuffer: () => buffer
323
351
  };
324
- this.uniforms.set(uniformID, uniforms);
325
- return uniforms;
326
- }
327
- getUniformsByID(id) {
328
- return this.uniforms.get(id);
329
352
  }
330
353
  /**
331
354
  * Create a sampler
@@ -338,25 +361,7 @@ var WGSLRenderer = class {
338
361
  addressModeV: "clamp-to-edge"
339
362
  }, options));
340
363
  }
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) {
364
+ async loadImageTexture(url) {
360
365
  const resp = fetch(url);
361
366
  resp.catch((err) => {
362
367
  console.error("Failed to load texture:", err);
@@ -382,9 +387,6 @@ var WGSLRenderer = class {
382
387
  renderFrame() {
383
388
  if (this.passes.length === 0) return;
384
389
  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
390
  for (let i = 0; i < this.passes.length; i++) {
389
391
  const pass = this.passes[i];
390
392
  let renderTarget;
@@ -422,11 +424,6 @@ var WGSLRenderer = class {
422
424
  this.animationFrameId = null;
423
425
  }
424
426
  }
425
- resize(width, height) {
426
- this.canvas.width = width;
427
- this.canvas.height = height;
428
- this.textureManager.resize(width, height);
429
- }
430
427
  };
431
428
  async function createWGSLRenderer(cvs, options) {
432
429
  const renderer = new WGSLRenderer(cvs, options);