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/esm/index.js CHANGED
@@ -7,7 +7,12 @@ var RenderPass = class {
7
7
  clearColor;
8
8
  blendMode;
9
9
  hasOutputTexture = false;
10
+ passResources = [];
11
+ device;
12
+ originalBindGroupEntries;
10
13
  constructor(descriptor, device, format, layout) {
14
+ this.device = device;
15
+ this.originalBindGroupEntries = [...descriptor.bindGroupEntries || []];
11
16
  this.name = descriptor.name;
12
17
  this.clearColor = descriptor.clearColor || {
13
18
  r: 0,
@@ -16,8 +21,8 @@ var RenderPass = class {
16
21
  a: 1
17
22
  };
18
23
  this.blendMode = descriptor.blendMode || "alpha";
19
- const module = device.createShaderModule({ code: descriptor.shaderCode });
20
- this.vertexBuffer = device.createBuffer({
24
+ const module = this.device.createShaderModule({ code: descriptor.shaderCode });
25
+ this.vertexBuffer = this.device.createBuffer({
21
26
  size: 36,
22
27
  usage: GPUBufferUsage.VERTEX,
23
28
  mappedAtCreation: true
@@ -34,7 +39,7 @@ var RenderPass = class {
34
39
  0
35
40
  ]);
36
41
  this.vertexBuffer.unmap();
37
- this.pipeline = device.createRenderPipeline({
42
+ this.pipeline = this.device.createRenderPipeline({
38
43
  layout,
39
44
  vertex: {
40
45
  module,
@@ -59,9 +64,20 @@ var RenderPass = class {
59
64
  primitive: { topology: "triangle-list" }
60
65
  });
61
66
  const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
62
- this.bindGroup = device.createBindGroup({
67
+ this.bindGroup = this.device.createBindGroup({
68
+ layout: bindGroupLayout,
69
+ entries: this.originalBindGroupEntries
70
+ });
71
+ }
72
+ /**
73
+ * Update bind group with new entries (e.g., after texture resize)
74
+ */
75
+ updateBindGroup(newEntries) {
76
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
77
+ this.originalBindGroupEntries = [...newEntries];
78
+ this.bindGroup = this.device.createBindGroup({
63
79
  layout: bindGroupLayout,
64
- entries: descriptor.bindGroupEntries || []
80
+ entries: newEntries
65
81
  });
66
82
  }
67
83
  getBlendState() {
@@ -115,13 +131,17 @@ var TextureManager = class {
115
131
  device;
116
132
  width;
117
133
  height;
134
+ oldTextures = [];
118
135
  constructor(device, width, height) {
119
136
  this.device = device;
120
137
  this.width = width;
121
138
  this.height = height;
122
139
  }
123
140
  createTexture(name, format) {
124
- if (this.textures.has(name)) this.textures.get(name).destroy();
141
+ if (this.textures.has(name)) {
142
+ const oldTexture = this.textures.get(name);
143
+ this.oldTextures.push(oldTexture);
144
+ }
125
145
  const texture = this.device.createTexture({
126
146
  size: [this.width, this.height],
127
147
  format: format || "bgra8unorm",
@@ -135,14 +155,40 @@ var TextureManager = class {
135
155
  }
136
156
  resize(width, height) {
137
157
  if (width === this.width && height === this.height) return;
138
- this.textures.forEach((texture) => texture.destroy());
139
- this.textures.clear();
140
158
  this.width = width;
141
159
  this.height = height;
160
+ this.textures.forEach((texture) => {
161
+ this.oldTextures.push(texture);
162
+ });
163
+ this.textures.clear();
164
+ }
165
+ /**
166
+ * Recreate a specific texture with current dimensions
167
+ */
168
+ recreateTexture(name, format) {
169
+ const texture = this.device.createTexture({
170
+ size: [this.width, this.height],
171
+ format: format || "bgra8unorm",
172
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
173
+ });
174
+ this.textures.set(name, texture);
175
+ return texture;
176
+ }
177
+ /**
178
+ * Clean up old textures that are no longer needed
179
+ * Call this after ensuring GPU work is complete
180
+ */
181
+ cleanupOldTextures() {
182
+ this.oldTextures.forEach((texture) => {
183
+ texture.destroy();
184
+ });
185
+ this.oldTextures.length = 0;
142
186
  }
143
187
  destroy() {
144
188
  this.textures.forEach((texture) => texture.destroy());
145
189
  this.textures.clear();
190
+ this.oldTextures.forEach((texture) => texture.destroy());
191
+ this.oldTextures.length = 0;
146
192
  }
147
193
  getPixelSize() {
148
194
  return {
@@ -160,89 +206,93 @@ var WGSLRenderer = class {
160
206
  format;
161
207
  passes = [];
162
208
  textureManager;
163
- backgroundPassAdded = false;
164
- backgroundColor = {
165
- r: .1,
166
- g: .1,
167
- b: .1,
168
- a: 1
169
- };
170
- uniforms = /* @__PURE__ */ new Map();
171
209
  animationFrameId = null;
210
+ isResizing = false;
172
211
  constructor(canvas, options) {
173
212
  this.canvas = canvas;
213
+ this.options = options;
174
214
  if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
175
215
  this.ctx = canvas.getContext("webgpu");
176
- switch (typeof options?.backgroundColor) {
177
- case "number":
178
- const hex = options.backgroundColor;
179
- this.backgroundColor = {
180
- r: (hex >> 16 & 255) / 255,
181
- g: (hex >> 8 & 255) / 255,
182
- b: (hex & 255) / 255,
183
- a: 1
184
- };
185
- break;
186
- case "string":
187
- const m = options.backgroundColor.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i);
188
- if (m) this.backgroundColor = {
189
- r: Number.parseInt(m[1], 16) / 255,
190
- g: Number.parseInt(m[2], 16) / 255,
191
- b: Number.parseInt(m[3], 16) / 255,
192
- a: 1
193
- };
194
- break;
195
- case "object":
196
- Object.assign(this.backgroundColor, options.backgroundColor);
197
- break;
198
- }
199
216
  }
200
217
  async init() {
201
218
  this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
202
219
  this.format = navigator.gpu.getPreferredCanvasFormat();
203
- this.ctx.configure({
220
+ const config = Object.assign({
204
221
  device: this.device,
205
222
  format: this.format,
206
223
  alphaMode: "opaque"
207
- });
224
+ }, this.options?.config);
225
+ this.ctx.configure(config);
208
226
  const canvasWidth = this.canvas.width || this.canvas.clientWidth;
209
227
  const canvasHeight = this.canvas.height || this.canvas.clientHeight;
210
228
  this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
211
- this.ensureBackgroundPass();
229
+ }
230
+ async resize(width, height) {
231
+ if (this.isResizing) return;
232
+ const currentSize = this.textureManager.getPixelSize();
233
+ if (currentSize.width === width && currentSize.height === height) return;
234
+ this.isResizing = true;
235
+ this.canvas.width = width;
236
+ this.canvas.height = height;
237
+ const future = this.device.queue.onSubmittedWorkDone();
238
+ future.catch(() => {
239
+ this.isResizing = false;
240
+ });
241
+ await future;
242
+ this.textureManager.cleanupOldTextures();
243
+ this.textureManager.resize(width, height);
244
+ this.updateAllBindGroups();
245
+ this.isResizing = false;
246
+ }
247
+ getContext() {
248
+ return this.ctx;
249
+ }
250
+ getDevice() {
251
+ return this.device;
212
252
  }
213
253
  /**
214
- * Ensure background pass is added
254
+ * Update bind groups for all passes after texture resize
215
255
  */
216
- ensureBackgroundPass() {
217
- if (!this.backgroundPassAdded) {
218
- const backgroundPass = new RenderPass({
219
- name: "builtin_background",
220
- shaderCode: `
221
- @vertex
222
- fn vs_main(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
223
- return vec4<f32>(p, 1.0);
224
- }
225
-
226
- @fragment
227
- fn fs_main() -> @location(0) vec4<f32> {
228
- return vec4<f32>(${this.backgroundColor.r}, ${this.backgroundColor.g}, ${this.backgroundColor.b}, ${this.backgroundColor.a});
229
- }
230
- `,
231
- blendMode: "none",
232
- clearColor: this.backgroundColor,
233
- bindGroupEntries: []
234
- }, this.device, this.format, "auto");
235
- this.passes.unshift(backgroundPass);
236
- this.backgroundPassAdded = true;
237
- this.textureManager.createTexture("pass_0_output", this.format);
238
- }
256
+ updateAllBindGroups() {
257
+ this.recreatePassTextures();
258
+ this.passes.forEach((pass, index) => {
259
+ const finalBindGroupEntries = [];
260
+ if (index >= 1) {
261
+ const previousOutputTextureName = `pass_${index - 1}_output`;
262
+ let previousOutput = this.textureManager.getTexture(previousOutputTextureName);
263
+ if (!previousOutput) previousOutput = this.textureManager.recreateTexture(previousOutputTextureName, this.format);
264
+ finalBindGroupEntries.push({
265
+ binding: 0,
266
+ resource: previousOutput.createView()
267
+ });
268
+ }
269
+ if (pass.passResources) pass.passResources.forEach((resource, resourceIndex) => {
270
+ finalBindGroupEntries.push({
271
+ binding: resourceIndex + 1,
272
+ resource
273
+ });
274
+ });
275
+ pass.updateBindGroup(finalBindGroupEntries);
276
+ });
277
+ }
278
+ /**
279
+ * Recreate all pass output textures after resize
280
+ */
281
+ recreatePassTextures() {
282
+ this.passes.forEach((pass, index) => {
283
+ if (index < this.passes.length - 1 || pass.hasOutputTexture) {
284
+ const textureName = `pass_${index}_output`;
285
+ this.textureManager.recreateTexture(textureName, this.format);
286
+ }
287
+ });
239
288
  }
240
289
  /**
241
290
  * Add a render pass to the multi-pass pipeline
242
291
  */
243
292
  addPass(descriptor) {
244
293
  const finalBindGroupEntries = [];
245
- if (this.passes.length > 0) {
294
+ const firstPass = this.passes.length === 0;
295
+ if (!firstPass) {
246
296
  const previousOutput = this.getPassOutput(this.passes.length - 1);
247
297
  if (previousOutput) finalBindGroupEntries.push({
248
298
  binding: 0,
@@ -251,7 +301,7 @@ var WGSLRenderer = class {
251
301
  }
252
302
  descriptor.resources.forEach((resource, index) => {
253
303
  finalBindGroupEntries.push({
254
- binding: index + 1,
304
+ binding: index + (firstPass ? 0 : 1),
255
305
  resource
256
306
  });
257
307
  });
@@ -262,6 +312,7 @@ var WGSLRenderer = class {
262
312
  blendMode: descriptor.blendMode,
263
313
  bindGroupEntries: finalBindGroupEntries
264
314
  }, this.device, this.format, "auto");
315
+ pass.passResources = [...descriptor.resources];
265
316
  this.passes.push(pass);
266
317
  const currentPassIndex = this.passes.length - 1;
267
318
  const textureName = `pass_${currentPassIndex}_output`;
@@ -269,32 +320,11 @@ var WGSLRenderer = class {
269
320
  this.passes[currentPassIndex].hasOutputTexture = true;
270
321
  }
271
322
  /**
272
- * Set background color
273
- */
274
- setBackgroundColor(r, g, b, a = 1) {
275
- this.backgroundColor = {
276
- r,
277
- g,
278
- b,
279
- a
280
- };
281
- }
282
- /**
283
- * Force create output texture for a specific pass
284
- * This is useful when you need the output texture immediately after adding a pass
285
- */
286
- createPassOutput(passIndex) {
287
- if (passIndex < 0 || passIndex >= this.passes.length) return;
288
- if (passIndex === this.passes.length - 1) return;
289
- const textureName = `pass_${passIndex}_output`;
290
- return this.textureManager.createTexture(textureName, this.format);
291
- }
292
- /**
293
323
  * Get the output texture of a specific pass
294
324
  */
295
325
  getPassOutput(passIndex) {
296
326
  if (passIndex < 0 || passIndex >= this.passes.length) return;
297
- if (passIndex !== 0 && passIndex === this.passes.length - 1) {
327
+ if (passIndex === this.passes.length - 1) {
298
328
  if (!this.passes[passIndex]?.hasOutputTexture) return;
299
329
  }
300
330
  const textureName = `pass_${passIndex}_output`;
@@ -311,20 +341,13 @@ var WGSLRenderer = class {
311
341
  size: values.byteLength,
312
342
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
313
343
  });
314
- const uniformID = Symbol();
315
- const uniforms = {
316
- id: uniformID,
344
+ return {
317
345
  values,
318
346
  apply: () => {
319
347
  this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
320
348
  },
321
349
  getBuffer: () => buffer
322
350
  };
323
- this.uniforms.set(uniformID, uniforms);
324
- return uniforms;
325
- }
326
- getUniformsByID(id) {
327
- return this.uniforms.get(id);
328
351
  }
329
352
  /**
330
353
  * Create a sampler
@@ -337,25 +360,7 @@ var WGSLRenderer = class {
337
360
  addressModeV: "clamp-to-edge"
338
361
  }, options));
339
362
  }
340
- /**
341
- * Create a bind group entry for texture
342
- */
343
- createTextureBinding(texture) {
344
- return texture.createView();
345
- }
346
- /**
347
- * Configure multi-pass rendering
348
- */
349
- setupMultiPass(descriptor) {
350
- this.passes = [];
351
- this.textureManager.destroy();
352
- const canvasWidth = this.canvas.width || this.canvas.clientWidth;
353
- const canvasHeight = this.canvas.height || this.canvas.clientHeight;
354
- this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
355
- descriptor.passes.forEach((passDesc) => this.addPass(passDesc));
356
- if (descriptor.output?.texture && !descriptor.output.writeToCanvas) this.textureManager.createTexture("final_output");
357
- }
358
- async loadTexture(url) {
363
+ async loadImageTexture(url) {
359
364
  const resp = fetch(url);
360
365
  resp.catch((err) => {
361
366
  console.error("Failed to load texture:", err);
@@ -381,9 +386,6 @@ var WGSLRenderer = class {
381
386
  renderFrame() {
382
387
  if (this.passes.length === 0) return;
383
388
  const commandEncoder = this.device.createCommandEncoder();
384
- const canvasWidth = this.canvas.width || this.canvas.clientWidth;
385
- const canvasHeight = this.canvas.height || this.canvas.clientHeight;
386
- this.textureManager.resize(canvasWidth, canvasHeight);
387
389
  for (let i = 0; i < this.passes.length; i++) {
388
390
  const pass = this.passes[i];
389
391
  let renderTarget;
@@ -421,11 +423,6 @@ var WGSLRenderer = class {
421
423
  this.animationFrameId = null;
422
424
  }
423
425
  }
424
- resize(width, height) {
425
- this.canvas.width = width;
426
- this.canvas.height = height;
427
- this.textureManager.resize(width, height);
428
- }
429
426
  };
430
427
  async function createWGSLRenderer(cvs, options) {
431
428
  const renderer = new WGSLRenderer(cvs, options);
@@ -11,38 +11,10 @@ interface RenderPassDescriptor {
11
11
  blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
12
12
  resources: GPUBindingResource[];
13
13
  }
14
- interface InternalRenderPassDescriptor {
15
- name: string;
16
- shaderCode: string;
17
- clearColor?: {
18
- r: number;
19
- g: number;
20
- b: number;
21
- a: number;
22
- };
23
- blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
24
- bindGroupEntries: GPUBindGroupEntry[];
25
- }
26
14
  interface RenderPassOutput {
27
15
  texture?: GPUTexture;
28
16
  writeToCanvas?: boolean;
29
17
  }
30
- declare class RenderPass {
31
- name: string;
32
- pipeline: GPURenderPipeline;
33
- bindGroup: GPUBindGroup;
34
- vertexBuffer: GPUBuffer;
35
- clearColor: {
36
- r: number;
37
- g: number;
38
- b: number;
39
- a: number;
40
- };
41
- blendMode: 'additive' | 'alpha' | 'multiply' | 'none';
42
- hasOutputTexture: boolean;
43
- constructor(descriptor: InternalRenderPassDescriptor, device: GPUDevice, format: GPUTextureFormat, layout: GPUPipelineLayout | 'auto');
44
- private getBlendState;
45
- }
46
18
  //#endregion
47
19
  //#region src/index.d.ts
48
20
  interface MultiPassDescriptor {
@@ -50,80 +22,54 @@ interface MultiPassDescriptor {
50
22
  output?: RenderPassOutput;
51
23
  }
52
24
  interface WGSLRendererOptions {
53
- backgroundColor?: number | string | {
54
- r: number;
55
- g: number;
56
- b: number;
57
- };
25
+ config?: Omit<GPUCanvasConfiguration, 'device' | 'format'>;
58
26
  }
59
27
  declare class WGSLRenderer {
60
28
  canvas: HTMLCanvasElement;
61
- ctx: GPUCanvasContext;
62
- device: GPUDevice;
63
- format: GPUTextureFormat;
64
- passes: RenderPass[];
29
+ options?: WGSLRendererOptions | undefined;
30
+ private ctx;
31
+ private device;
32
+ private format;
33
+ private passes;
65
34
  private textureManager;
66
- private backgroundPassAdded;
67
- private backgroundColor;
68
- private uniforms;
69
35
  private animationFrameId;
70
- constructor(canvas: HTMLCanvasElement, options?: WGSLRendererOptions);
36
+ private isResizing;
37
+ constructor(canvas: HTMLCanvasElement, options?: WGSLRendererOptions | undefined);
71
38
  init(): Promise<void>;
39
+ resize(width: number, height: number): Promise<void>;
40
+ getContext(): GPUCanvasContext;
41
+ getDevice(): GPUDevice;
72
42
  /**
73
- * Ensure background pass is added
43
+ * Update bind groups for all passes after texture resize
74
44
  */
75
- private ensureBackgroundPass;
45
+ private updateAllBindGroups;
76
46
  /**
77
- * Add a render pass to the multi-pass pipeline
47
+ * Recreate all pass output textures after resize
78
48
  */
79
- addPass(descriptor: RenderPassDescriptor): void;
49
+ private recreatePassTextures;
80
50
  /**
81
- * Set background color
82
- */
83
- setBackgroundColor(r: number, g: number, b: number, a?: number): void;
84
- /**
85
- * Force create output texture for a specific pass
86
- * This is useful when you need the output texture immediately after adding a pass
51
+ * Add a render pass to the multi-pass pipeline
87
52
  */
88
- createPassOutput(passIndex: number): GPUTexture | undefined;
53
+ addPass(descriptor: RenderPassDescriptor): void;
89
54
  /**
90
55
  * Get the output texture of a specific pass
91
56
  */
92
- getPassOutput(passIndex: number): GPUTexture | undefined;
57
+ private getPassOutput;
93
58
  /**
94
59
  * Create a uniforms
95
60
  * @param length The length of the uniform buffer in number of floats
96
61
  * @return The uniform object containing the buffer and data array
97
62
  */
98
63
  createUniforms(length: number): {
99
- id: symbol;
100
64
  values: Float32Array<ArrayBuffer>;
101
65
  apply: () => void;
102
66
  getBuffer: () => GPUBuffer;
103
67
  };
104
- getUniformsByID(id: symbol): {
105
- id: symbol;
106
- values: Float32Array;
107
- apply: {
108
- (): void;
109
- };
110
- getBuffer: {
111
- (): GPUBuffer;
112
- };
113
- } | undefined;
114
68
  /**
115
69
  * Create a sampler
116
70
  */
117
71
  createSampler(options?: GPUSamplerDescriptor): GPUSampler;
118
- /**
119
- * Create a bind group entry for texture
120
- */
121
- createTextureBinding(texture: GPUTexture): GPUTextureView;
122
- /**
123
- * Configure multi-pass rendering
124
- */
125
- setupMultiPass(descriptor: MultiPassDescriptor): void;
126
- loadTexture(url: string): Promise<{
72
+ loadImageTexture(url: string): Promise<{
127
73
  texture: GPUTexture;
128
74
  width: number;
129
75
  height: number;
@@ -133,7 +79,6 @@ declare class WGSLRenderer {
133
79
  (): void;
134
80
  }): void;
135
81
  stopLoop(): void;
136
- resize(width: number, height: number): void;
137
82
  }
138
83
  declare function createWGSLRenderer(cvs: HTMLCanvasElement, options?: WGSLRendererOptions): Promise<WGSLRenderer>;
139
84
  //#endregion