@bloopjs/toodle 0.0.103 → 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 (128) hide show
  1. package/dist/Toodle.d.ts +50 -20
  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 +6741 -6151
  45. package/dist/mod.js.map +28 -23
  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/scene/SceneNode.d.ts +2 -2
  53. package/dist/scene/SceneNode.d.ts.map +1 -1
  54. package/dist/text/TextShader.d.ts +8 -6
  55. package/dist/text/TextShader.d.ts.map +1 -1
  56. package/dist/textures/AssetManager.d.ts +21 -5
  57. package/dist/textures/AssetManager.d.ts.map +1 -1
  58. package/dist/textures/types.d.ts +4 -2
  59. package/dist/textures/types.d.ts.map +1 -1
  60. package/dist/textures/util.d.ts.map +1 -1
  61. package/dist/utils/boilerplate.d.ts +1 -1
  62. package/dist/utils/boilerplate.d.ts.map +1 -1
  63. package/package.json +1 -1
  64. package/src/Toodle.ts +155 -171
  65. package/src/backends/IBackendShader.ts +52 -0
  66. package/src/backends/IRenderBackend.ts +118 -0
  67. package/src/backends/ITextureAtlas.ts +35 -0
  68. package/src/backends/detection.ts +46 -0
  69. package/src/backends/mod.ts +29 -0
  70. package/src/backends/webgl2/WebGLBackend.ts +256 -0
  71. package/src/backends/webgl2/WebGLQuadShader.ts +278 -0
  72. package/src/backends/webgl2/glsl/quad.glsl.ts +114 -0
  73. package/src/backends/webgl2/mod.ts +2 -0
  74. package/src/{textures → backends/webgpu}/TextureComputeShader.ts +2 -48
  75. package/src/backends/webgpu/WebGPUBackend.ts +350 -0
  76. package/src/{shaders/QuadShader.ts → backends/webgpu/WebGPUQuadShader.ts} +226 -170
  77. package/src/backends/webgpu/mod.ts +2 -0
  78. package/src/{shaders → backends/webgpu}/parser.ts +2 -2
  79. package/src/{shaders → backends/webgpu}/postprocess/blur.ts +2 -2
  80. package/src/{shaders → backends/webgpu}/postprocess/mod.ts +1 -1
  81. package/src/mod.ts +3 -2
  82. package/src/scene/Batcher.ts +3 -3
  83. package/src/scene/QuadNode.ts +7 -6
  84. package/src/scene/RenderComponent.ts +2 -2
  85. package/src/scene/SceneNode.ts +11 -12
  86. package/src/text/TextNode.ts +2 -2
  87. package/src/text/TextShader.ts +17 -11
  88. package/src/textures/AssetManager.ts +119 -94
  89. package/src/textures/types.ts +4 -2
  90. package/src/textures/util.ts +0 -65
  91. package/src/utils/boilerplate.ts +1 -1
  92. package/dist/shaders/EngineUniform.d.ts.map +0 -1
  93. package/dist/shaders/IShader.d.ts +0 -15
  94. package/dist/shaders/IShader.d.ts.map +0 -1
  95. package/dist/shaders/QuadShader.d.ts +0 -18
  96. package/dist/shaders/QuadShader.d.ts.map +0 -1
  97. package/dist/shaders/ShaderDescriptor.d.ts.map +0 -1
  98. package/dist/shaders/mod.d.ts +0 -6
  99. package/dist/shaders/mod.d.ts.map +0 -1
  100. package/dist/shaders/parser.d.ts.map +0 -1
  101. package/dist/shaders/postprocess/blur.d.ts.map +0 -1
  102. package/dist/shaders/postprocess/mod.d.ts.map +0 -1
  103. package/dist/shaders/samplers.d.ts.map +0 -1
  104. package/dist/shaders/wgsl/example.wgsl.d.ts.map +0 -1
  105. package/dist/shaders/wgsl/hello.wgsl.d.ts.map +0 -1
  106. package/dist/shaders/wgsl/helloInstanced.wgsl.d.ts.map +0 -1
  107. package/dist/shaders/wgsl/quad.wgsl.d.ts.map +0 -1
  108. package/dist/textures/TextureComputeShader.d.ts.map +0 -1
  109. package/dist/textures/pixel-scraping.wgsl.d.ts.map +0 -1
  110. package/src/shaders/IShader.ts +0 -20
  111. package/src/shaders/mod.ts +0 -5
  112. /package/dist/{shaders → backends/webgpu}/ShaderDescriptor.d.ts +0 -0
  113. /package/dist/{shaders → backends/webgpu}/parser.d.ts +0 -0
  114. /package/dist/{shaders → backends/webgpu}/samplers.d.ts +0 -0
  115. /package/dist/{shaders → backends/webgpu}/wgsl/example.wgsl.d.ts +0 -0
  116. /package/dist/{shaders → backends/webgpu}/wgsl/hello.wgsl.d.ts +0 -0
  117. /package/dist/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.d.ts +0 -0
  118. /package/dist/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.d.ts +0 -0
  119. /package/dist/{shaders → backends/webgpu}/wgsl/quad.wgsl.d.ts +0 -0
  120. /package/dist/{shaders → coreTypes}/EngineUniform.d.ts +0 -0
  121. /package/src/{shaders → backends/webgpu}/ShaderDescriptor.ts +0 -0
  122. /package/src/{shaders → backends/webgpu}/samplers.ts +0 -0
  123. /package/src/{shaders → backends/webgpu}/wgsl/example.wgsl.ts +0 -0
  124. /package/src/{shaders → backends/webgpu}/wgsl/hello.wgsl.ts +0 -0
  125. /package/src/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.ts +0 -0
  126. /package/src/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.ts +0 -0
  127. /package/src/{shaders → backends/webgpu}/wgsl/quad.wgsl.ts +0 -0
  128. /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
  /**
@@ -510,7 +429,7 @@ export class Toodle {
510
429
  */
