@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
@@ -0,0 +1,46 @@
1
+ import type { BackendType } from "./IRenderBackend";
2
+
3
+ /**
4
+ * Detect the best available rendering backend.
5
+ *
6
+ * Tries WebGPU first (better performance), falls back to WebGL 2.
7
+ */
8
+ export async function detectBackend(): Promise<BackendType> {
9
+ // Check for WebGPU support
10
+ if (typeof navigator !== "undefined" && "gpu" in navigator) {
11
+ try {
12
+ const adapter = await navigator.gpu.requestAdapter();
13
+ if (adapter) {
14
+ return "webgpu";
15
+ }
16
+ } catch {
17
+ // WebGPU initialization failed, fall back to WebGL
18
+ }
19
+ }
20
+
21
+ return "webgl2";
22
+ }
23
+
24
+ /**
25
+ * Check if WebGPU is available in the current environment.
26
+ */
27
+ export function isWebGPUAvailable(): boolean {
28
+ return typeof navigator !== "undefined" && "gpu" in navigator;
29
+ }
30
+
31
+ /**
32
+ * Check if WebGL 2 is available in the current environment.
33
+ */
34
+ export function isWebGL2Available(): boolean {
35
+ if (typeof document === "undefined") {
36
+ return false;
37
+ }
38
+
39
+ try {
40
+ const canvas = document.createElement("canvas");
41
+ const gl = canvas.getContext("webgl2");
42
+ return gl !== null;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
@@ -0,0 +1,29 @@
1
+ // Backend abstraction layer
2
+
3
+ export {
4
+ detectBackend,
5
+ isWebGL2Available,
6
+ isWebGPUAvailable,
7
+ } from "./detection";
8
+ export type { IBackendShader, QuadShaderCreationOpts } from "./IBackendShader";
9
+ export type {
10
+ BackendType,
11
+ BlendFactor,
12
+ BlendMode,
13
+ BlendOperation,
14
+ IRenderBackend,
15
+ } from "./IRenderBackend";
16
+ export type {
17
+ ITextureAtlas,
18
+ TextureAtlasFormat,
19
+ TextureAtlasOptions,
20
+ } from "./ITextureAtlas";
21
+ export { defaultFragmentShader as defaultGLSLFragmentShader } from "./webgl2/glsl/quad.glsl";
22
+ export { WebGLBackend } from "./webgl2/WebGLBackend";
23
+ // WebGPU-specific postprocess utilities
24
+ export {
25
+ type PostProcess,
26
+ PostProcessDefaults,
27
+ } from "./webgpu/postprocess/mod";
28
+ // Concrete backend implementations
29
+ export { WebGPUBackend } from "./webgpu/WebGPUBackend";
@@ -0,0 +1,256 @@
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, LimitsOptions } from "../../limits";
5
+ import { DEFAULT_LIMITS } from "../../limits";
6
+ import type { CpuTextureAtlas } from "../../textures/types";
7
+ import { assert } from "../../utils/assert";
8
+ import type { IBackendShader, QuadShaderCreationOpts } from "../IBackendShader";
9
+ import type { IRenderBackend } from "../IRenderBackend";
10
+ import type {
11
+ ITextureAtlas,
12
+ TextureAtlasFormat,
13
+ TextureAtlasOptions,
14
+ } from "../ITextureAtlas";
15
+ import { WebGLQuadShader } from "./WebGLQuadShader";
16
+
17
+ export type WebGLBackendOptions = {
18
+ limits?: LimitsOptions;
19
+ format?: "rgba8unorm" | "rg8unorm";
20
+ };
21
+
22
+ /**
23
+ * WebGL 2 implementation of the render backend.
24
+ */
25
+ export class WebGLBackend implements IRenderBackend {
26
+ readonly type = "webgl2" as const;
27
+ readonly limits: Limits;
28
+ readonly atlasSize: Size;
29
+ readonly defaultAtlasId = "default";
30
+
31
+ #atlases = new Map<string, ITextureAtlas>();
32
+ #gl: WebGL2RenderingContext;
33
+ #canvas: HTMLCanvasElement;
34
+
35
+ private constructor(
36
+ gl: WebGL2RenderingContext,
37
+ canvas: HTMLCanvasElement,
38
+ limits: Limits,
39
+ ) {
40
+ this.#gl = gl;
41
+ this.#canvas = canvas;
42
+ this.limits = limits;
43
+ this.atlasSize = {
44
+ width: limits.textureSize,
45
+ height: limits.textureSize,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Create a WebGL 2 backend attached to a canvas.
51
+ */
52
+ static async create(
53
+ canvas: HTMLCanvasElement,
54
+ options: WebGLBackendOptions = {},
55
+ ): Promise<WebGLBackend> {
56
+ const gl = canvas.getContext("webgl2", {
57
+ alpha: true,
58
+ antialias: false,
59
+ premultipliedAlpha: true,
60
+ });
61
+
62
+ if (!gl) {
63
+ throw new Error("WebGL 2 not supported");
64
+ }
65
+
66
+ const limits: Limits = {
67
+ ...DEFAULT_LIMITS,
68
+ ...options.limits,
69
+ };
70
+
71
+ const backend = new WebGLBackend(gl, canvas, limits);
72
+
73
+ // Create the default texture atlas
74
+ backend.createTextureAtlas("default", {
75
+ format: options.format ?? "rgba8unorm",
76
+ layers: limits.textureArrayLayers,
77
+ size: limits.textureSize,
78
+ });
79
+
80
+ return backend;
81
+ }
82
+
83
+ startFrame(clearColor: Color, loadOp: "clear" | "load"): void {
84
+ const gl = this.#gl;
85
+
86
+ // Set viewport to canvas size
87
+ gl.viewport(0, 0, this.#canvas.width, this.#canvas.height);
88
+
89
+ if (loadOp === "clear") {
90
+ gl.clearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
91
+ gl.clear(gl.COLOR_BUFFER_BIT);
92
+ }
93
+
94
+ // Enable blending
95
+ gl.enable(gl.BLEND);
96
+ gl.blendFuncSeparate(
97
+ gl.SRC_ALPHA,
98
+ gl.ONE_MINUS_SRC_ALPHA,
99
+ gl.ONE,
100
+ gl.ONE_MINUS_SRC_ALPHA,
101
+ );
102
+ }
103
+
104
+ endFrame(): void {
105
+ const gl = this.#gl;
106
+ gl.flush();
107
+ }
108
+
109
+ updateEngineUniform(_uniform: EngineUniform): void {
110
+ // Uniforms are updated per-shader in WebGL, not at the backend level
111
+ }
112
+
113
+ async uploadAtlas(
114
+ atlas: CpuTextureAtlas,
115
+ layerIndex: number,
116
+ atlasId?: string,
117
+ ): Promise<void> {
118
+ const gl = this.#gl;
119
+ const targetAtlas = this.getTextureAtlas(atlasId ?? "default");
120
+ assert(targetAtlas, `Atlas "${atlasId ?? "default"}" not found`);
121
+ const texture = targetAtlas.handle as WebGLTexture;
122
+
123
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
124
+
125
+ if (atlas.rg8Bytes) {
126
+ // Upload raw bytes for RG8 format
127
+ gl.texSubImage3D(
128
+ gl.TEXTURE_2D_ARRAY,
129
+ 0, // mip level
130
+ 0,
131
+ 0,
132
+ layerIndex, // x, y, z offset
133
+ targetAtlas.size,
134
+ targetAtlas.size,
135
+ 1, // width, height, depth
136
+ gl.RG,
137
+ gl.UNSIGNED_BYTE,
138
+ atlas.rg8Bytes,
139
+ );
140
+ } else {
141
+ // Upload ImageBitmap for RGBA format
142
+ gl.texSubImage3D(
143
+ gl.TEXTURE_2D_ARRAY,
144
+ 0,
145
+ 0,
146
+ 0,
147
+ layerIndex,
148
+ atlas.texture.width,
149
+ atlas.texture.height,
150
+ 1,
151
+ gl.RGBA,
152
+ gl.UNSIGNED_BYTE,
153
+ atlas.texture,
154
+ );
155
+ }
156
+
157
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
158
+ }
159
+
160
+ createQuadShader(opts: QuadShaderCreationOpts): IBackendShader {
161
+ return new WebGLQuadShader(
162
+ opts.label,
163
+ this,
164
+ opts.instanceCount,
165
+ opts.userCode,
166
+ opts.atlasId,
167
+ );
168
+ }
169
+
170
+ createTextureAtlas(id: string, options?: TextureAtlasOptions): ITextureAtlas {
171
+ if (this.#atlases.has(id)) {
172
+ throw new Error(`Atlas "${id}" already exists`);
173
+ }
174
+
175
+ const gl = this.#gl;
176
+ const format: TextureAtlasFormat = options?.format ?? "rgba8unorm";
177
+ const layers = options?.layers ?? this.limits.textureArrayLayers;
178
+ const size = options?.size ?? this.limits.textureSize;
179
+
180
+ const texture = gl.createTexture();
181
+ assert(texture, "Failed to create WebGL texture");
182
+
183
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
184
+
185
+ // Configure texture parameters
186
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
187
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
188
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
189
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
190
+
191
+ // Allocate storage for texture array
192
+ const internalFormat = format === "rg8unorm" ? gl.RG8 : gl.RGBA8;
193
+
194
+ gl.texStorage3D(
195
+ gl.TEXTURE_2D_ARRAY,
196
+ 1, // mip levels
197
+ internalFormat,
198
+ size,
199
+ size,
200
+ layers,
201
+ );
202
+
203
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
204
+
205
+ const atlas: ITextureAtlas = { id, format, layers, size, handle: texture };
206
+ this.#atlases.set(id, atlas);
207
+ return atlas;
208
+ }
209
+
210
+ getTextureAtlas(id: string): ITextureAtlas | null {
211
+ return this.#atlases.get(id) ?? null;
212
+ }
213
+
214
+ destroyTextureAtlas(id: string): void {
215
+ const atlas = this.#atlases.get(id);
216
+ if (atlas) {
217
+ this.#gl.deleteTexture(atlas.handle as WebGLTexture);
218
+ this.#atlases.delete(id);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Get the default texture atlas handle.
224
+ * @deprecated Use getTextureAtlas("default").handle instead
225
+ */
226
+ get textureArrayHandle(): WebGLTexture {
227
+ return this.getTextureAtlas("default")!.handle as WebGLTexture;
228
+ }
229
+
230
+ resize(_width: number, _height: number): void {
231
+ // Canvas resize is handled by the application
232
+ }
233
+
234
+ destroy(): void {
235
+ const gl = this.#gl;
236
+ // Destroy all atlases
237
+ for (const atlas of this.#atlases.values()) {
238
+ gl.deleteTexture(atlas.handle as WebGLTexture);
239
+ }
240
+ this.#atlases.clear();
241
+ }
242
+
243
+ /**
244
+ * Get the WebGL 2 rendering context.
245
+ */
246
+ get gl(): WebGL2RenderingContext {
247
+ return this.#gl;
248
+ }
249
+
250
+ /**
251
+ * Get the presentation format (of the default atlas).
252
+ */
253
+ get presentationFormat(): TextureAtlasFormat {
254
+ return this.getTextureAtlas("default")?.format ?? "rgba8unorm";
255
+ }
256
+ }
@@ -0,0 +1,278 @@
1
+ import type { EngineUniform } from "../../coreTypes/EngineUniform";
2
+ import type { SceneNode } from "../../scene/SceneNode";
3
+ import { assert } from "../../utils/assert";
4
+ import type { IBackendShader } from "../IBackendShader";
5
+ import type { ITextureAtlas } from "../ITextureAtlas";
6
+ import { fragmentShader, vertexShader } from "./glsl/quad.glsl";
7
+ import type { WebGLBackend } from "./WebGLBackend";
8
+
9
+ // Instance data size in floats (must match WGSL shader)
10
+ // model (12) + tint (4) + uvOffsetAndScale (4) + cropOffsetAndScale (4) + atlasIndex (1) + padding (3) = 28
11
+ const INSTANCE_FLOATS = 28;
12
+ const INSTANCE_BYTES = INSTANCE_FLOATS * Float32Array.BYTES_PER_ELEMENT;
13
+
14
+ /**
15
+ * WebGL 2 implementation of quad shader for instanced rendering.
16
+ */
17
+ export class WebGLQuadShader implements IBackendShader {
18
+ readonly label: string;
19
+
20
+ #backend: WebGLBackend;
21
+ #atlas: ITextureAtlas;
22
+ #program: WebGLProgram;
23
+ #vao: WebGLVertexArrayObject;
24
+ #instanceBuffer: WebGLBuffer;
25
+ #cpuBuffer: Float32Array;
26
+ #instanceCount: number;
27
+ #instanceIndex = 0;
28
+
29
+ // Uniform locations
30
+ #uViewProjection: WebGLUniformLocation | null = null;
31
+ #uResolution: WebGLUniformLocation | null = null;
32
+ #uTextureArray: WebGLUniformLocation | null = null;
33
+
34
+ constructor(
35
+ label: string,
36
+ backend: WebGLBackend,
37
+ instanceCount: number,
38
+ userFragmentShader?: string,
39
+ atlasId?: string,
40
+ ) {
41
+ const atlas = backend.getTextureAtlas(atlasId ?? "default");
42
+ if (!atlas) {
43
+ throw new Error(`Atlas "${atlasId ?? "default"}" not found`);
44
+ }
45
+
46
+ this.#atlas = atlas;
47
+ this.label = label;
48
+ this.#backend = backend;
49
+ this.#instanceCount = instanceCount;
50
+
51
+ const gl = backend.gl;
52
+
53
+ // Compile shaders
54
+ const vs = this.#compileShader(gl, gl.VERTEX_SHADER, vertexShader);
55
+ const fs = this.#compileShader(
56
+ gl,
57
+ gl.FRAGMENT_SHADER,
58
+ userFragmentShader ?? fragmentShader,
59
+ );
60
+
61
+ // Create program
62
+ const program = gl.createProgram();
63
+ assert(program, "Failed to create WebGL program");
64
+ gl.attachShader(program, vs);
65
+ gl.attachShader(program, fs);
66
+ gl.linkProgram(program);
67
+
68
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
69
+ const info = gl.getProgramInfoLog(program);
70
+ throw new Error(`Failed to link shader program: ${info}`);
71
+ }
72
+
73
+ this.#program = program;
74
+
75
+ // Get uniform locations
76
+ this.#uViewProjection = gl.getUniformLocation(program, "u_viewProjection");
77
+ this.#uResolution = gl.getUniformLocation(program, "u_resolution");
78
+ this.#uTextureArray = gl.getUniformLocation(program, "u_textureArray");
79
+
80
+ // Create VAO
81
+ const vao = gl.createVertexArray();
82
+ assert(vao, "Failed to create WebGL VAO");
83
+ this.#vao = vao;
84
+
85
+ // Create instance buffer
86
+ const instanceBuffer = gl.createBuffer();
87
+ assert(instanceBuffer, "Failed to create WebGL instance buffer");
88
+ this.#instanceBuffer = instanceBuffer;
89
+
90
+ // Allocate CPU buffer
91
+ this.#cpuBuffer = new Float32Array(instanceCount * INSTANCE_FLOATS);
92
+
93
+ // Set up VAO
94
+ gl.bindVertexArray(vao);
95
+ gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
96
+ gl.bufferData(
97
+ gl.ARRAY_BUFFER,
98
+ instanceCount * INSTANCE_BYTES,
99
+ gl.DYNAMIC_DRAW,
100
+ );
101
+
102
+ // Set up instance attributes
103
+ // Each vec4 attribute takes up 16 bytes
104
+ // model0 at location 0
105
+ gl.enableVertexAttribArray(0);
106
+ gl.vertexAttribPointer(0, 4, gl.FLOAT, false, INSTANCE_BYTES, 0);
107
+ gl.vertexAttribDivisor(0, 1);
108
+
109
+ // model1 at location 1
110
+ gl.enableVertexAttribArray(1);
111
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, INSTANCE_BYTES, 16);
112
+ gl.vertexAttribDivisor(1, 1);
113
+
114
+ // model2 at location 2
115
+ gl.enableVertexAttribArray(2);
116
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, INSTANCE_BYTES, 32);
117
+ gl.vertexAttribDivisor(2, 1);
118
+
119
+ // tint at location 3
120
+ gl.enableVertexAttribArray(3);
121
+ gl.vertexAttribPointer(3, 4, gl.FLOAT, false, INSTANCE_BYTES, 48);
122
+ gl.vertexAttribDivisor(3, 1);
123
+
124
+ // uvOffsetAndScale at location 4
125
+ gl.enableVertexAttribArray(4);
126
+ gl.vertexAttribPointer(4, 4, gl.FLOAT, false, INSTANCE_BYTES, 64);
127
+ gl.vertexAttribDivisor(4, 1);
128
+
129
+ // cropOffsetAndScale at location 5
130
+ gl.enableVertexAttribArray(5);
131
+ gl.vertexAttribPointer(5, 4, gl.FLOAT, false, INSTANCE_BYTES, 80);
132
+ gl.vertexAttribDivisor(5, 1);
133
+
134
+ // atlasIndex at location 6 (integer attribute - use vertexAttribIPointer)
135
+ gl.enableVertexAttribArray(6);
136
+ gl.vertexAttribIPointer(6, 1, gl.UNSIGNED_INT, INSTANCE_BYTES, 96);
137
+ gl.vertexAttribDivisor(6, 1);
138
+
139
+ gl.bindVertexArray(null);
140
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
141
+
142
+ // Cleanup shaders (they're linked to the program now)
143
+ gl.deleteShader(vs);
144
+ gl.deleteShader(fs);
145
+ }
146
+
147
+ #cachedUniform: EngineUniform | null = null;
148
+
149
+ startFrame(uniform: EngineUniform): void {
150
+ this.#instanceIndex = 0;
151
+ // Cache uniform for use in processBatch - WebGL state is global,
152
+ // so we need to set program/uniforms/textures right before drawing
153
+ this.#cachedUniform = uniform;
154
+ }
155
+
156
+ #bindState(): void {
157
+ const gl = this.#backend.gl;
158
+ const uniform = this.#cachedUniform;
159
+ if (!uniform)
160
+ throw new Error("Tried to bind state but engine uniform is not set");
161
+
162
+ gl.useProgram(this.#program);
163
+
164
+ // Set uniforms
165
+ if (this.#uViewProjection) {
166
+ // wgpu-matrix mat3 is stored as 12 floats (3 columns × 4 floats with padding)
167
+ // WebGL uniformMatrix3fv expects 9 floats, so extract the relevant values
168
+ const m = uniform.viewProjectionMatrix;
169
+ const mat3x3 = new Float32Array([
170
+ m[0],
171
+ m[1],
172
+ m[2], // column 0
173
+ m[4],
174
+ m[5],
175
+ m[6], // column 1
176
+ m[8],
177
+ m[9],
178
+ m[10], // column 2
179
+ ]);
180
+ gl.uniformMatrix3fv(this.#uViewProjection, false, mat3x3);
181
+ }
182
+
183
+ if (this.#uResolution) {
184
+ gl.uniform2f(
185
+ this.#uResolution,
186
+ uniform.resolution.width,
187
+ uniform.resolution.height,
188
+ );
189
+ }
190
+
191
+ // Bind texture array to texture unit 0
192
+ gl.activeTexture(gl.TEXTURE0);
193
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.#atlas.handle as WebGLTexture);
194
+ if (this.#uTextureArray) {
195
+ gl.uniform1i(this.#uTextureArray, 0);
196
+ }
197
+ }
198
+
199
+ processBatch(nodes: SceneNode[]): number {
200
+ const gl = this.#backend.gl;
201
+
202
+ // Bind program, uniforms, and textures right before drawing
203
+ // (WebGL state is global, so this must happen per-batch)
204
+ this.#bindState();
205
+
206
+ if (nodes.length > this.#instanceCount) {
207
+ throw new Error(
208
+ `ToodleInstanceCap: ${nodes.length} instances enqueued, max is ${this.#instanceCount} for ${this.label} shader`,
209
+ );
210
+ }
211
+
212
+ let instanceCount = 0;
213
+
214
+ // WebGL2 doesn't support firstInstance in drawArraysInstanced,
215
+ // so we always write starting at offset 0 for each batch
216
+ for (let i = 0; i < nodes.length; i++) {
217
+ const instance = nodes[i];
218
+ assert(instance.renderComponent, "instance has no render component");
219
+ const floatOffset = instanceCount * INSTANCE_FLOATS;
220
+
221
+ instanceCount += instance.renderComponent.writeInstance(
222
+ instance,
223
+ this.#cpuBuffer,
224
+ floatOffset,
225
+ );
226
+ }
227
+
228
+ // Upload instance data to GPU starting at offset 0
229
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.#instanceBuffer);
230
+
231
+ gl.bufferSubData(
232
+ gl.ARRAY_BUFFER,
233
+ 0,
234
+ this.#cpuBuffer,
235
+ 0,
236
+ instanceCount * INSTANCE_FLOATS,
237
+ );
238
+
239
+ // Draw instances from offset 0
240
+ gl.bindVertexArray(this.#vao);
241
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount);
242
+ gl.bindVertexArray(null);
243
+
244
+ return 1;
245
+ }
246
+
247
+ endFrame(): void {
248
+ // Nothing to do
249
+ }
250
+
251
+ #compileShader(
252
+ gl: WebGL2RenderingContext,
253
+ type: number,
254
+ source: string,
255
+ ): WebGLShader {
256
+ const shader = gl.createShader(type);
257
+ assert(shader, "Failed to create WebGL shader");
258
+
259
+ gl.shaderSource(shader, source);
260
+ gl.compileShader(shader);
261
+
262
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
263
+ const info = gl.getShaderInfoLog(shader);
264
+ const typeStr = type === gl.VERTEX_SHADER ? "vertex" : "fragment";
265
+ gl.deleteShader(shader);
266
+ throw new Error(`Failed to compile ${typeStr} shader: ${info}`);
267
+ }
268
+
269
+ return shader;
270
+ }
271
+
272
+ destroy(): void {
273
+ const gl = this.#backend.gl;
274
+ gl.deleteProgram(this.#program);
275
+ gl.deleteVertexArray(this.#vao);
276
+ gl.deleteBuffer(this.#instanceBuffer);
277
+ }
278
+ }