wgsl-renderer 0.0.2 → 0.0.4

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,13 +7,12 @@ var RenderPass = class {
7
7
  vertexBuffer;
8
8
  clearColor;
9
9
  blendMode;
10
- hasOutputTexture = false;
10
+ view;
11
+ format;
11
12
  passResources = [];
12
13
  device;
13
- originalBindGroupEntries;
14
14
  constructor(descriptor, device, format, layout) {
15
15
  this.device = device;
16
- this.originalBindGroupEntries = [...descriptor.bindGroupEntries || []];
17
16
  this.name = descriptor.name;
18
17
  this.clearColor = descriptor.clearColor || {
19
18
  r: 0,
@@ -21,7 +20,10 @@ var RenderPass = class {
21
20
  b: 0,
22
21
  a: 1
23
22
  };
24
- this.blendMode = descriptor.blendMode || "alpha";
23
+ this.blendMode = descriptor.blendMode || "none";
24
+ this.view = descriptor.view;
25
+ this.format = descriptor.format;
26
+ const actualFormat = descriptor.format || format;
25
27
  const module$1 = this.device.createShaderModule({ code: descriptor.shaderCode });
26
28
  this.vertexBuffer = this.device.createBuffer({
27
29
  size: 36,
@@ -40,11 +42,13 @@ var RenderPass = class {
40
42
  0
41
43
  ]);
42
44
  this.vertexBuffer.unmap();
45
+ const vertexEntryPoint = descriptor.entryPoints?.vertex || "vs_main";
46
+ const fragmentEntryPoint = descriptor.entryPoints?.fragment || "fs_main";
43
47
  this.pipeline = this.device.createRenderPipeline({
44
48
  layout,
45
49
  vertex: {
46
50
  module: module$1,
47
- entryPoint: "vs_main",
51
+ entryPoint: vertexEntryPoint,
48
52
  buffers: [{
49
53
  arrayStride: 12,
50
54
  attributes: [{
@@ -56,26 +60,21 @@ var RenderPass = class {
56
60
  },
57
61
  fragment: {
58
62
  module: module$1,
59
- entryPoint: "fs_main",
63
+ entryPoint: fragmentEntryPoint,
60
64
  targets: [{
61
- format,
65
+ format: actualFormat,
62
66
  blend: this.getBlendState()
63
67
  }]
64
68
  },
65
69
  primitive: { topology: "triangle-list" }
66
70
  });
67
- const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
68
- this.bindGroup = this.device.createBindGroup({
69
- layout: bindGroupLayout,
70
- entries: this.originalBindGroupEntries
71
- });
71
+ this.bindGroup = null;
72
72
  }
73
73
  /**
74
74
  * Update bind group with new entries (e.g., after texture resize)
75
75
  */
76
76
  updateBindGroup(newEntries) {
77
77
  const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
78
- this.originalBindGroupEntries = [...newEntries];
79
78
  this.bindGroup = this.device.createBindGroup({
80
79
  layout: bindGroupLayout,
81
80
  entries: newEntries
@@ -132,21 +131,17 @@ var TextureManager = class {
132
131
  device;
133
132
  width;
134
133
  height;
135
- oldTextures = [];
136
134
  constructor(device, width, height) {
137
135
  this.device = device;
138
136
  this.width = width;
139
137
  this.height = height;
140
138
  }
141
139
  createTexture(name, format) {
142
- if (this.textures.has(name)) {
143
- const oldTexture = this.textures.get(name);
144
- this.oldTextures.push(oldTexture);
145
- }
140
+ if (this.textures.has(name)) this.textures.get(name).destroy();
146
141
  const texture = this.device.createTexture({
147
142
  size: [this.width, this.height],
148
143
  format: format || "bgra8unorm",
149
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
144
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
150
145
  });
151
146
  this.textures.set(name, texture);
152
147
  return texture;
@@ -159,37 +154,13 @@ var TextureManager = class {
159
154
  this.width = width;
160
155
  this.height = height;
161
156
  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
157
  texture.destroy();
185
158
  });
186
- this.oldTextures.length = 0;
159
+ this.textures.clear();
187
160
  }
188
161
  destroy() {
189
162
  this.textures.forEach((texture) => texture.destroy());
190
163
  this.textures.clear();
191
- this.oldTextures.forEach((texture) => texture.destroy());
192
- this.oldTextures.length = 0;
193
164
  }
194
165
  getPixelSize() {
195
166
  return {
@@ -199,6 +170,30 @@ var TextureManager = class {
199
170
  }
200
171
  };
201
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
+
202
197
  //#endregion
203
198
  //#region src/index.ts
204
199
  var WGSLRenderer = class {
@@ -230,19 +225,17 @@ var WGSLRenderer = class {
230
225
  }
231
226
  async resize(width, height) {
232
227
  if (this.isResizing) return;
233
- const currentSize = this.textureManager.getPixelSize();
234
- if (currentSize.width === width && currentSize.height === height) return;
228
+ if (this.canvas.width === width && this.canvas.height === height) return;
235
229
  this.isResizing = true;
236
230
  this.canvas.width = width;
237
231
  this.canvas.height = height;
238
232
  const future = this.device.queue.onSubmittedWorkDone();
239
233
  future.catch(() => {
234
+ console.warn("GPU work submission failed during resize.");
240
235
  this.isResizing = false;
241
236
  });
242
237
  await future;
243
- this.textureManager.cleanupOldTextures();
244
238
  this.textureManager.resize(width, height);
245
- this.updateAllBindGroups();
246
239
  this.isResizing = false;
247
240
  }
248
241
  getContext() {
@@ -252,84 +245,79 @@ var WGSLRenderer = class {
252
245
  return this.device;
253
246
  }
254
247
  /**
255
- * Update bind groups for all passes after texture resize
248
+ * Get texture reference by pass name
249
+ * Returns a PassTextureRef that will resolve to the actual texture at render time
256
250
  */
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
- });
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);
278
254
  }
279
255
  /**
280
- * Recreate all pass output textures after resize
256
+ * Resolve a PassTextureRef to actual GPUTextureView with validation
281
257
  */
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
- });
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);
289
271
  }
290
272
  /**
291
273
  * Add a render pass to the multi-pass pipeline
292
274
  */
293
275
  addPass(descriptor) {
294
276
  const finalBindGroupEntries = [];
295
- const firstPass = this.passes.length === 0;
296
- if (!firstPass) {
297
- const previousOutput = this.getPassOutput(this.passes.length - 1);
298
- if (previousOutput) finalBindGroupEntries.push({
299
- binding: 0,
300
- resource: previousOutput.createView()
301
- });
302
- }
303
- descriptor.resources.forEach((resource, index) => {
277
+ descriptor.resources?.forEach((resource, index) => {
304
278
  finalBindGroupEntries.push({
305
- binding: index + (firstPass ? 0 : 1),
279
+ binding: index,
306
280
  resource
307
281
  });
308
282
  });
309
- const pass = new RenderPass({
283
+ const internalDescriptor = {
310
284
  name: descriptor.name,
311
285
  shaderCode: descriptor.shaderCode,
286
+ entryPoints: descriptor.entryPoints,
312
287
  clearColor: descriptor.clearColor,
313
288
  blendMode: descriptor.blendMode,
314
- bindGroupEntries: finalBindGroupEntries
315
- }, this.device, this.format, "auto");
316
- pass.passResources = [...descriptor.resources];
289
+ bindGroupEntries: finalBindGroupEntries,
290
+ view: descriptor.view,
291
+ format: descriptor.format
292
+ };
293
+ const pipelineFormat = descriptor.format || this.format;
294
+ const pass = new RenderPass(internalDescriptor, this.device, pipelineFormat, "auto");
295
+ pass.passResources = descriptor.resources ?? [];
317
296
  this.passes.push(pass);
318
- const currentPassIndex = this.passes.length - 1;
319
- const textureName = `pass_${currentPassIndex}_output`;
320
- this.textureManager.createTexture(textureName, this.format);
321
- this.passes[currentPassIndex].hasOutputTexture = true;
322
297
  }
323
298
  /**
324
- * Get the output texture of a specific pass
299
+ * Resolve resource to actual GPU binding resource
300
+ * Handles PassTextureRef by getting the current texture view with validation
325
301
  */
326
- getPassOutput(passIndex) {
327
- if (passIndex < 0 || passIndex >= this.passes.length) return;
328
- if (passIndex === this.passes.length - 1) {
329
- if (!this.passes[passIndex]?.hasOutputTexture) return;
330
- }
331
- const textureName = `pass_${passIndex}_output`;
332
- return this.textureManager.getTexture(textureName);
302
+ resolveResource(resource) {
303
+ if (isPassTextureRef(resource)) return this.resolveTextureRef(resource);
304
+ return resource;
305
+ }
306
+ /**
307
+ * Update bind groups to resolve current texture references
308
+ * Call this before rendering to ensure all PassTextureRef are resolved
309
+ */
310
+ updateBindGroups() {
311
+ this.passes.forEach((pass) => {
312
+ const finalBindGroupEntries = [];
313
+ pass.passResources.forEach((resource, index) => {
314
+ if (resource) finalBindGroupEntries.push({
315
+ binding: index,
316
+ resource: this.resolveResource(resource)
317
+ });
318
+ });
319
+ pass.updateBindGroup(finalBindGroupEntries);
320
+ });
333
321
  }
334
322
  /**
335
323
  * Create a uniforms
@@ -367,7 +355,11 @@ var WGSLRenderer = class {
367
355
  console.error("Failed to load texture:", err);
368
356
  });
369
357
  const res = await resp;
370
- const imgBitmap = await createImageBitmap(await res.blob());
358
+ const future = createImageBitmap(await res.blob());
359
+ future.catch((err) => {
360
+ console.error("Failed to load texture:", err);
361
+ });
362
+ const imgBitmap = await future;
371
363
  const texture = this.device.createTexture({
372
364
  size: [
373
365
  imgBitmap.width,
@@ -386,22 +378,25 @@ var WGSLRenderer = class {
386
378
  }
387
379
  renderFrame() {
388
380
  if (this.passes.length === 0) return;
381
+ this.updateBindGroups();
389
382
  const commandEncoder = this.device.createCommandEncoder();
390
383
  for (let i = 0; i < this.passes.length; i++) {
391
384
  const pass = this.passes[i];
385
+ let loadOp = "load";
386
+ const isLast = i === this.passes.length - 1;
387
+ if (isLast) loadOp = "clear";
392
388
  let renderTarget;
393
- let loadOp = "clear";
394
- if (i === this.passes.length - 1) renderTarget = this.ctx.getCurrentTexture().createView();
389
+ if (pass.view) renderTarget = pass.view;
390
+ else if (isLast) renderTarget = this.ctx.getCurrentTexture().createView();
395
391
  else {
396
392
  const textureName = `pass_${i}_output`;
397
- const texture = this.textureManager.getTexture(textureName);
398
- if (!texture) continue;
393
+ let texture = this.textureManager.getTexture(textureName);
394
+ if (!texture) texture = this.textureManager.createTexture(textureName, "rgba16float");
399
395
  renderTarget = texture.createView();
400
- loadOp = "load";
401
396
  }
402
397
  const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
403
398
  view: renderTarget,
404
- loadOp: i === 0 ? "clear" : loadOp,
399
+ loadOp,
405
400
  storeOp: "store",
406
401
  clearValue: pass.clearColor
407
402
  }] });
@@ -414,9 +409,11 @@ var WGSLRenderer = class {
414
409
  this.device.queue.submit([commandEncoder.finish()]);
415
410
  }
416
411
  loopRender(cb) {
417
- cb?.();
418
- this.renderFrame();
419
- this.animationFrameId = requestAnimationFrame(() => this.loopRender(cb));
412
+ this.animationFrameId = requestAnimationFrame((t) => {
413
+ cb?.(t);
414
+ this.renderFrame();
415
+ this.loopRender(cb);
416
+ });
420
417
  }
421
418
  stopLoop() {
422
419
  if (this.animationFrameId !== null) {
@@ -0,0 +1,154 @@
1
+ //#region src/PassTextureRef.d.ts
2
+ declare const PASS_TEXTURE_REF_SYMBOL: unique symbol;
3
+ declare class PassTextureRef {
4
+ readonly [PASS_TEXTURE_REF_SYMBOL] = true;
5
+ readonly passName: string;
6
+ constructor(passName: string);
7
+ static is(obj: any): obj is PassTextureRef;
8
+ static fromGPUBindingResource(resource: GPUBindingResource): PassTextureRef | null;
9
+ static create(passName: string): PassTextureRef;
10
+ }
11
+ //#endregion
12
+ //#region src/RenderPass.d.ts
13
+ type BandingResource = GPUBindingResource | PassTextureRef;
14
+ type BindingEntry = {
15
+ binding: number;
16
+ resource: BandingResource;
17
+ };
18
+ interface RenderPassOptions {
19
+ name: string;
20
+ shaderCode: string;
21
+ entryPoints?: {
22
+ vertex?: string;
23
+ fragment?: string;
24
+ };
25
+ clearColor?: {
26
+ r: number;
27
+ g: number;
28
+ b: number;
29
+ a: number;
30
+ };
31
+ blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
32
+ resources?: BandingResource[];
33
+ view?: GPUTextureView;
34
+ format?: GPUTextureFormat;
35
+ }
36
+ interface InternalRenderPassDescriptor {
37
+ name: string;
38
+ shaderCode: string;
39
+ entryPoints?: {
40
+ vertex?: string;
41
+ fragment?: string;
42
+ };
43
+ clearColor?: {
44
+ r: number;
45
+ g: number;
46
+ b: number;
47
+ a: number;
48
+ };
49
+ blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
50
+ bindGroupEntries: BindingEntry[];
51
+ view?: GPUTextureView;
52
+ format?: GPUTextureFormat;
53
+ }
54
+ declare class RenderPass {
55
+ name: string;
56
+ pipeline: GPURenderPipeline;
57
+ bindGroup: GPUBindGroup | null;
58
+ vertexBuffer: GPUBuffer;
59
+ clearColor: {
60
+ r: number;
61
+ g: number;
62
+ b: number;
63
+ a: number;
64
+ };
65
+ blendMode: 'additive' | 'alpha' | 'multiply' | 'none';
66
+ view?: GPUTextureView;
67
+ format?: GPUTextureFormat;
68
+ passResources: BandingResource[];
69
+ private device;
70
+ constructor(descriptor: InternalRenderPassDescriptor, device: GPUDevice, format: GPUTextureFormat, layout: GPUPipelineLayout | 'auto');
71
+ /**
72
+ * Update bind group with new entries (e.g., after texture resize)
73
+ */
74
+ updateBindGroup(newEntries: BindingEntry[]): void;
75
+ private getBlendState;
76
+ }
77
+ //#endregion
78
+ //#region src/index.d.ts
79
+ interface MultiPassDescriptor {
80
+ passes: RenderPassOptions[];
81
+ }
82
+ interface WGSLRendererOptions {
83
+ config?: GPUCanvasConfiguration;
84
+ }
85
+ declare class WGSLRenderer {
86
+ canvas: HTMLCanvasElement;
87
+ options?: WGSLRendererOptions | undefined;
88
+ private ctx;
89
+ private device;
90
+ private format;
91
+ private passes;
92
+ private textureManager;
93
+ private animationFrameId;
94
+ private isResizing;
95
+ constructor(canvas: HTMLCanvasElement, options?: WGSLRendererOptions | undefined);
96
+ init(): Promise<void>;
97
+ resize(width: number, height: number): Promise<void>;
98
+ getContext(): GPUCanvasContext;
99
+ getDevice(): GPUDevice;
100
+ /**
101
+ * Get texture reference by pass name
102
+ * Returns a PassTextureRef that will resolve to the actual texture at render time
103
+ */
104
+ getPassTexture(passName: string): PassTextureRef;
105
+ /**
106
+ * Resolve a PassTextureRef to actual GPUTextureView with validation
107
+ */
108
+ private resolveTextureRef;
109
+ /**
110
+ * Get pass by name
111
+ */
112
+ getPassByName(passName: string): RenderPass | undefined;
113
+ /**
114
+ * Add a render pass to the multi-pass pipeline
115
+ */
116
+ addPass(descriptor: RenderPassOptions): void;
117
+ /**
118
+ * Resolve resource to actual GPU binding resource
119
+ * Handles PassTextureRef by getting the current texture view with validation
120
+ */
121
+ private resolveResource;
122
+ /**
123
+ * Update bind groups to resolve current texture references
124
+ * Call this before rendering to ensure all PassTextureRef are resolved
125
+ */
126
+ private updateBindGroups;
127
+ /**
128
+ * Create a uniforms
129
+ * @param length The length of the uniform buffer in number of floats
130
+ * @return The uniform object containing the buffer and data array
131
+ */
132
+ createUniforms(length: number): {
133
+ values: Float32Array<ArrayBuffer>;
134
+ apply: () => void;
135
+ getBuffer: () => GPUBuffer;
136
+ };
137
+ /**
138
+ * Create a sampler
139
+ */
140
+ createSampler(options?: GPUSamplerDescriptor): GPUSampler;
141
+ loadImageTexture(url: string): Promise<{
142
+ texture: GPUTexture;
143
+ width: number;
144
+ height: number;
145
+ }>;
146
+ renderFrame(): void;
147
+ loopRender(cb?: {
148
+ (t?: number): void;
149
+ }): void;
150
+ stopLoop(): void;
151
+ }
152
+ declare function createWGSLRenderer(cvs: HTMLCanvasElement, options?: WGSLRendererOptions): Promise<WGSLRenderer>;
153
+ //#endregion
154
+ export { MultiPassDescriptor, createWGSLRenderer };