511
430
  Quad(assetId: TextureId, options: QuadOptions = {}) {
512
431
  const assetManager = options.assetManager ?? this.assets;
513
- options.idealSize ??= assetManager.getSize(assetId);
432
+ options.size ??= assetManager.getSize(assetId);
514
433
  options.shader ??= this.#defaultQuadShader();
515
434
  options.atlasCoords ??= assetManager.extra.getAtlasCoords(assetId)[0];
516
435
  options.textureId ??= assetId;
@@ -578,7 +497,7 @@ export class Toodle {
578
497
  height: originalSize.height,
579
498
  };
580
499
 
581
- options.idealSize ??= {
500
+ options.size ??= {
582
501
  width: originalSize.width,
583
502
  height: originalSize.height,
584
503
  };
@@ -612,7 +531,7 @@ export class Toodle {
612
531
 
613
532
  shapes = {
614
533
  Rect: (options: QuadOptions = {}) => {
615
- options.idealSize ??= { width: 1, height: 1 };
534
+ options.size ??= { width: 1, height: 1 };
616
535
  options.shader ??= this.#defaultQuadShader();
617
536
  options.atlasCoords ??= {
618
537
  atlasIndex: 1000,
@@ -640,18 +559,25 @@ export class Toodle {
640
559
  return quad;
641
560
  },
642
561
 
643
- Circle: (options: QuadOptions = {}) => {
644
- options.idealSize ??= { width: 1, height: 1 };
645
- options.shader ??= this.#defaultQuadShader();
646
- options.atlasCoords ??= {
647
- atlasIndex: 1001,
648
- uvOffset: { x: 0, y: 0 },
649
- uvScale: { width: 0, height: 0 },
650
- cropOffset: { x: 0, y: 0 },
651
- originalSize: { width: 1, height: 1 },
562
+ Circle: (options: CircleOptions = { radius: 50 }) => {
563
+ const radius = options.radius ?? 50;
564
+ const diameter = radius * 2;
565
+
566
+ const quadOptions: QuadOptions = {
567
+ ...options,
568
+ size: { width: diameter, height: diameter },
569
+ shader: options.shader ?? this.#defaultQuadShader(),
570
+ atlasCoords: options.atlasCoords ?? {
571
+ atlasIndex: 1001,
572
+ uvOffset: { x: 0, y: 0 },
573
+ uvScale: { width: 0, height: 0 },
574
+ cropOffset: { x: 0, y: 0 },
575
+ originalSize: { width: 1, height: 1 },
576
+ },
577
+ assetManager: this.assets,
652
578
  };
653
- options.assetManager = this.assets;
654
- const quad = new QuadNode(options, this.#matrixPool);
579
+
580
+ const quad = new QuadNode(quadOptions, this.#matrixPool);
655
581
 
656
582
  if (options?.position) {
657
583
  quad.position = options.position;
@@ -693,7 +619,7 @@ export class Toodle {
693
619
  originalSize: { width: 1, height: 1 },
694
620
  },
695
621
  shader: options.shader ?? this.#defaultQuadShader(),
696
- idealSize: { width: 1, height: 1 },
622
+ size: { width: 1, height: 1 },
697
623
  layer: options.layer,
698
624
  key: options.key,
699
625
  rotationRadians: angle,
@@ -711,17 +637,18 @@ export class Toodle {
711
637
  },
712
638
  };
713
639
 
714
- #quadShader: QuadShader | null = null;
640
+ #quadShader: IBackendShader | null = null;
715
641
 
716
- #defaultQuadShader() {
642
+ #defaultQuadShader(): IBackendShader {
717
643
  if (this.#quadShader) {
718
644
  return this.#quadShader;
719
645
  }
720
646
 
721
- const shader = this.QuadShader(
722
- "default quad shader",
723
- this.#limits.instanceCount,
724
- /*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*/ `
725
652
  @fragment
726
653
  fn frag(vertex: VertexOutput) -> @location(0) vec4f {
727
654
  let color = default_fragment_shader(vertex, ${
@@ -731,8 +658,14 @@ export class Toodle {
731
658
  });
732
659
  return color;
733
660
  }
734
- `,
735
- );
661
+ `
662
+ : undefined;
663
+
664
+ const shader = this.#backend.createQuadShader({
665
+ label: "default quad shader",
666
+ instanceCount: this.limits.instanceCount,
667
+ userCode,
668
+ });
736
669
 
737
670
  this.#quadShader = shader;
738
671
  return shader;
@@ -754,11 +687,29 @@ export class Toodle {
754
687
  static async attach(canvas: HTMLCanvasElement, options?: ToodleOptions) {
755
688
  canvas.width = canvas.clientWidth * devicePixelRatio;
756
689
  canvas.height = canvas.clientHeight * devicePixelRatio;
757
- 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
+
758
711
  return new Toodle(
759
- device,
760
- context,
761
- presentationFormat,
712
+ backend,
762
713
  canvas,
763
714
  {
764
715
  width: canvas.clientWidth,
@@ -775,21 +726,22 @@ export class Toodle {
775
726
  */
776
727
  destroy() {
777
728
  this.#resizeObserver.disconnect();
778
- this.#device.destroy();
729
+ this.#backend.destroy();
779
730
  this.assets.destroy();
780
731
  }
781
732
 
782
733
  /**
783
- * 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
+ * }
784
741
  */
785
- extra = {
786
- /**
787
- * Get the GPU device used by this Toodle instance.
788
- */
789
- device: (): GPUDevice => {
790
- return this.#device;
791
- },
792
- };
742
+ get backend(): WebGPUBackend | WebGLBackend {
743
+ return this.#backend as WebGPUBackend | WebGLBackend;
744
+ }
793
745
  }
794
746
 
795
747
  export type StartFrameOptions = {
@@ -819,6 +771,27 @@ export type ToodleOptions = {
819
771
  */
820
772
  filter?: "nearest" | "linear";
821
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";
786
+ };
787
+
788
+ export type CircleOptions = Omit<QuadOptions, "size"> & {
789
+ /**
790
+ * The radius of the circle in pixels.
791
+ * The diameter will be radius * 2.
792
+ * @default 50
793
+ */
794
+ radius?: number;
822
795
  };
823
796
 
824
797
  export type LineOptions = {
@@ -841,7 +814,7 @@ export type LineOptions = {
841
814
  /**
842
815
  * The shader to use for the line.
843
816
  */
844
- shader?: IShader;
817
+ shader?: IBackendShader;
845
818
  /**
846
819
  * The layer to draw the line on.
847
820
  */
@@ -851,3 +824,14 @@ export type LineOptions = {
851
824
  */
852
825
  key?: string;
853
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
+ }