@bloopjs/toodle 0.0.104 → 0.1.1

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.
Files changed (121) hide show
  1. package/dist/Toodle.d.ts +41 -19
  2. package/dist/Toodle.d.ts.map +1 -1
  3. package/dist/backends/IBackendShader.d.ts +48 -0
  4. package/dist/backends/IBackendShader.d.ts.map +1 -0
  5. package/dist/backends/IRenderBackend.d.ts +92 -0
  6. package/dist/backends/IRenderBackend.d.ts.map +1 -0
  7. package/dist/backends/ITextureAtlas.d.ts +34 -0
  8. package/dist/backends/ITextureAtlas.d.ts.map +1 -0
  9. package/dist/backends/detection.d.ts +16 -0
  10. package/dist/backends/detection.d.ts.map +1 -0
  11. package/dist/backends/mod.d.ts +9 -0
  12. package/dist/backends/mod.d.ts.map +1 -0
  13. package/dist/backends/webgl2/WebGLBackend.d.ts +51 -0
  14. package/dist/backends/webgl2/WebGLBackend.d.ts.map +1 -0
  15. package/dist/backends/webgl2/WebGLQuadShader.d.ts +17 -0
  16. package/dist/backends/webgl2/WebGLQuadShader.d.ts.map +1 -0
  17. package/dist/backends/webgl2/glsl/quad.glsl.d.ts +12 -0
  18. package/dist/backends/webgl2/glsl/quad.glsl.d.ts.map +1 -0
  19. package/dist/backends/webgl2/mod.d.ts +3 -0
  20. package/dist/backends/webgl2/mod.d.ts.map +1 -0
  21. package/dist/backends/webgpu/ShaderDescriptor.d.ts.map +1 -0
  22. package/dist/{textures → backends/webgpu}/TextureComputeShader.d.ts +1 -1
  23. package/dist/backends/webgpu/TextureComputeShader.d.ts.map +1 -0
  24. package/dist/backends/webgpu/WebGPUBackend.d.ts +67 -0
  25. package/dist/backends/webgpu/WebGPUBackend.d.ts.map +1 -0
  26. package/dist/backends/webgpu/WebGPUQuadShader.d.ts +18 -0
  27. package/dist/backends/webgpu/WebGPUQuadShader.d.ts.map +1 -0
  28. package/dist/backends/webgpu/mod.d.ts +3 -0
  29. package/dist/backends/webgpu/mod.d.ts.map +1 -0
  30. package/dist/backends/webgpu/parser.d.ts.map +1 -0
  31. package/dist/{shaders → backends/webgpu}/postprocess/blur.d.ts +1 -1
  32. package/dist/backends/webgpu/postprocess/blur.d.ts.map +1 -0
  33. package/dist/{shaders → backends/webgpu}/postprocess/mod.d.ts +1 -1
  34. package/dist/backends/webgpu/postprocess/mod.d.ts.map +1 -0
  35. package/dist/backends/webgpu/samplers.d.ts.map +1 -0
  36. package/dist/backends/webgpu/wgsl/example.wgsl.d.ts.map +1 -0
  37. package/dist/backends/webgpu/wgsl/hello.wgsl.d.ts.map +1 -0
  38. package/dist/backends/webgpu/wgsl/helloInstanced.wgsl.d.ts.map +1 -0
  39. package/dist/backends/webgpu/wgsl/pixel-scraping.wgsl.d.ts.map +1 -0
  40. package/dist/backends/webgpu/wgsl/quad.wgsl.d.ts.map +1 -0
  41. package/dist/coreTypes/EngineUniform.d.ts.map +1 -0
  42. package/dist/mod.d.ts +3 -2
  43. package/dist/mod.d.ts.map +1 -1
  44. package/dist/mod.js +7247 -6663
  45. package/dist/mod.js.map +27 -22
  46. package/dist/scene/Batcher.d.ts +2 -2
  47. package/dist/scene/Batcher.d.ts.map +1 -1
  48. package/dist/scene/QuadNode.d.ts +3 -2
  49. package/dist/scene/QuadNode.d.ts.map +1 -1
  50. package/dist/scene/RenderComponent.d.ts +2 -2
  51. package/dist/scene/RenderComponent.d.ts.map +1 -1
  52. package/dist/text/TextShader.d.ts +8 -6
  53. package/dist/text/TextShader.d.ts.map +1 -1
  54. package/dist/textures/AssetManager.d.ts +21 -5
  55. package/dist/textures/AssetManager.d.ts.map +1 -1
  56. package/dist/textures/util.d.ts.map +1 -1
  57. package/dist/utils/boilerplate.d.ts +1 -1
  58. package/dist/utils/boilerplate.d.ts.map +1 -1
  59. package/package.json +1 -1
  60. package/src/Toodle.ts +124 -156
  61. package/src/backends/IBackendShader.ts +52 -0
  62. package/src/backends/IRenderBackend.ts +118 -0
  63. package/src/backends/ITextureAtlas.ts +35 -0
  64. package/src/backends/detection.ts +46 -0
  65. package/src/backends/mod.ts +29 -0
  66. package/src/backends/webgl2/WebGLBackend.ts +256 -0
  67. package/src/backends/webgl2/WebGLQuadShader.ts +278 -0
  68. package/src/backends/webgl2/glsl/quad.glsl.ts +114 -0
  69. package/src/backends/webgl2/mod.ts +2 -0
  70. package/src/{textures → backends/webgpu}/TextureComputeShader.ts +2 -48
  71. package/src/backends/webgpu/WebGPUBackend.ts +350 -0
  72. package/src/{shaders/QuadShader.ts → backends/webgpu/WebGPUQuadShader.ts} +226 -170
  73. package/src/backends/webgpu/mod.ts +2 -0
  74. package/src/{shaders → backends/webgpu}/parser.ts +2 -2
  75. package/src/{shaders → backends/webgpu}/postprocess/blur.ts +2 -2
  76. package/src/{shaders → backends/webgpu}/postprocess/mod.ts +1 -1
  77. package/src/mod.ts +3 -2
  78. package/src/scene/Batcher.ts +3 -3
  79. package/src/scene/QuadNode.ts +6 -2
  80. package/src/scene/RenderComponent.ts +2 -2
  81. package/src/text/TextShader.ts +17 -11
  82. package/src/textures/AssetManager.ts +117 -93
  83. package/src/textures/util.ts +0 -65
  84. package/src/utils/boilerplate.ts +1 -1
  85. package/dist/shaders/EngineUniform.d.ts.map +0 -1
  86. package/dist/shaders/IShader.d.ts +0 -15
  87. package/dist/shaders/IShader.d.ts.map +0 -1
  88. package/dist/shaders/QuadShader.d.ts +0 -18
  89. package/dist/shaders/QuadShader.d.ts.map +0 -1
  90. package/dist/shaders/ShaderDescriptor.d.ts.map +0 -1
  91. package/dist/shaders/mod.d.ts +0 -6
  92. package/dist/shaders/mod.d.ts.map +0 -1
  93. package/dist/shaders/parser.d.ts.map +0 -1
  94. package/dist/shaders/postprocess/blur.d.ts.map +0 -1
  95. package/dist/shaders/postprocess/mod.d.ts.map +0 -1
  96. package/dist/shaders/samplers.d.ts.map +0 -1
  97. package/dist/shaders/wgsl/example.wgsl.d.ts.map +0 -1
  98. package/dist/shaders/wgsl/hello.wgsl.d.ts.map +0 -1
  99. package/dist/shaders/wgsl/helloInstanced.wgsl.d.ts.map +0 -1
  100. package/dist/shaders/wgsl/quad.wgsl.d.ts.map +0 -1
  101. package/dist/textures/TextureComputeShader.d.ts.map +0 -1
  102. package/dist/textures/pixel-scraping.wgsl.d.ts.map +0 -1
  103. package/src/shaders/IShader.ts +0 -20
  104. package/src/shaders/mod.ts +0 -5
  105. /package/dist/{shaders → backends/webgpu}/ShaderDescriptor.d.ts +0 -0
  106. /package/dist/{shaders → backends/webgpu}/parser.d.ts +0 -0
  107. /package/dist/{shaders → backends/webgpu}/samplers.d.ts +0 -0
  108. /package/dist/{shaders → backends/webgpu}/wgsl/example.wgsl.d.ts +0 -0
  109. /package/dist/{shaders → backends/webgpu}/wgsl/hello.wgsl.d.ts +0 -0
  110. /package/dist/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.d.ts +0 -0
  111. /package/dist/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.d.ts +0 -0
  112. /package/dist/{shaders → backends/webgpu}/wgsl/quad.wgsl.d.ts +0 -0
  113. /package/dist/{shaders → coreTypes}/EngineUniform.d.ts +0 -0
  114. /package/src/{shaders → backends/webgpu}/ShaderDescriptor.ts +0 -0
  115. /package/src/{shaders → backends/webgpu}/samplers.ts +0 -0
  116. /package/src/{shaders → backends/webgpu}/wgsl/example.wgsl.ts +0 -0
  117. /package/src/{shaders → backends/webgpu}/wgsl/hello.wgsl.ts +0 -0
  118. /package/src/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.ts +0 -0
  119. /package/src/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.ts +0 -0
  120. /package/src/{shaders → backends/webgpu}/wgsl/quad.wgsl.ts +0 -0
  121. /package/src/{shaders → coreTypes}/EngineUniform.ts +0 -0
