@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.
- package/dist/Toodle.d.ts +41 -19
- package/dist/Toodle.d.ts.map +1 -1
- package/dist/backends/IBackendShader.d.ts +48 -0
- package/dist/backends/IBackendShader.d.ts.map +1 -0
- package/dist/backends/IRenderBackend.d.ts +92 -0
- package/dist/backends/IRenderBackend.d.ts.map +1 -0
- package/dist/backends/ITextureAtlas.d.ts +34 -0
- package/dist/backends/ITextureAtlas.d.ts.map +1 -0
- package/dist/backends/detection.d.ts +16 -0
- package/dist/backends/detection.d.ts.map +1 -0
- package/dist/backends/mod.d.ts +9 -0
- package/dist/backends/mod.d.ts.map +1 -0
- package/dist/backends/webgl2/WebGLBackend.d.ts +51 -0
- package/dist/backends/webgl2/WebGLBackend.d.ts.map +1 -0
- package/dist/backends/webgl2/WebGLQuadShader.d.ts +17 -0
- package/dist/backends/webgl2/WebGLQuadShader.d.ts.map +1 -0
- package/dist/backends/webgl2/glsl/quad.glsl.d.ts +12 -0
- package/dist/backends/webgl2/glsl/quad.glsl.d.ts.map +1 -0
- package/dist/backends/webgl2/mod.d.ts +3 -0
- package/dist/backends/webgl2/mod.d.ts.map +1 -0
- package/dist/backends/webgpu/ShaderDescriptor.d.ts.map +1 -0
- package/dist/{textures → backends/webgpu}/TextureComputeShader.d.ts +1 -1
- package/dist/backends/webgpu/TextureComputeShader.d.ts.map +1 -0
- package/dist/backends/webgpu/WebGPUBackend.d.ts +67 -0
- package/dist/backends/webgpu/WebGPUBackend.d.ts.map +1 -0
- package/dist/backends/webgpu/WebGPUQuadShader.d.ts +18 -0
- package/dist/backends/webgpu/WebGPUQuadShader.d.ts.map +1 -0
- package/dist/backends/webgpu/mod.d.ts +3 -0
- package/dist/backends/webgpu/mod.d.ts.map +1 -0
- package/dist/backends/webgpu/parser.d.ts.map +1 -0
- package/dist/{shaders → backends/webgpu}/postprocess/blur.d.ts +1 -1
- package/dist/backends/webgpu/postprocess/blur.d.ts.map +1 -0
- package/dist/{shaders → backends/webgpu}/postprocess/mod.d.ts +1 -1
- package/dist/backends/webgpu/postprocess/mod.d.ts.map +1 -0
- package/dist/backends/webgpu/samplers.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/example.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/hello.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/helloInstanced.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/pixel-scraping.wgsl.d.ts.map +1 -0
- package/dist/backends/webgpu/wgsl/quad.wgsl.d.ts.map +1 -0
- package/dist/coreTypes/EngineUniform.d.ts.map +1 -0
- package/dist/mod.d.ts +3 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +7247 -6663
- package/dist/mod.js.map +27 -22
- package/dist/scene/Batcher.d.ts +2 -2
- package/dist/scene/Batcher.d.ts.map +1 -1
- package/dist/scene/QuadNode.d.ts +3 -2
- package/dist/scene/QuadNode.d.ts.map +1 -1
- package/dist/scene/RenderComponent.d.ts +2 -2
- package/dist/scene/RenderComponent.d.ts.map +1 -1
- package/dist/text/TextShader.d.ts +8 -6
- package/dist/text/TextShader.d.ts.map +1 -1
- package/dist/textures/AssetManager.d.ts +21 -5
- package/dist/textures/AssetManager.d.ts.map +1 -1
- package/dist/textures/util.d.ts.map +1 -1
- package/dist/utils/boilerplate.d.ts +1 -1
- package/dist/utils/boilerplate.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Toodle.ts +124 -156
- package/src/backends/IBackendShader.ts +52 -0
- package/src/backends/IRenderBackend.ts +118 -0
- package/src/backends/ITextureAtlas.ts +35 -0
- package/src/backends/detection.ts +46 -0
- package/src/backends/mod.ts +29 -0
- package/src/backends/webgl2/WebGLBackend.ts +256 -0
- package/src/backends/webgl2/WebGLQuadShader.ts +278 -0
- package/src/backends/webgl2/glsl/quad.glsl.ts +114 -0
- package/src/backends/webgl2/mod.ts +2 -0
- package/src/{textures → backends/webgpu}/TextureComputeShader.ts +2 -48
- package/src/backends/webgpu/WebGPUBackend.ts +350 -0
- package/src/{shaders/QuadShader.ts → backends/webgpu/WebGPUQuadShader.ts} +226 -170
- package/src/backends/webgpu/mod.ts +2 -0
- package/src/{shaders → backends/webgpu}/parser.ts +2 -2
- package/src/{shaders → backends/webgpu}/postprocess/blur.ts +2 -2
- package/src/{shaders → backends/webgpu}/postprocess/mod.ts +1 -1
- package/src/mod.ts +3 -2
- package/src/scene/Batcher.ts +3 -3
- package/src/scene/QuadNode.ts +6 -2
- package/src/scene/RenderComponent.ts +2 -2
- package/src/text/TextShader.ts +17 -11
- package/src/textures/AssetManager.ts +117 -93
- package/src/textures/util.ts +0 -65
- package/src/utils/boilerplate.ts +1 -1
- package/dist/shaders/EngineUniform.d.ts.map +0 -1
- package/dist/shaders/IShader.d.ts +0 -15
- package/dist/shaders/IShader.d.ts.map +0 -1
- package/dist/shaders/QuadShader.d.ts +0 -18
- package/dist/shaders/QuadShader.d.ts.map +0 -1
- package/dist/shaders/ShaderDescriptor.d.ts.map +0 -1
- package/dist/shaders/mod.d.ts +0 -6
- package/dist/shaders/mod.d.ts.map +0 -1
- package/dist/shaders/parser.d.ts.map +0 -1
- package/dist/shaders/postprocess/blur.d.ts.map +0 -1
- package/dist/shaders/postprocess/mod.d.ts.map +0 -1
- package/dist/shaders/samplers.d.ts.map +0 -1
- package/dist/shaders/wgsl/example.wgsl.d.ts.map +0 -1
- package/dist/shaders/wgsl/hello.wgsl.d.ts.map +0 -1
- package/dist/shaders/wgsl/helloInstanced.wgsl.d.ts.map +0 -1
- package/dist/shaders/wgsl/quad.wgsl.d.ts.map +0 -1
- package/dist/textures/TextureComputeShader.d.ts.map +0 -1
- package/dist/textures/pixel-scraping.wgsl.d.ts.map +0 -1
- package/src/shaders/IShader.ts +0 -20
- package/src/shaders/mod.ts +0 -5
- /package/dist/{shaders → backends/webgpu}/ShaderDescriptor.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/parser.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/samplers.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/example.wgsl.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/hello.wgsl.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.d.ts +0 -0
- /package/dist/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.d.ts +0 -0
- /package/dist/{shaders → backends/webgpu}/wgsl/quad.wgsl.d.ts +0 -0
- /package/dist/{shaders → coreTypes}/EngineUniform.d.ts +0 -0
- /package/src/{shaders → backends/webgpu}/ShaderDescriptor.ts +0 -0
- /package/src/{shaders → backends/webgpu}/samplers.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/example.wgsl.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/hello.wgsl.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/helloInstanced.wgsl.ts +0 -0
- /package/src/{textures → backends/webgpu/wgsl}/pixel-scraping.wgsl.ts +0 -0
- /package/src/{shaders → backends/webgpu}/wgsl/quad.wgsl.ts +0 -0
- /package/src/{shaders → coreTypes}/EngineUniform.ts +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const vertexShader = /*glsl*/ `#version 300 es
|
|
2
|
+
precision highp float;
|
|
3
|
+
|
|
4
|
+
// Engine uniforms
|
|
5
|
+
uniform mat3 u_viewProjection;
|
|
6
|
+
uniform vec2 u_resolution;
|
|
7
|
+
|
|
8
|
+
// Instance data attributes
|
|
9
|
+
// location 0-2 are the model matrix for this instanced quad (mat3 as 3 vec3s)
|
|
10
|
+
layout(location = 0) in vec4 a_model0;
|
|
11
|
+
layout(location = 1) in vec4 a_model1;
|
|
12
|
+
layout(location = 2) in vec4 a_model2;
|
|
13
|
+
// location 3 is the tint color
|
|
14
|
+
layout(location = 3) in vec4 a_tint;
|
|
15
|
+
// location 4 is the uv offset and scale
|
|
16
|
+
layout(location = 4) in vec4 a_uvOffsetAndScale;
|
|
17
|
+
// location 5 is the crop offset and scale
|
|
18
|
+
layout(location = 5) in vec4 a_cropOffsetAndScale;
|
|
19
|
+
// location 6 is the atlas index (integer attribute)
|
|
20
|
+
layout(location = 6) in uint a_atlasIndex;
|
|
21
|
+
|
|
22
|
+
// Outputs to fragment shader
|
|
23
|
+
out vec4 v_uv; // xy = atlas uv, zw = original uv
|
|
24
|
+
out vec4 v_tint;
|
|
25
|
+
flat out int v_atlasIndex;
|
|
26
|
+
|
|
27
|
+
// Lookup tables for unit quad positions and UVs
|
|
28
|
+
const vec2 posLookup[4] = vec2[4](
|
|
29
|
+
vec2(-0.5, 0.5),
|
|
30
|
+
vec2(-0.5, -0.5),
|
|
31
|
+
vec2(0.5, 0.5),
|
|
32
|
+
vec2(0.5, -0.5)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const vec2 uvLookup[4] = vec2[4](
|
|
36
|
+
vec2(0.0, 0.0),
|
|
37
|
+
vec2(0.0, 1.0),
|
|
38
|
+
vec2(1.0, 0.0),
|
|
39
|
+
vec2(1.0, 1.0)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
void main() {
|
|
43
|
+
// Reconstruct model matrix from instance data
|
|
44
|
+
mat3 modelMatrix = mat3(a_model0.xyz, a_model1.xyz, a_model2.xyz);
|
|
45
|
+
|
|
46
|
+
// Transform vertex position
|
|
47
|
+
vec2 localPosition = posLookup[gl_VertexID];
|
|
48
|
+
vec2 cropOffset = a_cropOffsetAndScale.xy;
|
|
49
|
+
vec2 cropScale = a_cropOffsetAndScale.zw;
|
|
50
|
+
vec2 croppedPosition = localPosition * cropScale + cropOffset;
|
|
51
|
+
vec3 worldPosition = modelMatrix * vec3(croppedPosition, 1.0);
|
|
52
|
+
vec3 clipPosition = u_viewProjection * worldPosition;
|
|
53
|
+
gl_Position = vec4(clipPosition.xy, 0.0, 1.0);
|
|
54
|
+
|
|
55
|
+
// Set UV coordinates
|
|
56
|
+
vec2 originalUv = uvLookup[gl_VertexID];
|
|
57
|
+
vec2 atlasUv = originalUv * a_uvOffsetAndScale.zw * cropScale + a_uvOffsetAndScale.xy;
|
|
58
|
+
v_uv = vec4(atlasUv, originalUv);
|
|
59
|
+
|
|
60
|
+
// Pass through tint and atlas index
|
|
61
|
+
v_tint = a_tint;
|
|
62
|
+
v_atlasIndex = int(a_atlasIndex);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Default fragment shader for WebGL2 quad rendering.
|
|
69
|
+
* Custom fragment shaders must follow the same contract:
|
|
70
|
+
* - Required uniforms: u_resolution, u_textureArray
|
|
71
|
+
* - Required inputs: v_uv (vec4), v_tint (vec4), v_atlasIndex (flat int)
|
|
72
|
+
* - Required output: fragColor (vec4)
|
|
73
|
+
*/
|
|
74
|
+
export const fragmentShader = /*glsl*/ `#version 300 es
|
|
75
|
+
precision highp float;
|
|
76
|
+
precision highp sampler2DArray;
|
|
77
|
+
|
|
78
|
+
// Engine uniforms
|
|
79
|
+
uniform vec2 u_resolution;
|
|
80
|
+
|
|
81
|
+
// Texture array sampler
|
|
82
|
+
uniform sampler2DArray u_textureArray;
|
|
83
|
+
|
|
84
|
+
// Inputs from vertex shader
|
|
85
|
+
in vec4 v_uv; // xy = atlas uv, zw = original uv
|
|
86
|
+
in vec4 v_tint;
|
|
87
|
+
flat in int v_atlasIndex;
|
|
88
|
+
|
|
89
|
+
// Output color
|
|
90
|
+
out vec4 fragColor;
|
|
91
|
+
|
|
92
|
+
void main() {
|
|
93
|
+
vec2 atlasUv = v_uv.xy;
|
|
94
|
+
vec2 originalUv = v_uv.zw;
|
|
95
|
+
|
|
96
|
+
if (v_atlasIndex == 1000) {
|
|
97
|
+
// Rectangle - return solid color
|
|
98
|
+
fragColor = vec4(1.0, 1.0, 1.0, 1.0) * v_tint;
|
|
99
|
+
} else if (v_atlasIndex == 1001) {
|
|
100
|
+
// Circle
|
|
101
|
+
float edgeWidth = 4.0 / max(u_resolution.x, u_resolution.y);
|
|
102
|
+
float centerDistance = 2.0 * distance(vec2(0.5, 0.5), originalUv);
|
|
103
|
+
float alpha = 1.0 - smoothstep(1.0 - edgeWidth, 1.0 + edgeWidth, centerDistance);
|
|
104
|
+
fragColor = vec4(v_tint.rgb, alpha * v_tint.a);
|
|
105
|
+
} else {
|
|
106
|
+
// Texture - sample from texture array
|
|
107
|
+
vec4 color = texture(u_textureArray, vec3(atlasUv, float(v_atlasIndex)));
|
|
108
|
+
fragColor = color * v_tint;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
/** Alias for users who want to extend the default shader */
|
|
114
|
+
export const defaultFragmentShader = fragmentShader;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type { TextureWithMetadata } from "../../textures/types";
|
|
2
|
+
import computeShader from "./wgsl/pixel-scraping.wgsl";
|
|
3
3
|
|
|
4
4
|
// Constants
|
|
5
5
|
const BOUNDING_BOX_SIZE = 4 * Uint32Array.BYTES_PER_ELEMENT;
|
|
6
6
|
const WORKGROUP_SIZE = 8;
|
|
7
7
|
const MAX_BOUND = 0xffffffff;
|
|
8
8
|
const MIN_BOUND = 0x00000000;
|
|
9
|
-
const BYTES_PER_PIXEL = 4;
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* The data returned by the compute shader that represents the opaque pixels in a texture.
|
|
@@ -260,51 +259,6 @@ export class TextureComputeShader {
|
|
|
260
259
|
};
|
|
261
260
|
}
|
|
262
261
|
|
|
263
|
-
/**
|
|
264
|
-
* Converts a GPUTexture to an ImageBitmap for display or further use.
|
|
265
|
-
*/
|
|
266
|
-
async #textureToBitmap(
|
|
267
|
-
texture: GPUTexture,
|
|
268
|
-
width: number,
|
|
269
|
-
height: number,
|
|
270
|
-
): Promise<ImageBitmap> {
|
|
271
|
-
const paddedBytesPerRow = Math.ceil((width * BYTES_PER_PIXEL) / 256) * 256;
|
|
272
|
-
const bufferSize = paddedBytesPerRow * height;
|
|
273
|
-
|
|
274
|
-
const readBuffer = this.#device.createBuffer({
|
|
275
|
-
size: bufferSize,
|
|
276
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const encoder = this.#device.createCommandEncoder();
|
|
280
|
-
encoder.copyTextureToBuffer(
|
|
281
|
-
{ texture },
|
|
282
|
-
{ buffer: readBuffer, bytesPerRow: paddedBytesPerRow },
|
|
283
|
-
{ width, height, depthOrArrayLayers: 1 },
|
|
284
|
-
);
|
|
285
|
-
this.#device.queue.submit([encoder.finish()]);
|
|
286
|
-
|
|
287
|
-
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
288
|
-
const raw = readBuffer.getMappedRange();
|
|
289
|
-
const rawArray = new Uint8Array(raw);
|
|
290
|
-
|
|
291
|
-
const pixelData = new Uint8ClampedArray(width * height * 4);
|
|
292
|
-
for (let y = 0; y < height; y++) {
|
|
293
|
-
const src = y * paddedBytesPerRow;
|
|
294
|
-
const dst = y * width * 4;
|
|
295
|
-
pixelData.set(rawArray.subarray(src, src + width * 4), dst);
|
|
296
|
-
}
|
|
297
|
-
readBuffer.unmap();
|
|
298
|
-
|
|
299
|
-
const canvas = document.createElement("canvas");
|
|
300
|
-
canvas.width = width;
|
|
301
|
-
canvas.height = height;
|
|
302
|
-
const ctx = canvas.getContext("2d")!;
|
|
303
|
-
ctx.putImageData(new ImageData(pixelData, width, height), 0, 0);
|
|
304
|
-
|
|
305
|
-
return await createImageBitmap(canvas);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
262
|
// Bind group helpers
|
|
309
263
|
|
|
310
264
|
#boundsBindGroup(inputTexture: GPUTexture): GPUBindGroup {
|
|
@@ -0,0 +1,350 @@
|
|
|
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 type { PostProcess } from "./postprocess/mod";
|
|
16
|
+
import { WebGPUQuadShader } from "./WebGPUQuadShader";
|
|
17
|
+
|
|
18
|
+
export type WebGPUBackendOptions = {
|
|
19
|
+
limits?: LimitsOptions;
|
|
20
|
+
format?: "rgba8unorm" | "rg8unorm";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* WebGPU implementation of the render backend.
|
|
25
|
+
*/
|
|
26
|
+
export class WebGPUBackend implements IRenderBackend {
|
|
27
|
+
readonly type = "webgpu" as const;
|
|
28
|
+
readonly limits: Limits;
|
|
29
|
+
readonly atlasSize: Size;
|
|
30
|
+
readonly defaultAtlasId = "default";
|
|
31
|
+
|
|
32
|
+
#atlases = new Map<string, ITextureAtlas>();
|
|
33
|
+
|
|
34
|
+
#device: GPUDevice;
|
|
35
|
+
#context: GPUCanvasContext;
|
|
36
|
+
#presentationFormat: GPUTextureFormat;
|
|
37
|
+
#encoder: GPUCommandEncoder | null = null;
|
|
38
|
+
#renderPass: GPURenderPassEncoder | null = null;
|
|
39
|
+
#postprocess: PostProcess | null = null;
|
|
40
|
+
#pingpong: [GPUTexture, GPUTexture] | null = null;
|
|
41
|
+
#canvas: HTMLCanvasElement;
|
|
42
|
+
|
|
43
|
+
private constructor(
|
|
44
|
+
device: GPUDevice,
|
|
45
|
+
context: GPUCanvasContext,
|
|
46
|
+
presentationFormat: GPUTextureFormat,
|
|
47
|
+
limits: Limits,
|
|
48
|
+
canvas: HTMLCanvasElement,
|
|
49
|
+
) {
|
|
50
|
+
this.#device = device;
|
|
51
|
+
this.#context = context;
|
|
52
|
+
this.#presentationFormat = presentationFormat;
|
|
53
|
+
this.limits = limits;
|
|
54
|
+
this.atlasSize = {
|
|
55
|
+
width: limits.textureSize,
|
|
56
|
+
height: limits.textureSize,
|
|
57
|
+
};
|
|
58
|
+
this.#canvas = canvas;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a WebGPU backend attached to a canvas.
|
|
63
|
+
*/
|
|
64
|
+
static async create(
|
|
65
|
+
canvas: HTMLCanvasElement,
|
|
66
|
+
options: WebGPUBackendOptions = {},
|
|
67
|
+
): Promise<WebGPUBackend> {
|
|
68
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
69
|
+
if (!adapter) {
|
|
70
|
+
throw new Error("WebGPU not supported: no adapter found");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const device = await adapter.requestDevice();
|
|
74
|
+
device.lost.then((info) => {
|
|
75
|
+
console.error("GPU Device lost", info);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const context = canvas.getContext("webgpu");
|
|
79
|
+
assert(context, "Could not get WebGPU context from canvas");
|
|
80
|
+
|
|
81
|
+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
|
82
|
+
|
|
83
|
+
context.configure({
|
|
84
|
+
device,
|
|
85
|
+
format: presentationFormat,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const limits: Limits = {
|
|
89
|
+
...DEFAULT_LIMITS,
|
|
90
|
+
...options.limits,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const backend = new WebGPUBackend(
|
|
94
|
+
device,
|
|
95
|
+
context,
|
|
96
|
+
presentationFormat,
|
|
97
|
+
limits,
|
|
98
|
+
canvas,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Create the default texture atlas
|
|
102
|
+
backend.createTextureAtlas("default", {
|
|
103
|
+
format: options.format ?? "rgba8unorm",
|
|
104
|
+
layers: limits.textureArrayLayers,
|
|
105
|
+
size: limits.textureSize,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return backend;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
startFrame(clearColor: Color, loadOp: "clear" | "load"): void {
|
|
112
|
+
this.#encoder = this.#device.createCommandEncoder();
|
|
113
|
+
|
|
114
|
+
// If postprocessing, render to ping-pong texture; otherwise render to canvas
|
|
115
|
+
const target = this.#postprocess
|
|
116
|
+
? this.#pingpong![0]
|
|
117
|
+
: this.#context.getCurrentTexture();
|
|
118
|
+
|
|
119
|
+
this.#renderPass = this.#encoder.beginRenderPass({
|
|
120
|
+
label: "toodle frame",
|
|
121
|
+
colorAttachments: [
|
|
122
|
+
{
|
|
123
|
+
view: target.createView(),
|
|
124
|
+
clearValue: clearColor,
|
|
125
|
+
loadOp,
|
|
126
|
+
storeOp: "store",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
endFrame(): void {
|
|
133
|
+
assert(this.#renderPass, "No render pass - did you call startFrame?");
|
|
134
|
+
assert(this.#encoder, "No encoder - did you call startFrame?");
|
|
135
|
+
|
|
136
|
+
this.#renderPass.end();
|
|
137
|
+
|
|
138
|
+
// Run postprocessing if set
|
|
139
|
+
if (this.#postprocess && this.#pingpong) {
|
|
140
|
+
this.#postprocess.process(
|
|
141
|
+
this.#device.queue,
|
|
142
|
+
this.#encoder,
|
|
143
|
+
this.#pingpong,
|
|
144
|
+
this.#context.getCurrentTexture(),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.#device.queue.submit([this.#encoder.finish()]);
|
|
149
|
+
|
|
150
|
+
this.#renderPass = null;
|
|
151
|
+
this.#encoder = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateEngineUniform(_uniform: EngineUniform): void {
|
|
155
|
+
// Uniforms are updated per-shader in WebGPU, not at the backend level
|
|
156
|
+
// This is handled in WebGPUQuadShader.startFrame
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async uploadAtlas(
|
|
160
|
+
atlas: CpuTextureAtlas,
|
|
161
|
+
layerIndex: number,
|
|
162
|
+
atlasId?: string,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const targetAtlas = this.getTextureAtlas(atlasId ?? "default");
|
|
165
|
+
assert(targetAtlas, `Atlas "${atlasId ?? "default"}" not found`);
|
|
166
|
+
const texture = targetAtlas.handle as GPUTexture;
|
|
167
|
+
|
|
168
|
+
if (atlas.rg8Bytes) {
|
|
169
|
+
const w = texture.width;
|
|
170
|
+
const h = texture.height;
|
|
171
|
+
|
|
172
|
+
// WebGPU requires 256-byte bytesPerRow
|
|
173
|
+
const rowBytes = w * 2;
|
|
174
|
+
assert(rowBytes % 256 === 0, "rowBytes must be a multiple of 256");
|
|
175
|
+
|
|
176
|
+
this.#device.queue.writeTexture(
|
|
177
|
+
{
|
|
178
|
+
texture,
|
|
179
|
+
origin: { x: 0, y: 0, z: layerIndex },
|
|
180
|
+
},
|
|
181
|
+
atlas.rg8Bytes,
|
|
182
|
+
{ bytesPerRow: rowBytes, rowsPerImage: h },
|
|
183
|
+
{ width: w, height: h, depthOrArrayLayers: 1 },
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
this.#device.queue.copyExternalImageToTexture(
|
|
187
|
+
{
|
|
188
|
+
source: atlas.texture,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
texture,
|
|
192
|
+
origin: [0, 0, layerIndex],
|
|
193
|
+
},
|
|
194
|
+
[atlas.texture.width, atlas.texture.height, 1],
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
createQuadShader(opts: QuadShaderCreationOpts): IBackendShader {
|
|
200
|
+
return new WebGPUQuadShader(
|
|
201
|
+
opts.label,
|
|
202
|
+
this,
|
|
203
|
+
opts.instanceCount,
|
|
204
|
+
opts.userCode,
|
|
205
|
+
opts.blendMode,
|
|
206
|
+
opts.atlasId,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
createTextureAtlas(id: string, options?: TextureAtlasOptions): ITextureAtlas {
|
|
211
|
+
if (this.#atlases.has(id)) {
|
|
212
|
+
throw new Error(`Atlas "${id}" already exists`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const format: TextureAtlasFormat = options?.format ?? "rgba8unorm";
|
|
216
|
+
const layers = options?.layers ?? this.limits.textureArrayLayers;
|
|
217
|
+
const size = options?.size ?? this.limits.textureSize;
|
|
218
|
+
|
|
219
|
+
const texture = this.#device.createTexture({
|
|
220
|
+
label: `Toodle Atlas "${id}"`,
|
|
221
|
+
size: [size, size, layers],
|
|
222
|
+
format,
|
|
223
|
+
usage:
|
|
224
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
225
|
+
GPUTextureUsage.COPY_DST |
|
|
226
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const atlas: ITextureAtlas = { id, format, layers, size, handle: texture };
|
|
230
|
+
this.#atlases.set(id, atlas);
|
|
231
|
+
return atlas;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getTextureAtlas(id?: string): ITextureAtlas | null {
|
|
235
|
+
return this.#atlases.get(id ?? "default") ?? null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
destroyTextureAtlas(id: string): void {
|
|
239
|
+
const atlas = this.#atlases.get(id);
|
|
240
|
+
if (atlas) {
|
|
241
|
+
(atlas.handle as GPUTexture).destroy();
|
|
242
|
+
this.#atlases.delete(id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
resize(_width: number, _height: number): void {
|
|
247
|
+
// Canvas resize is handled automatically by WebGPU context
|
|
248
|
+
// The presentation size updates on next getCurrentTexture()
|
|
249
|
+
|
|
250
|
+
// Recreate ping-pong textures if postprocessing is active
|
|
251
|
+
if (this.#postprocess && this.#pingpong) {
|
|
252
|
+
this.#destroyPingPongTextures();
|
|
253
|
+
this.#createPingPongTextures();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
destroy(): void {
|
|
258
|
+
this.#destroyPingPongTextures();
|
|
259
|
+
// Destroy all atlases
|
|
260
|
+
for (const atlas of this.#atlases.values()) {
|
|
261
|
+
(atlas.handle as GPUTexture).destroy();
|
|
262
|
+
}
|
|
263
|
+
this.#atlases.clear();
|
|
264
|
+
this.#device.destroy();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Set a post-processor for screen effects.
|
|
269
|
+
* Setting a post-processor will cause the main render to go to an offscreen texture.
|
|
270
|
+
* Note: Ping-pong textures are not destroyed when setting to null to avoid
|
|
271
|
+
* race conditions with in-flight command buffers. They are cleaned up on destroy().
|
|
272
|
+
*/
|
|
273
|
+
setPostprocess(processor: PostProcess | null): void {
|
|
274
|
+
this.#postprocess = processor;
|
|
275
|
+
if (processor && !this.#pingpong) {
|
|
276
|
+
this.#createPingPongTextures();
|
|
277
|
+
}
|
|
278
|
+
// Don't destroy pingpong textures when setting to null - they may still be
|
|
279
|
+
// referenced by in-flight command buffers. They'll be cleaned up on destroy().
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get the current post-processor.
|
|
284
|
+
*/
|
|
285
|
+
getPostprocess(): PostProcess | null {
|
|
286
|
+
return this.#postprocess;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#createPingPongTextures(): void {
|
|
290
|
+
const width = this.#canvas.width;
|
|
291
|
+
const height = this.#canvas.height;
|
|
292
|
+
|
|
293
|
+
const createTexture = (label: string) =>
|
|
294
|
+
this.#device.createTexture({
|
|
295
|
+
label,
|
|
296
|
+
size: [width, height],
|
|
297
|
+
format: this.#presentationFormat,
|
|
298
|
+
usage:
|
|
299
|
+
GPUTextureUsage.RENDER_ATTACHMENT |
|
|
300
|
+
GPUTextureUsage.TEXTURE_BINDING |
|
|
301
|
+
GPUTextureUsage.COPY_SRC,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.#pingpong = [
|
|
305
|
+
createTexture("toodle pingpong 0"),
|
|
306
|
+
createTexture("toodle pingpong 1"),
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#destroyPingPongTextures(): void {
|
|
311
|
+
if (this.#pingpong) {
|
|
312
|
+
this.#pingpong[0].destroy();
|
|
313
|
+
this.#pingpong[1].destroy();
|
|
314
|
+
this.#pingpong = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get the GPU device for advanced operations.
|
|
320
|
+
*/
|
|
321
|
+
get device(): GPUDevice {
|
|
322
|
+
return this.#device;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get the canvas context.
|
|
327
|
+
*/
|
|
328
|
+
get context(): GPUCanvasContext {
|
|
329
|
+
return this.#context;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the presentation format.
|
|
334
|
+
*/
|
|
335
|
+
get presentationFormat(): GPUTextureFormat {
|
|
336
|
+
return this.#presentationFormat;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get the current render pass encoder.
|
|
341
|
+
* Only available between startFrame() and endFrame().
|
|
342
|
+
*/
|
|
343
|
+
get renderPass(): GPURenderPassEncoder {
|
|
344
|
+
assert(
|
|
345
|
+
this.#renderPass,
|
|
346
|
+
"No render pass available - did you call startFrame?",
|
|
347
|
+
);
|
|
348
|
+
return this.#renderPass;
|
|
349
|
+
}
|
|
350
|
+
}
|