package/src/Toodle.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  import { type Mat3, mat3 } from "wgpu-matrix";
2
+ import type { IBackendShader } from "./backends/IBackendShader";
3
+ import type { BlendMode } from "./backends/IRenderBackend";
4
+ import type { BackendType, IRenderBackend } from "./backends/mod";
5
+ import { detectBackend } from "./backends/mod";
6
+ import { WebGLBackend } from "./backends/webgl2/mod";
7
+ import { WebGPUBackend } from "./backends/webgpu/mod";
8
+ import type { PostProcess } from "./backends/webgpu/postprocess/mod";
2
9
  import type { Color } from "./coreTypes/Color";
10
+ import type { EngineUniform } from "./coreTypes/EngineUniform";
3
11
  import type { Point } from "./coreTypes/Point";
4
12
  import type { Size } from "./coreTypes/Size";
5
13
  import type { Limits, LimitsOptions } from "./limits";
6
- import { DEFAULT_LIMITS } from "./limits";
7
14
  import {
8
15
  convertScreenToWorld,
9
16
  convertWorldToScreen,
@@ -15,14 +22,9 @@ import { JumboQuadNode, type JumboQuadOptions } from "./scene/JumboQuadNode";
15
22
  import { QuadNode, type QuadOptions } from "./scene/QuadNode";
16
23
  import { type NodeOptions, SceneNode } from "./scene/SceneNode";
17
24
  import type { Resolution } from "./screen/resolution";
18
- import type { EngineUniform } from "./shaders/EngineUniform";
19
- import type { IShader } from "./shaders/IShader";
20
- import type { PostProcess } from "./shaders/postprocess/mod";
21
- import { QuadShader, type QuadShaderOpts } from "./shaders/QuadShader";
22
25
  import { TextNode, type TextOptions } from "./text/TextNode";
23
26
  import { AssetManager, type TextureId } from "./textures/AssetManager";
24
- import { initGpu } from "./utils/boilerplate";
25
- import { assert, Pool } from "./utils/mod";
27
+ import { Pool } from "./utils/mod";
26
28
 
27
29
  export class Toodle {
28
30
  /**
@@ -45,8 +47,8 @@ export class Toodle {
45
47
  instancesEnqueued: 0,
46
48
  };
47
49
 
48
- /** sometimes for debugging you might want to access the GPU device, this should not be necessary in normal operation */
49
- debug: { device: GPUDevice; presentationFormat: GPUTextureFormat };
50
+ /** The render backend (WebGPU or WebGL2) */
51
+ #backend: IRenderBackend;
50
52
 
51
53
  /**
52
54
  * Camera. This applies a 2d perspective projection matrix to any nodes drawn with toodle.draw
@@ -64,14 +66,6 @@ export class Toodle {
64
66
  #engineUniform: EngineUniform;
65
67
  #projectionMatrix: Mat3 = mat3.identity();
66
68
  #batcher = new Batcher();
67
- #limits: Limits;
68
- #device: GPUDevice;
69
- #context: GPUCanvasContext;
70
- #presentationFormat: GPUTextureFormat;
71
- #postprocess: PostProcess | null = null;
72
- #renderPass?: GPURenderPassEncoder;
73
- #encoder?: GPUCommandEncoder;
74
- #pingpong!: [GPUTexture, GPUTexture];
75
69
  #defaultFilter: GPUFilterMode;
76
70
  #matrixPool: Pool<Mat3>;
77
71
  #atlasSize: Size;
@@ -81,31 +75,22 @@ export class Toodle {
81
75
  * see {@link Toodle.attach} for creating a Toodle instance that draws to a canvas.
82
76
  */
83
77
  constructor(
84
- device: GPUDevice,
85
- context: GPUCanvasContext,
86
- presentationFormat: GPUTextureFormat,
78
+ backend: IRenderBackend,
87
79
  canvas: HTMLCanvasElement,
88
80
  resolution: Resolution,
89
81
  options: ToodleOptions,
90
82
  ) {
91
- this.#limits = {
92
- ...DEFAULT_LIMITS,
93
- ...options.limits,
94
- };
83
+ this.#backend = backend;
95
84
  this.#matrixPool = new Pool<Mat3>(
96
85
  () => mat3.identity(),
97
- this.#limits.instanceCount,
86
+ backend.limits.instanceCount,
98
87
  );
99
- this.#device = device;
100
- this.#context = context;
101
- this.#presentationFormat = presentationFormat;
102
88
  this.#defaultFilter = options.filter ?? "linear";
103
- this.assets = new AssetManager(device, presentationFormat, this.#limits);
104
- this.#atlasSize = {
105
- width: this.assets.textureAtlas.width,
106
- height: this.assets.textureAtlas.height,
107
- };
108
- this.debug = { device, presentationFormat };
89
+
90
+ // Create AssetManager with the backend
91
+ this.assets = new AssetManager(backend);
92
+
93
+ this.#atlasSize = backend.atlasSize;
109
94
  this.#engineUniform = {
110
95
  resolution,
111
96
  camera: this.camera,
@@ -113,29 +98,26 @@ export class Toodle {
113
98
  };
114
99
  this.#resolution = resolution;
115
100
 
116
- this.#createPingPongTextures(resolution);
117
101
  this.resize(this.#resolution);
118
-
119
102
  this.#resizeObserver = this.#createResizeObserver(canvas);
120
103
  }
121
104
 
122
105
  /**
123
106
  * Screen shader is an optional slot for post-processing effects.
124
107
  * Note that this will do the main render pass to an offscreen texture, which may impact performance.
108
+ * Currently only supported in WebGPU mode.
125
109
  */
126
- get postprocess() {
127
- return this.#postprocess;
110
+ get postprocess(): PostProcess | null {
111
+ if (this.#backend.type !== "webgpu") return null;
112
+ return (this.#backend as WebGPUBackend).getPostprocess();
128
113
  }
129
114
 
130
115
  set postprocess(value: PostProcess | null) {
131
- this.#postprocess = value;
132
- const hasChanged =
133
- this.#resolution.width !==
134
- this.#pingpong[0].width / window.devicePixelRatio ||
135
- this.#resolution.height !==
136
- this.#pingpong[0].height / window.devicePixelRatio;
137
- if (value != null && hasChanged) {
138
- this.#createPingPongTextures(this.#resolution);
116
+ if (value !== null && this.#backend.type !== "webgpu") {
117
+ throw new Error("Post-processing is only supported in WebGPU mode");
118
+ }
119
+ if (this.#backend.type === "webgpu") {
120
+ (this.#backend as WebGPUBackend).setPostprocess(value);
139
121
  }
140
122
  }
141
123
 
@@ -159,43 +141,8 @@ export class Toodle {
159
141
  */
160
142
  resize(resolution: Resolution) {
161
143
  createProjectionMatrix(resolution, this.#projectionMatrix);
162
-
163
- const hasChanged =
164
- resolution.width !== this.#resolution.width ||
165
- resolution.height !== this.#resolution.height;
166
- if (this.postprocess && hasChanged) {
167
- this.#pingpong[0].destroy();
168
- this.#pingpong[1].destroy();
169
- }
170
144
  this.#resolution = resolution;
171
- }
172
-
173
- #createPingPongTextures(resolution: Resolution) {
174
- if (this.#pingpong?.length) {
175
- this.#pingpong[0].destroy();
176
- this.#pingpong[1].destroy();
177
- }
178
-
179
- this.#pingpong = [
180
- this.#device.createTexture({
181
- size: {
182
- width: resolution.width * window.devicePixelRatio,
183
- height: resolution.height * window.devicePixelRatio,
184
- },
185
- format: this.#presentationFormat,
186
- usage:
187
- GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
188
- }),
189
- this.#device.createTexture({
190
- size: {
191
- width: resolution.width * window.devicePixelRatio,
192
- height: resolution.height * window.devicePixelRatio,
193
- },
194
- format: this.#presentationFormat,
195
- usage:
196
- GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
197
- }),
198
- ];
145
+ this.#backend.resize(resolution.width, resolution.height);
199
146
  }
200
147
 
201
148
  #createResizeObserver(canvas: HTMLCanvasElement) {
@@ -265,7 +212,7 @@ export class Toodle {
265
212
  * const instanceLimit: number = toodle.limits.instanceCount;
266
213
  */
267
214
  get limits(): Limits {
268
- return this.#limits;
215
+ return this.#backend.limits;
269
216
  }
270
217
 
271
218
  get batcher(): Batcher {
@@ -277,9 +224,9 @@ export class Toodle {
277
224
  */
278
225
  get maxPixels() {
279
226
  return (
280
- this.#limits.textureSize *
281
- this.#limits.textureSize *
282
- this.#limits.textureArrayLayers
227
+ this.limits.textureSize *
228
+ this.limits.textureSize *
229
+ this.limits.textureArrayLayers
283
230
  );
284
231
  }
285
232
 
@@ -289,9 +236,9 @@ export class Toodle {
289
236
  */
290
237
  get maxGpuMemory() {
291
238
  return (
292
- this.#limits.instanceCount *
293
- this.#limits.instanceBufferSize *
294
- this.#limits.shaderCount
239
+ this.limits.instanceCount *
240
+ this.limits.instanceBufferSize *
241
+ this.limits.shaderCount
295
242
  );
296
243
  }
297
244
 
@@ -306,21 +253,7 @@ export class Toodle {
306
253
  * toodle.endFrame();
307
254
  */
308
255
  startFrame(options?: StartFrameOptions) {
309
- this.#encoder = this.#device.createCommandEncoder();
310
- const target = this.postprocess
311
- ? this.#pingpong[0]
312
- : this.#context.getCurrentTexture();
313
- this.#renderPass = this.#encoder.beginRenderPass({
314
- label: `toodle frame ${this.diagnostics.frames}`,
315
- colorAttachments: [
316
- {
317
- view: target.createView(),
318
- clearValue: this.clearColor,
319
- loadOp: options?.loadOp ?? "clear",
320
- storeOp: "store",
321
- },
322
- ],
323
- });
256
+ this.#backend.startFrame(this.clearColor, options?.loadOp ?? "clear");
324
257
 
325
258
  this.diagnostics.drawCalls =
326
259
  this.diagnostics.pipelineSwitches =
@@ -360,24 +293,22 @@ export class Toodle {
360
293
  */
361
294
  endFrame() {
362
295
  try {
363
- assert(
364
- this.#renderPass,
365
- "no render pass found. have you called startFrame?",
366
- );
367
- assert(this.#encoder, "no encoder found. have you called startFrame?");
368
-
369
296
  mat3.mul(
370
297
  this.#projectionMatrix,
371
298
  this.camera.matrix,
372
299
  this.#engineUniform.viewProjectionMatrix,
373
300
  );
374
301
  this.#engineUniform.resolution = this.#resolution;
302
+
303
+ // Update engine uniforms on the backend
304
+ this.#backend.updateEngineUniform(this.#engineUniform);
305
+
375
306
  for (const pipeline of this.#batcher.pipelines) {
376
- pipeline.shader.startFrame(this.#device, this.#engineUniform);
307
+ pipeline.shader.startFrame(this.#engineUniform);
377
308
  }
378
309
 
379
310
  this.diagnostics.instancesEnqueued = this.#batcher.nodes.length;
380
- if (this.#batcher.nodes.length > this.#limits.instanceCount) {
311
+ if (this.#batcher.nodes.length > this.limits.instanceCount) {
381
312
  const err = new Error(
382
313
  `ToodleInstanceCap: ${this.batcher.nodes.length} instances enqueued, max is ${this.limits.instanceCount}`,
383
314
  );
@@ -389,7 +320,6 @@ export class Toodle {
389
320
  for (const pipeline of layer.pipelines) {
390
321
  this.diagnostics.pipelineSwitches++;
391
322
  this.diagnostics.drawCalls += pipeline.shader.processBatch(
392
- this.#renderPass,
393
323
  pipeline.nodes,
394
324
  );
395
325
  }
@@ -398,17 +328,8 @@ export class Toodle {
398
328
  for (const pipeline of this.#batcher.pipelines) {
399
329
  pipeline.shader.endFrame();
400
330
  }
401
- this.#renderPass.end();
402
-
403
- if (this.postprocess) {
404
- this.postprocess.process(
405
- this.#device.queue,
406
- this.#encoder,
407
- this.#pingpong,
408
- this.#context.getCurrentTexture(),
409
- );
410
- }
411
- this.#device.queue.submit([this.#encoder.finish()]);
331
+
332
+ this.#backend.endFrame();
412
333
  } finally {
413
334
  this.#batcher.flush();
414
335
  this.#matrixPool.free();
@@ -466,7 +387,7 @@ export class Toodle {
466
387
  *
467
388
  * @param label Debug name of the shader
468
389
  * @param instanceCount - The maximum number of instances that will be processed by the shader. Note that a worst-case buffer of this many instances will be immediately allocated.
469
- * @param userCode - The WGSL code to be used for the shader.
390
+ * @param userCode - The WGSL code to be used for the shader (WebGPU only).
470
391
  * @param blendMode - The blend mode to be used for the shader.
471
392
  *
472
393
  * @example
@@ -478,16 +399,14 @@ export class Toodle {
478
399
  instanceCount: number,
479
400
  userCode: string,
480
401
  shaderOpts?: QuadShaderOpts,
481
- ) {
482
- return new QuadShader(
402
+ ): IBackendShader {
403
+ return this.#backend.createQuadShader({
483
404
  label,
484
- shaderOpts?.assetManager ?? this.assets,
485
- this.#device,
486
- this.#presentationFormat,
487
- userCode,
488
405
  instanceCount,
489
- shaderOpts?.blendMode,
490
- );
406
+ userCode,
407
+ blendMode: shaderOpts?.blendMode,
408
+ atlasId: shaderOpts?.atlasId,
409
+ });
491
410
  }
492
411
 
493
412
  /**
@@ -718,17 +637,18 @@ export class Toodle {
718
637
  },
719
638
  };
720
639
 
721
- #quadShader: QuadShader | null = null;
640
+ #quadShader: IBackendShader | null = null;
722
641
 
723
- #defaultQuadShader() {
642
+ #defaultQuadShader(): IBackendShader {
724
643
  if (this.#quadShader) {
725
644
  return this.#quadShader;
726
645
  }
727
646
 
728
- const shader = this.QuadShader(
729
- "default quad shader",
730
- this.#limits.instanceCount,
731
- /*wgsl*/ `
647
+ // For WebGPU, we can provide custom WGSL shader code
648
+ // For WebGL, the backend will use its default shader
649
+ const userCode =
650
+ this.#backend.type === "webgpu"
651
+ ? /*wgsl*/ `
732
652
  @fragment
733
653
  fn frag(vertex: VertexOutput) -> @location(0) vec4f {
734
654
  let color = default_fragment_shader(vertex, ${
@@ -738,8 +658,14 @@ export class Toodle {
738
658
  });
739
659
  return color;
740
660
  }
741
- `,
742
- );
661
+ `
662
+ : undefined;
663
+
664
+ const shader = this.#backend.createQuadShader({
665
+ label: "default quad shader",
666
+ instanceCount: this.limits.instanceCount,
667
+ userCode,
668
+ });
743
669
 
744
670
  this.#quadShader = shader;
745
671
  return shader;
@@ -761,11 +687,29 @@ export class Toodle {
761
687
  static async attach(canvas: HTMLCanvasElement, options?: ToodleOptions) {
762
688
  canvas.width = canvas.clientWidth * devicePixelRatio;
763
689
  canvas.height = canvas.clientHeight * devicePixelRatio;
764
- const { device, context, presentationFormat } = await initGpu(canvas);
690
+
691
+ const backendOption = options?.backend ?? "auto";
692
+ let backendType: BackendType;
693
+
694
+ if (backendOption === "auto") {
695
+ backendType = await detectBackend();
696
+ } else {
697
+ backendType = backendOption;
698
+ }
699
+
700
+ let backend: IRenderBackend;
701
+ if (backendType === "webgpu") {
702
+ backend = await WebGPUBackend.create(canvas, {
703
+ limits: options?.limits,
704
+ });
705
+ } else {
706
+ backend = await WebGLBackend.create(canvas, {
707
+ limits: options?.limits,
708
+ });
709
+ }
710
+
765
711
  return new Toodle(
766
- device,
767
- context,
768
- presentationFormat,
712
+ backend,
769
713
  canvas,
770
714
  {
771
715
  width: canvas.clientWidth,
@@ -782,21 +726,22 @@ export class Toodle {
782
726
  */
783
727
  destroy() {
784
728
  this.#resizeObserver.disconnect();
785
- this.#device.destroy();
729
+ this.#backend.destroy();
786
730
  this.assets.destroy();
787
731
  }
788
732
 
789
733
  /**
790
- * Advanced and niche features
734
+ * Get the render backend instance.
735
+ * Cast to WebGPUBackend or WebGLBackend to access backend-specific properties.
736
+ *
737
+ * @example
738
+ * if (toodle.backend instanceof WebGPUBackend) {
739
+ * const device = toodle.backend.device;
740
+ * }
791
741
  */
792
- extra = {
793
- /**
794
- * Get the GPU device used by this Toodle instance.
795
- */
796
- device: (): GPUDevice => {
797
- return this.#device;
798
- },
799
- };
742
+ get backend(): WebGPUBackend | WebGLBackend {
743
+ return this.#backend as WebGPUBackend | WebGLBackend;
744
+ }
800
745
  }
801
746
 
802
747
  export type StartFrameOptions = {
@@ -826,6 +771,18 @@ export type ToodleOptions = {
826
771
  */
827
772
  filter?: "nearest" | "linear";
828
773
  limits?: LimitsOptions;
774
+ /**
775
+ * The rendering backend to use.
776
+ *
777
+ * **auto**: Automatically detect the best available backend (WebGPU > WebGL).
778
+ *
779
+ * **webgpu**: Use WebGPU backend. Throws if WebGPU is not available.
780
+ *
781
+ * **webgl2**: Use WebGL 2 backend (fallback for older browsers).
782
+ *
783
+ * @default "auto"
784
+ */
785
+ backend?: BackendType | "auto";
829
786
  };
830
787
 
831
788
  export type CircleOptions = Omit<QuadOptions, "size"> & {
@@ -857,7 +814,7 @@ export type LineOptions = {
857
814
  /**
858
815
  * The shader to use for the line.
859
816
  */
860
- shader?: IShader;
817
+ shader?: IBackendShader;
861
818
  /**
862
819
  * The layer to draw the line on.
863
820
  */
@@ -867,3 +824,14 @@ export type LineOptions = {
867
824
  */
868
825
  key?: string;
869
826
  };
827
+
828
+ export type QuadShaderOpts = {
829
+ /**
830
+ * Blend mode for alpha compositing.
831
+ */
832
+ blendMode?: BlendMode;
833
+ /**
834
+ * Which texture atlas to bind (default: "default").
835
+ */
836
+ atlasId?: string;
837
+ };
@@ -0,0 +1,52 @@
1
+ import type { EngineUniform } from "../coreTypes/EngineUniform";
2
+ import type { SceneNode } from "../scene/SceneNode";
3
+ import type { BlendMode } from "./IRenderBackend";
4
+
5
+ /**
6
+ * Options for creating a quad shader.
7
+ */
8
+ export type QuadShaderCreationOpts = {
9
+ /** Debug label for the shader */
10
+ label: string;
11
+ /** Maximum number of instances this shader can process per frame */
12
+ instanceCount: number;
13
+ /** User-defined shader code (WGSL for WebGPU, GLSL for WebGL) */
14
+ userCode?: string;
15
+ /** Blend mode for alpha compositing */
16
+ blendMode?: BlendMode;
17
+ /** Which texture atlas to bind (default: "default") */
18
+ atlasId?: string;
19
+ };
20
+
21
+ /**
22
+ * Backend-agnostic shader interface.
23
+ *
24
+ * This interface abstracts the differences between WebGPU and WebGL shaders.
25
+ * Each backend provides its own implementation.
26
+ */
27
+ export interface IBackendShader {
28
+ /** Debug label for the shader */
29
+ readonly label: string;
30
+
31
+ /**
32
+ * Prepare for a new frame.
33
+ * Called once per frame before any processBatch calls.
34
+ *
35
+ * @param uniform - Engine uniforms (view-projection, resolution)
36
+ */
37
+ startFrame(uniform: EngineUniform): void;
38
+
39
+ /**
40
+ * Process a batch of nodes and issue draw calls.
41
+ *
42
+ * @param nodes - The nodes to render
43
+ * @returns Number of draw calls issued
44
+ */
45
+ processBatch(nodes: SceneNode[]): number;
46
+
47
+ /**
48
+ * Cleanup after frame.
49
+ * Called once per frame after all processBatch calls.
50
+ */
51
+ endFrame(): void;
52
+ }
@@ -0,0 +1,118 @@
1
+ import type { Color } from "../coreTypes/Color";
2
+ import type { EngineUniform } from "../coreTypes/EngineUniform";
3
+ import type { Size } from "../coreTypes/Size";
4
+ import type { Limits } from "../limits";
5
+ import type { CpuTextureAtlas } from "../textures/types";
6
+ import type { IBackendShader, QuadShaderCreationOpts } from "./IBackendShader";
7
+ import type { ITextureAtlas, TextureAtlasOptions } from "./ITextureAtlas";
8
+
9
+ export type BackendType = "webgpu" | "webgl2";
10
+
11
+ export type BlendFactor =
12
+ | "one"
13
+ | "zero"
14
+ | "src-alpha"
15
+ | "one-minus-src-alpha"
16
+ | "dst-alpha"
17
+ | "one-minus-dst-alpha";
18
+ export type BlendOperation = "add" | "subtract" | "reverse-subtract";
19
+
20
+ export type BlendMode = {
21
+ color: {
22
+ srcFactor: BlendFactor;
23
+ dstFactor: BlendFactor;
24
+ operation: BlendOperation;
25
+ };
26
+ alpha: {
27
+ srcFactor: BlendFactor;
28
+ dstFactor: BlendFactor;
29
+ operation: BlendOperation;
30
+ };
31
+ };
32
+
33
+ /**
34
+ * The render backend interface abstracts WebGPU and WebGL differences.
35
+ *
36
+ * Implementations handle GPU-specific operations like texture management,
37
+ * shader creation, and frame lifecycle.
38
+ */
39
+ export interface IRenderBackend {
40
+ /** The type of backend ("webgpu" or "webgl2") */
41
+ readonly type: BackendType;
42
+
43
+ /** Engine limits (texture size, instance count, etc.) */
44
+ readonly limits: Limits;
45
+
46
+ /** Size of the default texture atlas */
47
+ readonly atlasSize: Size;
48
+
49
+ /** Default atlas ID (always "default") */
50
+ readonly defaultAtlasId: string;
51
+
52
+ /**
53
+ * Begin a new frame.
54
+ * WebGPU: Creates command encoder and render pass
55
+ * WebGL: Clears the canvas if loadOp is "clear"
56
+ */
57
+ startFrame(clearColor: Color, loadOp: "clear" | "load"): void;
58
+
59
+ /**
60
+ * End the current frame and submit to GPU.
61
+ * WebGPU: Ends render pass and submits command buffer
62
+ * WebGL: Flushes pending operations
63
+ */
64
+ endFrame(): void;
65
+
66
+ /**
67
+ * Update engine uniforms (view-projection matrix, resolution).
68
+ * Called once per frame before shader processing.
69
+ */
70
+ updateEngineUniform(uniform: EngineUniform): void;
71
+
72
+ /**
73
+ * Upload a CPU texture atlas to a GPU texture array layer.
74
+ * @param atlas - The CPU-side atlas data to upload
75
+ * @param layerIndex - Which layer in the texture array to upload to
76
+ * @param atlasId - Which atlas to upload to (default: "default")
77
+ */
78
+ uploadAtlas(
79
+ atlas: CpuTextureAtlas,
80
+ layerIndex: number,
81
+ atlasId?: string,
82
+ ): Promise<void>;
83
+
84
+ /**
85
+ * Create a new texture atlas.
86
+ * @param id - Unique identifier for this atlas
87
+ * @param options - Atlas configuration (format, layers, size)
88
+ */
89
+ createTextureAtlas(id: string, options?: TextureAtlasOptions): ITextureAtlas;
90
+
91
+ /**
92
+ * Get a texture atlas by ID.
93
+ * @param id - Atlas identifier or defaults to "default"
94
+ * @returns The atlas, or null if not found
95
+ */
96
+ getTextureAtlas(id?: string): ITextureAtlas | null;
97
+
98
+ /**
99
+ * Destroy a texture atlas and free GPU resources.
100
+ * @param id - Atlas identifier
101
+ */
102
+ destroyTextureAtlas(id: string): void;
103
+
104
+ /**
105
+ * Create a quad shader for instanced rendering.
106
+ */
107
+ createQuadShader(opts: QuadShaderCreationOpts): IBackendShader;
108
+
109
+ /**
110
+ * Handle canvas resize.
111
+ */
112
+ resize(width: number, height: number): void;
113
+
114
+ /**
115
+ * Clean up GPU resources.
116
+ */
117
+ destroy(): void;
118
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Supported texture atlas formats.
3
+ */
4
+ export type TextureAtlasFormat = "rgba8unorm" | "rg8unorm";
5
+
6
+ /**
7
+ * Options for creating a texture atlas.
8
+ */
9
+ export type TextureAtlasOptions = {
10
+ /** Texture format (default: "rgba8unorm") */
11
+ format?: TextureAtlasFormat;
12
+ /** Number of array layers (default: limits.textureArrayLayers) */
13
+ layers?: number;
14
+ /** Atlas size in pixels (default: limits.textureSize) */
15
+ size?: number;
16
+ };
17
+
18
+ /**
19
+ * Backend-agnostic texture atlas interface.
20
+ *
21
+ * Texture atlases are GPU texture arrays that store multiple textures.
22
+ * Each backend manages its atlases and provides this common interface.
23
+ */
24
+ export interface ITextureAtlas {
25
+ /** Unique identifier for this atlas */
26
+ readonly id: string;
27
+ /** Texture format */
28
+ readonly format: TextureAtlasFormat;
29
+ /** Number of array layers */
30
+ readonly layers: number;
31
+ /** Size in pixels (width = height) */
32
+ readonly size: number;
33
+ /** Underlying GPU handle (GPUTexture for WebGPU, WebGLTexture for WebGL) */
34
+ readonly handle: unknown;
35
+ }