@nodetool-ai/game-renderer 0.8.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 (62) hide show
  1. package/dist/.nodetool-build-stamp +1 -0
  2. package/dist/audio.d.ts +39 -0
  3. package/dist/audio.d.ts.map +1 -0
  4. package/dist/audio.js +227 -0
  5. package/dist/audio.js.map +1 -0
  6. package/dist/browser-fonts.d.ts +8 -0
  7. package/dist/browser-fonts.d.ts.map +1 -0
  8. package/dist/browser-fonts.js +39 -0
  9. package/dist/browser-fonts.js.map +1 -0
  10. package/dist/browser.d.ts +31 -0
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +106 -0
  13. package/dist/browser.js.map +1 -0
  14. package/dist/build.d.ts +19 -0
  15. package/dist/build.d.ts.map +1 -0
  16. package/dist/build.js +236 -0
  17. package/dist/build.js.map +1 -0
  18. package/dist/canvas2d.d.ts +34 -0
  19. package/dist/canvas2d.d.ts.map +1 -0
  20. package/dist/canvas2d.js +208 -0
  21. package/dist/canvas2d.js.map +1 -0
  22. package/dist/fonts.d.ts +5 -0
  23. package/dist/fonts.d.ts.map +1 -0
  24. package/dist/fonts.js +10 -0
  25. package/dist/fonts.js.map +1 -0
  26. package/dist/frame.d.ts +55 -0
  27. package/dist/frame.d.ts.map +1 -0
  28. package/dist/frame.js +123 -0
  29. package/dist/frame.js.map +1 -0
  30. package/dist/gpu-capture.d.ts +4 -0
  31. package/dist/gpu-capture.d.ts.map +1 -0
  32. package/dist/gpu-capture.js +192 -0
  33. package/dist/gpu-capture.js.map +1 -0
  34. package/dist/index.d.ts +43 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +3 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/input.d.ts +3 -0
  39. package/dist/input.d.ts.map +1 -0
  40. package/dist/input.js +16 -0
  41. package/dist/input.js.map +1 -0
  42. package/dist/lighting.d.ts +17 -0
  43. package/dist/lighting.d.ts.map +1 -0
  44. package/dist/lighting.js +79 -0
  45. package/dist/lighting.js.map +1 -0
  46. package/dist/node.d.ts +14 -0
  47. package/dist/node.d.ts.map +1 -0
  48. package/dist/node.js +177 -0
  49. package/dist/node.js.map +1 -0
  50. package/dist/standalone-player.d.ts +2 -0
  51. package/dist/standalone-player.d.ts.map +1 -0
  52. package/dist/standalone-player.js +286 -0
  53. package/dist/standalone-player.js.map +1 -0
  54. package/dist/touch-controls.d.ts +18 -0
  55. package/dist/touch-controls.d.ts.map +1 -0
  56. package/dist/touch-controls.js +136 -0
  57. package/dist/touch-controls.js.map +1 -0
  58. package/dist/webgpu.d.ts +75 -0
  59. package/dist/webgpu.d.ts.map +1 -0
  60. package/dist/webgpu.js +807 -0
  61. package/dist/webgpu.js.map +1 -0
  62. package/package.json +64 -0
package/dist/webgpu.js ADDED
@@ -0,0 +1,807 @@
1
+ import { colorBrightnessContrastV1, colorCubeLutV1, createDefaultRegistry, createExecutor, createGPUContextFromDevice, createLabeledTexture, createRecipeRunner, filtersGlowV1, LabeledTexture } from "@nodetool-ai/gpu/pool";
2
+ import * as d from "typegpu/data";
3
+ import { imageHeight, imageWidth } from "./canvas2d.js";
4
+ import { paintHud, parseTint, projectedCamera, visibleItems } from "./frame.js";
5
+ const INSTANCE_FLOATS = 14;
6
+ const SHADER = `
7
+ struct Camera { center: vec2f, viewport: vec2f, zoom: f32, padding0: f32, padding1: f32, padding2: f32 };
8
+ override linearizeInput: bool = false;
9
+ fn srgbToLinear(rgb: vec3f) -> vec3f {
10
+ return select(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3f(2.4)), rgb > vec3f(0.04045));
11
+ }
12
+ fn linearToSrgb(rgb: vec3f) -> vec3f {
13
+ return select(rgb * 12.92, 1.055 * pow(rgb, vec3f(1.0 / 2.4)) - 0.055, rgb > vec3f(0.0031308));
14
+ }
15
+ @group(0) @binding(0) var<uniform> camera: Camera;
16
+ @group(0) @binding(1) var atlasSampler: sampler;
17
+ @group(0) @binding(2) var atlas: texture_2d<f32>;
18
+ @group(1) @binding(0) var lightSampler: sampler;
19
+ @group(1) @binding(1) var lightTexture: texture_2d<f32>;
20
+
21
+ struct VertexInput {
22
+ @builtin(vertex_index) corner: u32,
23
+ @location(0) center: vec2f,
24
+ @location(1) size: vec2f,
25
+ @location(2) uv: vec4f,
26
+ @location(3) color: vec4f,
27
+ @location(4) rotation: f32,
28
+ @location(5) unlit: f32,
29
+ };
30
+ struct VertexOutput {
31
+ @builtin(position) position: vec4f,
32
+ @location(0) uv: vec2f,
33
+ @location(1) color: vec4f,
34
+ @location(3) @interpolate(flat) unlit: f32,
35
+ @location(4) lightUv: vec2f,
36
+ };
37
+ @vertex fn vertexMain(input: VertexInput) -> VertexOutput {
38
+ let corners = array<vec2f, 6>(
39
+ vec2f(-0.5, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, 0.5),
40
+ vec2f(0.5, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5));
41
+ let local = corners[input.corner] * input.size;
42
+ let sine = sin(input.rotation);
43
+ let cosine = cos(input.rotation);
44
+ let rotated = vec2f(local.x * cosine - local.y * sine, local.x * sine + local.y * cosine);
45
+ let world = input.center + rotated;
46
+ let clip = (world - camera.center) * camera.zoom * 2.0 / camera.viewport;
47
+ var output: VertexOutput;
48
+ output.position = vec4f(clip, 0.0, 1.0);
49
+ let uvCorner = corners[input.corner] + vec2f(0.5, 0.5);
50
+ output.uv = input.uv.xy + vec2f(uvCorner.x, 1.0 - uvCorner.y) * input.uv.zw;
51
+ output.color = input.color;
52
+ output.unlit = input.unlit;
53
+ output.lightUv = clip * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
54
+ return output;
55
+ }
56
+ @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
57
+ let sampled = textureSample(atlas, atlasSampler, input.uv) * input.color;
58
+ var straight = select(sampled.rgb, srgbToLinear(sampled.rgb), linearizeInput);
59
+ if (input.unlit < 0.5) {
60
+ let irradiance = textureSampleLevel(lightTexture, lightSampler, input.lightUv, 0.0).rgb;
61
+ let lit = clamp(srgbToLinear(sampled.rgb) * irradiance, vec3f(0.0), vec3f(1.0));
62
+ straight = select(linearToSrgb(lit), lit, linearizeInput);
63
+ }
64
+ return vec4f(straight * sampled.a, sampled.a);
65
+ }`;
66
+ const LIGHT_SHADER = `
67
+ struct Camera { center: vec2f, viewport: vec2f, zoom: f32, padding0: f32, padding1: f32, padding2: f32 };
68
+ struct PointLight { position: vec2f, radius: f32, intensity: f32, color: vec4f, falloff: f32, pad0: f32, pad1: f32, pad2: f32 };
69
+ struct Lighting { ambient: vec4f, info: vec4f, points: array<PointLight, 32> };
70
+ fn srgbToLinear(rgb: vec3f) -> vec3f {
71
+ return select(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3f(2.4)), rgb > vec3f(0.04045));
72
+ }
73
+ @group(0) @binding(0) var<uniform> camera: Camera;
74
+ @group(0) @binding(1) var<uniform> lighting: Lighting;
75
+ struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f };
76
+ @vertex fn vertexMain(@builtin(vertex_index) index: u32) -> VertexOutput {
77
+ let positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
78
+ var output: VertexOutput;
79
+ output.position = vec4f(positions[index], 0.0, 1.0);
80
+ output.uv = positions[index] * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
81
+ return output;
82
+ }
83
+ @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
84
+ let world = camera.center + (input.uv - vec2f(0.5, 0.5)) * camera.viewport / camera.zoom * vec2f(1.0, -1.0);
85
+ var irradiance = srgbToLinear(lighting.ambient.rgb) * lighting.ambient.a;
86
+ for (var i = 0u; i < 32u; i += 1u) {
87
+ if (i >= u32(lighting.info.x)) { break; }
88
+ let point = lighting.points[i];
89
+ let distance = length(world - point.position);
90
+ if (distance < point.radius) {
91
+ irradiance += srgbToLinear(point.color.rgb) * point.intensity * pow(1.0 - distance / point.radius, point.falloff);
92
+ }
93
+ }
94
+ return vec4f(irradiance, 1.0);
95
+ }`;
96
+ const PRESENT_SHADER = `
97
+ override decodeInput: bool = false;
98
+ override encodeOutput: bool = true;
99
+ override cropScaleX: f32 = 1.0;
100
+ override cropScaleY: f32 = 1.0;
101
+ override cropOffsetX: f32 = 0.0;
102
+ override cropOffsetY: f32 = 0.0;
103
+ @group(0) @binding(0) var sourceSampler: sampler;
104
+ @group(0) @binding(1) var sourceTexture: texture_2d<f32>;
105
+ struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f };
106
+ @vertex fn vertexMain(@builtin(vertex_index) index: u32) -> VertexOutput {
107
+ let positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
108
+ var output: VertexOutput;
109
+ output.position = vec4f(positions[index], 0.0, 1.0);
110
+ output.uv = positions[index] * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
111
+ return output;
112
+ }
113
+ @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
114
+ let uv = input.uv * vec2f(cropScaleX, cropScaleY) + vec2f(cropOffsetX, cropOffsetY);
115
+ let premul = textureSample(sourceTexture, sourceSampler, uv);
116
+ let straight = premul.rgb / max(premul.a, 1.0 / 255.0);
117
+ let linear = select(straight / 12.92, pow((straight + 0.055) / 1.055, vec3f(2.4)),
118
+ straight > vec3f(0.04045));
119
+ let encoded = select(straight * 12.92, 1.055 * pow(straight, vec3f(1.0 / 2.4)) - 0.055,
120
+ straight > vec3f(0.0031308));
121
+ var converted = select(straight, linear, decodeInput);
122
+ converted = select(converted, encoded, encodeOutput);
123
+ return vec4f(clamp(converted, vec3f(0.0), vec3f(1.0)) * premul.a, premul.a);
124
+ }`;
125
+ const HUD_SHADER = `
126
+ @group(0) @binding(0) var sourceSampler: sampler;
127
+ @group(0) @binding(1) var sourceTexture: texture_2d<f32>;
128
+ struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f };
129
+ @vertex fn vertexMain(@builtin(vertex_index) index: u32) -> VertexOutput {
130
+ let positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
131
+ var output: VertexOutput;
132
+ output.position = vec4f(positions[index], 0.0, 1.0);
133
+ output.uv = positions[index] * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
134
+ return output;
135
+ }
136
+ @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
137
+ let color = textureSample(sourceTexture, sourceSampler, input.uv);
138
+ return vec4f(color.rgb * color.a, color.a);
139
+ }`;
140
+ /** Draws ordered sprites as consecutive texture batches on a private WebGPU device. */
141
+ export class WebGPUGameRenderer {
142
+ canvas;
143
+ device;
144
+ context;
145
+ format;
146
+ assets;
147
+ adapterType;
148
+ backend = "webgpu";
149
+ pipeline;
150
+ effectSpritePipeline;
151
+ additivePipeline;
152
+ effectAdditivePipeline;
153
+ linearSampler;
154
+ presentPipeline;
155
+ presentModule;
156
+ hudPipeline;
157
+ toSrgbPipeline;
158
+ toLinearPipeline;
159
+ presentLayout;
160
+ effectContext;
161
+ effectExecutor = createExecutor();
162
+ effectRecipeRunner = createRecipeRunner();
163
+ effectRegistry = createDefaultRegistry();
164
+ effects = [];
165
+ hudOrder = "beforeEffects";
166
+ sourceTarget;
167
+ effectTarget;
168
+ srgbTarget;
169
+ lutTarget;
170
+ minimalRenderSucceeded = false;
171
+ sampler;
172
+ cameraBuffer;
173
+ lightingBuffer;
174
+ lightPipeline;
175
+ lightLayout;
176
+ lightSampleLayout;
177
+ lightFallback;
178
+ lightTarget;
179
+ textures = new Map();
180
+ validatedLuts = new Set();
181
+ fallback;
182
+ placeholders = new Map();
183
+ instanceBuffer;
184
+ instanceCapacity = 0;
185
+ hudCanvas;
186
+ hudTexture;
187
+ hudKey = "";
188
+ disposed = false;
189
+ lost = false;
190
+ constructor(canvas, device, context, format, assets, adapterType = "unknown") {
191
+ this.canvas = canvas;
192
+ this.device = device;
193
+ this.context = context;
194
+ this.format = format;
195
+ this.assets = assets;
196
+ this.adapterType = adapterType;
197
+ const module = device.createShaderModule({ code: SHADER });
198
+ const spriteLayout = device.createBindGroupLayout({ entries: [
199
+ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
200
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
201
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
202
+ ] });
203
+ this.lightSampleLayout = device.createBindGroupLayout({ entries: [
204
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
205
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
206
+ ] });
207
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [spriteLayout, this.lightSampleLayout] });
208
+ const pipelineDescriptor = (targetFormat, linearizeInput, blend = "normal") => ({
209
+ layout: pipelineLayout,
210
+ vertex: {
211
+ module,
212
+ entryPoint: "vertexMain",
213
+ buffers: [{
214
+ arrayStride: INSTANCE_FLOATS * 4,
215
+ stepMode: "instance",
216
+ attributes: [
217
+ { shaderLocation: 0, offset: 0, format: "float32x2" },
218
+ { shaderLocation: 1, offset: 8, format: "float32x2" },
219
+ { shaderLocation: 2, offset: 16, format: "float32x4" },
220
+ { shaderLocation: 3, offset: 32, format: "float32x4" },
221
+ { shaderLocation: 4, offset: 48, format: "float32" },
222
+ { shaderLocation: 5, offset: 52, format: "float32" },
223
+ ],
224
+ }],
225
+ },
226
+ fragment: {
227
+ module,
228
+ entryPoint: "fragmentMain",
229
+ constants: { linearizeInput: linearizeInput ? 1 : 0 },
230
+ targets: [{
231
+ format: targetFormat,
232
+ blend: blend === "additive" ? {
233
+ color: { srcFactor: "one", dstFactor: "one", operation: "add" },
234
+ alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
235
+ } : {
236
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
237
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
238
+ },
239
+ }],
240
+ },
241
+ primitive: { topology: "triangle-list" },
242
+ });
243
+ this.pipeline = device.createRenderPipeline(pipelineDescriptor(format, false));
244
+ this.effectSpritePipeline = device.createRenderPipeline(pipelineDescriptor("rgba8unorm", true));
245
+ this.additivePipeline = device.createRenderPipeline(pipelineDescriptor(format, false, "additive"));
246
+ this.effectAdditivePipeline = device.createRenderPipeline(pipelineDescriptor("rgba8unorm", true, "additive"));
247
+ this.lightLayout = device.createBindGroupLayout({ entries: [
248
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
249
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
250
+ ] });
251
+ const lightModule = device.createShaderModule({ code: LIGHT_SHADER });
252
+ this.lightPipeline = device.createRenderPipeline({
253
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.lightLayout] }),
254
+ vertex: { module: lightModule, entryPoint: "vertexMain" },
255
+ fragment: { module: lightModule, entryPoint: "fragmentMain", targets: [{ format: "rgba16float" }] },
256
+ primitive: { topology: "triangle-list" },
257
+ });
258
+ this.presentLayout = device.createBindGroupLayout({ entries: [
259
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
260
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
261
+ ] });
262
+ const presentModule = device.createShaderModule({ code: PRESENT_SHADER });
263
+ this.presentModule = presentModule;
264
+ this.presentPipeline = device.createRenderPipeline({
265
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.presentLayout] }),
266
+ vertex: { module: presentModule, entryPoint: "vertexMain" },
267
+ fragment: { module: presentModule, entryPoint: "fragmentMain", constants: { decodeInput: 0, encodeOutput: 1 }, targets: [{ format }] },
268
+ primitive: { topology: "triangle-list" },
269
+ });
270
+ const convertPipeline = (decodeInput) => device.createRenderPipeline({
271
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.presentLayout] }),
272
+ vertex: { module: presentModule, entryPoint: "vertexMain" },
273
+ fragment: { module: presentModule, entryPoint: "fragmentMain",
274
+ constants: { decodeInput: decodeInput ? 1 : 0, encodeOutput: decodeInput ? 0 : 1 },
275
+ targets: [{ format: "rgba8unorm" }] },
276
+ primitive: { topology: "triangle-list" },
277
+ });
278
+ this.toSrgbPipeline = convertPipeline(false);
279
+ this.toLinearPipeline = convertPipeline(true);
280
+ const hudModule = device.createShaderModule({ code: HUD_SHADER });
281
+ this.hudPipeline = device.createRenderPipeline({
282
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.presentLayout] }),
283
+ vertex: { module: hudModule, entryPoint: "vertexMain" },
284
+ fragment: { module: hudModule, entryPoint: "fragmentMain", targets: [{ format,
285
+ blend: { color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
286
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" } } }] },
287
+ primitive: { topology: "triangle-list" },
288
+ });
289
+ this.effectContext = createGPUContextFromDevice(device);
290
+ this.sampler = device.createSampler({ magFilter: "nearest", minFilter: "nearest", mipmapFilter: "nearest" });
291
+ this.linearSampler = device.createSampler({ magFilter: "linear", minFilter: "linear", mipmapFilter: "linear" });
292
+ this.cameraBuffer = device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
293
+ this.lightingBuffer = device.createBuffer({ size: 1568, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
294
+ this.lightFallback = device.createTexture({ size: [1, 1], format: "rgba8unorm",
295
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
296
+ device.queue.writeTexture({ texture: this.lightFallback }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
297
+ const fallbackTexture = device.createTexture({ size: [1, 1], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
298
+ device.queue.writeTexture({ texture: fallbackTexture }, new Uint8Array([255, 0, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
299
+ this.fallback = this.textureEntry(fallbackTexture, 1, 1);
300
+ void device.lost.then(() => { this.lost = true; });
301
+ }
302
+ get capabilities() {
303
+ return { backend: this.backend, core2D: true, gpuEffects: !this.lost && !this.disposed,
304
+ lighting: !this.lost && !this.disposed,
305
+ adapterType: this.adapterType, deviceStatus: this.disposed ? "disposed" : this.lost ? "lost" : "ready",
306
+ enabledFeatures: [...this.device.features], requestedFeatures: [],
307
+ limits: { maxTextureDimension2D: this.device.limits.maxTextureDimension2D,
308
+ maxBufferSize: this.device.limits.maxBufferSize },
309
+ minimalRenderSucceeded: this.minimalRenderSucceeded, deviceLossCount: this.lost ? 1 : 0,
310
+ fallbackReason: null };
311
+ }
312
+ setEffects(effects, hudOrder) {
313
+ if (effects.length > 8) {
314
+ throw new Error("A game supports at most eight render effects");
315
+ }
316
+ for (const effect of effects) {
317
+ if (effect.kind === "brightnessContrast" && (!Number.isFinite(effect.brightness) || effect.brightness < -1 || effect.brightness > 1 ||
318
+ !Number.isFinite(effect.contrast) || effect.contrast < 0 || effect.contrast > 4)) {
319
+ throw new Error("Brightness and contrast are outside the approved range");
320
+ }
321
+ if (effect.kind === "bloom" && (!Number.isFinite(effect.threshold) || effect.threshold < 0 || effect.threshold > 1 ||
322
+ !Number.isFinite(effect.softness) || effect.softness < 0 || effect.softness > 0.5 ||
323
+ !Number.isFinite(effect.radius) || effect.radius < 0 || effect.radius > 64 ||
324
+ !Number.isFinite(effect.intensity) || effect.intensity < 0 || effect.intensity > 4)) {
325
+ throw new Error("Bloom parameters are outside the approved range");
326
+ }
327
+ if (effect.kind === "lut" && (!Number.isFinite(effect.intensity) || effect.intensity < 0 || effect.intensity > 1 ||
328
+ effect.size < 2 || effect.size > 32 || effect.domainMin.some((value, channel) => value >= effect.domainMax[channel]))) {
329
+ throw new Error("LUT parameters are outside the approved range");
330
+ }
331
+ }
332
+ const changed = JSON.stringify(this.effects) !== JSON.stringify(effects);
333
+ this.effects = [...effects];
334
+ this.hudOrder = hudOrder ?? (effects.some((effect) => effect.kind === "bloom") ? "afterEffects" : "beforeEffects");
335
+ if (changed) {
336
+ this.effectContext.scratch.dispose();
337
+ this.sourceTarget?.destroy();
338
+ this.effectTarget?.destroy();
339
+ this.srgbTarget?.destroy();
340
+ this.lutTarget?.destroy();
341
+ this.sourceTarget = undefined;
342
+ this.effectTarget = undefined;
343
+ this.srgbTarget = undefined;
344
+ this.lutTarget = undefined;
345
+ }
346
+ if (effects.length === 0) {
347
+ this.sourceTarget?.destroy();
348
+ this.effectTarget?.destroy();
349
+ this.srgbTarget?.destroy();
350
+ this.lutTarget?.destroy();
351
+ this.sourceTarget = undefined;
352
+ this.effectTarget = undefined;
353
+ this.srgbTarget = undefined;
354
+ this.lutTarget = undefined;
355
+ }
356
+ }
357
+ ensureEffectTargets() {
358
+ const margin = Math.ceil(this.effects.reduce((maximum, effect) => effect.kind === "bloom" ? Math.max(maximum, effect.radius * 2) : maximum, 0));
359
+ const width = Math.max(1, this.canvas.width + margin * 2);
360
+ const height = Math.max(1, this.canvas.height + margin * 2);
361
+ if (width > this.device.limits.maxTextureDimension2D || height > this.device.limits.maxTextureDimension2D) {
362
+ throw new Error("Bloom overscan exceeds the WebGPU texture limit");
363
+ }
364
+ if (this.sourceTarget?.width === width && this.sourceTarget.height === height) {
365
+ return;
366
+ }
367
+ this.presentPipeline = this.device.createRenderPipeline({
368
+ layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.presentLayout] }),
369
+ vertex: { module: this.presentModule, entryPoint: "vertexMain" },
370
+ fragment: { module: this.presentModule, entryPoint: "fragmentMain",
371
+ constants: { decodeInput: 0, encodeOutput: 1,
372
+ cropScaleX: this.canvas.width / width, cropScaleY: this.canvas.height / height,
373
+ cropOffsetX: margin / width, cropOffsetY: margin / height }, targets: [{ format: this.format }] },
374
+ primitive: { topology: "triangle-list" },
375
+ });
376
+ this.effectContext.scratch.dispose();
377
+ this.sourceTarget?.destroy();
378
+ this.effectTarget?.destroy();
379
+ this.srgbTarget?.destroy();
380
+ this.lutTarget?.destroy();
381
+ const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
382
+ this.sourceTarget = createLabeledTexture(this.device, { width, height, format: "rgba8unorm", usage,
383
+ label: "game-effect-source" });
384
+ this.effectTarget = createLabeledTexture(this.device, { width, height, format: "rgba8unorm", usage,
385
+ label: "game-effect-output" });
386
+ if (this.effects.some((effect) => effect.kind === "lut")) {
387
+ this.srgbTarget = createLabeledTexture(this.device, { width, height, format: "rgba8unorm", usage,
388
+ label: "game-lut-source", meta: { colorSpace: "srgb" } });
389
+ this.lutTarget = createLabeledTexture(this.device, { width, height, format: "rgba8unorm", usage,
390
+ label: "game-lut-output", meta: { colorSpace: "srgb" } });
391
+ }
392
+ }
393
+ ensureLightTarget(width, height) {
394
+ if (width > this.device.limits.maxTextureDimension2D || height > this.device.limits.maxTextureDimension2D) {
395
+ throw new Error("Light texture exceeds the WebGPU texture limit");
396
+ }
397
+ if (this.lightTarget?.width === width && this.lightTarget.height === height)
398
+ return this.lightTarget;
399
+ this.lightTarget?.destroy();
400
+ this.lightTarget = createLabeledTexture(this.device, { width, height, format: "rgba16float",
401
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, label: "game-light-texture" });
402
+ return this.lightTarget;
403
+ }
404
+ convertColor(encoder, source, output, pipeline) {
405
+ const group = this.device.createBindGroup({ layout: this.presentLayout, entries: [
406
+ { binding: 0, resource: this.sampler }, { binding: 1, resource: source.createView() },
407
+ ] });
408
+ const pass = encoder.beginRenderPass({ colorAttachments: [{ view: output.createView(),
409
+ loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 } }] });
410
+ pass.setPipeline(pipeline);
411
+ pass.setBindGroup(0, group);
412
+ pass.draw(3);
413
+ pass.end();
414
+ output.markWritten();
415
+ }
416
+ textureEntry(texture, width, height, sampling = "nearest") {
417
+ return {
418
+ texture,
419
+ width,
420
+ height,
421
+ bindGroup: this.device.createBindGroup({
422
+ layout: this.pipeline.getBindGroupLayout(0),
423
+ entries: [
424
+ { binding: 0, resource: { buffer: this.cameraBuffer } },
425
+ { binding: 1, resource: sampling === "linear" ? this.linearSampler : this.sampler },
426
+ { binding: 2, resource: texture.createView() },
427
+ ],
428
+ }),
429
+ };
430
+ }
431
+ upload(image, sampling = "nearest") {
432
+ const width = image instanceof HTMLCanvasElement ? image.width : imageWidth(image);
433
+ const height = image instanceof HTMLCanvasElement ? image.height : imageHeight(image);
434
+ if (width <= 0 || height <= 0) {
435
+ return this.fallback;
436
+ }
437
+ // copyExternalImageToTexture requires RENDER_ATTACHMENT on the destination as well as COPY_DST.
438
+ const texture = this.device.createTexture({ size: [width, height], format: "rgba8unorm",
439
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT });
440
+ this.device.queue.copyExternalImageToTexture({ source: image }, { texture, premultipliedAlpha: false }, [width, height]);
441
+ return this.textureEntry(texture, width, height, sampling);
442
+ }
443
+ async getTexture(assetId, sampling) {
444
+ const cached = this.textures.get(assetId);
445
+ if (cached) {
446
+ return { texture: cached, uploadedBytes: 0 };
447
+ }
448
+ const image = await this.assets.get(assetId);
449
+ if (!image) {
450
+ return { texture: this.placeholder(assetId), uploadedBytes: 0 };
451
+ }
452
+ const texture = this.upload(image, sampling);
453
+ this.textures.set(assetId, texture);
454
+ return { texture, uploadedBytes: texture.width * texture.height * 4 };
455
+ }
456
+ async getLutTexture(assetId, size) {
457
+ const image = await this.assets.get(assetId);
458
+ if (!image || imageWidth(image) !== size * size || imageHeight(image) !== size) {
459
+ throw new Error(`LUT ${assetId} is missing or has invalid dimensions`);
460
+ }
461
+ if (!this.validatedLuts.has(assetId)) {
462
+ const canvas = document.createElement("canvas");
463
+ canvas.width = imageWidth(image);
464
+ canvas.height = imageHeight(image);
465
+ const context = canvas.getContext("2d", { willReadFrequently: true });
466
+ if (!context) {
467
+ throw new Error("LUT validation requires Canvas2D");
468
+ }
469
+ context.drawImage(image, 0, 0);
470
+ const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
471
+ for (let index = 3; index < pixels.length; index += 4) {
472
+ if (pixels[index] !== 255) {
473
+ throw new Error(`LUT ${assetId} must be opaque`);
474
+ }
475
+ }
476
+ this.validatedLuts.add(assetId);
477
+ }
478
+ const { texture } = await this.getTexture(assetId, "nearest");
479
+ return new LabeledTexture(texture.texture, { label: `game-lut-${assetId}`, format: "rgba8unorm",
480
+ width: texture.width, height: texture.height,
481
+ meta: { colorSpace: "srgb", alpha: "premultiplied", bindingKind: "texture_2d" } });
482
+ }
483
+ placeholder(assetId) {
484
+ const cached = this.placeholders.get(assetId);
485
+ if (cached) {
486
+ return cached;
487
+ }
488
+ const colors = {
489
+ player: [42, 202, 233, 255], wall: [92, 105, 120, 255], gem: [255, 196, 50, 255],
490
+ };
491
+ const color = assetId === "player" ? colors.player : assetId === "wall" ? colors.wall : assetId === "gem" ? colors.gem : undefined;
492
+ if (!color) {
493
+ return this.fallback;
494
+ }
495
+ const texture = this.device.createTexture({ size: [1, 1], format: "rgba8unorm", usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
496
+ this.device.queue.writeTexture({ texture }, new Uint8Array(color), { bytesPerRow: 4 }, [1, 1]);
497
+ const entry = this.textureEntry(texture, 1, 1);
498
+ this.placeholders.set(assetId, entry);
499
+ return entry;
500
+ }
501
+ async render(frame, interpolation) {
502
+ if (this.disposed || this.lost) {
503
+ throw new Error(this.lost ? "WebGPU device was lost" : "Game renderer is disposed");
504
+ }
505
+ this.device.pushErrorScope("validation");
506
+ let popped = false;
507
+ try {
508
+ const stats = await this.renderFrame(frame, interpolation);
509
+ const validationError = await this.device.popErrorScope();
510
+ popped = true;
511
+ if (validationError) {
512
+ throw new Error(`WebGPU game render failed: ${validationError.message}`);
513
+ }
514
+ this.minimalRenderSucceeded = true;
515
+ return stats;
516
+ }
517
+ catch (error) {
518
+ if (!popped) {
519
+ await this.device.popErrorScope();
520
+ }
521
+ throw error;
522
+ }
523
+ }
524
+ async renderFrame(frame, interpolation) {
525
+ const bloomRadius = this.effects.reduce((maximum, effect) => effect.kind === "bloom" ? Math.max(maximum, effect.radius) : maximum, 0);
526
+ const overscanPixels = Math.ceil(bloomRadius * 2);
527
+ const overscanWorld = overscanPixels * frame.width / (Math.max(1, this.canvas.width) * projectedCamera(frame, interpolation).zoom);
528
+ const items = visibleItems(frame, interpolation, overscanWorld);
529
+ const sampling = new Map(items.map((item) => [item.assetId, item.sampling]));
530
+ const assetIds = [...sampling.keys()];
531
+ const textureResults = await Promise.all(assetIds.map((assetId) => this.getTexture(assetId, sampling.get(assetId) ?? "nearest")));
532
+ const textureById = new Map(assetIds.map((assetId, index) => [assetId, textureResults[index]]));
533
+ const hudTexture = this.updateHud(frame);
534
+ const count = items.length + (hudTexture ? 1 : 0);
535
+ this.ensureInstanceCapacity(count);
536
+ const instances = new Float32Array(count * INSTANCE_FLOATS);
537
+ const batches = [];
538
+ for (let index = 0; index < items.length; index++) {
539
+ const item = items[index];
540
+ const result = item ? textureById.get(item.assetId) : undefined;
541
+ if (!item || !result) {
542
+ continue;
543
+ }
544
+ const texture = result.texture;
545
+ const rect = texture.width === 1 && texture.height === 1 ? { x: 0, y: 0, width: 1, height: 1 } : item.frame ?? { x: 0, y: 0, width: texture.width, height: texture.height };
546
+ const tint = parseTint(item.tint);
547
+ const insetX = item.sampling === "linear" && item.frame && rect.width > 1 ? 0.5 : 0;
548
+ const insetY = item.sampling === "linear" && item.frame && rect.height > 1 ? 0.5 : 0;
549
+ const u = (rect.x + insetX) / texture.width;
550
+ const v = (rect.y + insetY) / texture.height;
551
+ const du = (rect.width - 2 * insetX) / texture.width;
552
+ const dv = (rect.height - 2 * insetY) / texture.height;
553
+ this.writeInstance(instances, index, item.x, item.y, item.width, item.height, item.flipX ? u + du : u, item.flipY ? v + dv : v, item.flipX ? -du : du, item.flipY ? -dv : dv, tint[0], tint[1], tint[2], item.opacity, item.rotation, item.unlit ? 1 : 0);
554
+ this.appendBatch(batches, texture, item.blend, index);
555
+ }
556
+ if (hudTexture) {
557
+ const scale = frame.camera.zoom;
558
+ const projected = projectedCamera(frame, interpolation);
559
+ this.writeInstance(instances, items.length, projected.x, projected.y, frame.width / scale, frame.height / scale, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1);
560
+ this.appendBatch(batches, hudTexture, "normal", items.length);
561
+ }
562
+ const projected = projectedCamera(frame, interpolation);
563
+ const camera = new Float32Array([projected.x, projected.y,
564
+ frame.width * (this.canvas.width + overscanPixels * 2) / Math.max(1, this.canvas.width),
565
+ frame.height * (this.canvas.height + overscanPixels * 2) / Math.max(1, this.canvas.height),
566
+ frame.camera.zoom, 0, 0, 0]);
567
+ this.device.queue.writeBuffer(this.cameraBuffer, 0, camera);
568
+ const lighting = new Float32Array(392);
569
+ const sceneLighting = frame.lighting;
570
+ if (sceneLighting) {
571
+ const ambient = parseTint(sceneLighting.ambient.color);
572
+ lighting.set([ambient[0], ambient[1], ambient[2], sceneLighting.ambient.intensity,
573
+ sceneLighting.points.length, 1], 0);
574
+ for (const [index, point] of sceneLighting.points.entries()) {
575
+ const color = parseTint(point.color);
576
+ lighting.set([point.x, point.y, point.radius, point.intensity,
577
+ color[0], color[1], color[2], 0, point.falloff], 8 + index * 12);
578
+ }
579
+ }
580
+ this.device.queue.writeBuffer(this.lightingBuffer, 0, lighting);
581
+ if (count > 0 && this.instanceBuffer) {
582
+ this.device.queue.writeBuffer(this.instanceBuffer, 0, instances);
583
+ }
584
+ const encoder = this.device.createCommandEncoder();
585
+ const hasEffects = this.effects.length > 0;
586
+ const hudAfterEffects = hasEffects && this.hudOrder === "afterEffects";
587
+ if (hasEffects) {
588
+ this.ensureEffectTargets();
589
+ }
590
+ if (!sceneLighting && this.lightTarget) {
591
+ this.lightTarget.destroy();
592
+ this.lightTarget = undefined;
593
+ }
594
+ if (sceneLighting) {
595
+ const target = this.ensureLightTarget(this.canvas.width + (hasEffects ? overscanPixels * 2 : 0), this.canvas.height + (hasEffects ? overscanPixels * 2 : 0));
596
+ const lightGroup = this.device.createBindGroup({ layout: this.lightLayout, entries: [
597
+ { binding: 0, resource: { buffer: this.cameraBuffer } },
598
+ { binding: 1, resource: { buffer: this.lightingBuffer } },
599
+ ] });
600
+ const lightPass = encoder.beginRenderPass({ colorAttachments: [{ view: target.createView(),
601
+ loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 1 } }] });
602
+ lightPass.setPipeline(this.lightPipeline);
603
+ lightPass.setBindGroup(0, lightGroup);
604
+ lightPass.draw(3);
605
+ lightPass.end();
606
+ target.markWritten();
607
+ }
608
+ const lightSampleGroup = this.device.createBindGroup({ layout: this.lightSampleLayout, entries: [
609
+ { binding: 0, resource: this.sampler },
610
+ { binding: 1, resource: (this.lightTarget?.texture ?? this.lightFallback).createView() },
611
+ ] });
612
+ const pass = encoder.beginRenderPass({ colorAttachments: [{
613
+ view: hasEffects ? this.sourceTarget.createView() : this.context.getCurrentTexture().createView(),
614
+ loadOp: "clear",
615
+ storeOp: "store",
616
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
617
+ }] });
618
+ if (this.instanceBuffer) {
619
+ pass.setVertexBuffer(0, this.instanceBuffer);
620
+ pass.setBindGroup(1, lightSampleGroup);
621
+ let blend;
622
+ for (const batch of batches) {
623
+ if (hudAfterEffects && batch.first >= items.length) {
624
+ continue;
625
+ }
626
+ if (batch.blend !== blend) {
627
+ blend = batch.blend;
628
+ pass.setPipeline(blend === "additive"
629
+ ? hasEffects ? this.effectAdditivePipeline : this.additivePipeline
630
+ : hasEffects ? this.effectSpritePipeline : this.pipeline);
631
+ }
632
+ pass.setBindGroup(0, batch.texture.bindGroup);
633
+ pass.draw(6, batch.count, 0, batch.first);
634
+ }
635
+ }
636
+ pass.end();
637
+ if (hasEffects) {
638
+ let source = this.sourceTarget;
639
+ let output = this.effectTarget;
640
+ source.markWritten();
641
+ this.effectContext.uniformRing.beginSubmission();
642
+ for (const effect of this.effects) {
643
+ if (effect.kind === "brightnessContrast") {
644
+ this.effectExecutor.encode({ ctx: this.effectContext, module: colorBrightnessContrastV1,
645
+ encoder, inputs: { source }, output,
646
+ params: { brightness: effect.brightness, contrast: effect.contrast },
647
+ dispatch: { kind: "fragment" } });
648
+ }
649
+ else if (effect.kind === "bloom") {
650
+ this.effectRecipeRunner.encode({ ctx: this.effectContext, module: filtersGlowV1,
651
+ registry: this.effectRegistry, executor: this.effectExecutor, encoder, inputs: { source }, output,
652
+ params: { threshold: effect.threshold, softness: effect.softness,
653
+ radius: effect.radius, intensity: effect.intensity } });
654
+ }
655
+ else {
656
+ const srgbSource = this.srgbTarget;
657
+ const srgbOutput = this.lutTarget;
658
+ const lut = await this.getLutTexture(effect.assetId, effect.size);
659
+ this.convertColor(encoder, source, srgbSource, this.toSrgbPipeline);
660
+ this.effectExecutor.encode({ ctx: this.effectContext, module: colorCubeLutV1,
661
+ encoder, inputs: { source: srgbSource, lut }, output: srgbOutput,
662
+ params: { size: effect.size, intensity: effect.intensity,
663
+ domainMin: d.vec4f(...effect.domainMin, 0), domainMax: d.vec4f(...effect.domainMax, 0) },
664
+ dispatch: { kind: "fragment" } });
665
+ this.convertColor(encoder, srgbOutput, output, this.toLinearPipeline);
666
+ }
667
+ [source, output] = [output, source];
668
+ }
669
+ const presentGroup = this.device.createBindGroup({ layout: this.presentLayout, entries: [
670
+ { binding: 0, resource: this.sampler }, { binding: 1, resource: source.createView() },
671
+ ] });
672
+ const presentPass = encoder.beginRenderPass({ colorAttachments: [{
673
+ view: this.context.getCurrentTexture().createView(), loadOp: "clear", storeOp: "store",
674
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
675
+ }] });
676
+ presentPass.setPipeline(this.presentPipeline);
677
+ presentPass.setBindGroup(0, presentGroup);
678
+ presentPass.draw(3);
679
+ presentPass.end();
680
+ if (hudAfterEffects && hudTexture) {
681
+ const hudGroup = this.device.createBindGroup({ layout: this.presentLayout, entries: [
682
+ { binding: 0, resource: this.sampler }, { binding: 1, resource: hudTexture.texture.createView() },
683
+ ] });
684
+ const hudPass = encoder.beginRenderPass({ colorAttachments: [{
685
+ view: this.context.getCurrentTexture().createView(), loadOp: "load", storeOp: "store",
686
+ }] });
687
+ hudPass.setPipeline(this.hudPipeline);
688
+ hudPass.setBindGroup(0, hudGroup);
689
+ hudPass.draw(3);
690
+ hudPass.end();
691
+ }
692
+ }
693
+ this.device.queue.submit([encoder.finish()]);
694
+ const textureBytes = [...this.textures.values(), ...this.placeholders.values(), this.fallback,
695
+ ...(this.hudTexture ? [this.hudTexture] : [])].reduce((sum, entry) => sum + entry.width * entry.height * 4, 0);
696
+ return {
697
+ backend: this.backend,
698
+ visibleSprites: items.length,
699
+ drawCalls: batches.length + this.effects.reduce((sum, effect) => sum + (effect.kind === "bloom" ? 4 : effect.kind === "lut" ? 3 : 1), 0) + (hasEffects ? 1 : 0) + (sceneLighting ? 1 : 0),
700
+ uploadedBytes: textureResults.reduce((sum, result) => sum + result.uploadedBytes, 0),
701
+ textureBytes,
702
+ targetBytes: (this.sourceTarget && this.effectTarget ? (this.sourceTarget.width * this.sourceTarget.height +
703
+ this.effectTarget.width * this.effectTarget.height + (this.effects.some((effect) => effect.kind === "bloom") ?
704
+ this.sourceTarget.width * this.sourceTarget.height * 3 : 0) + (this.srgbTarget && this.lutTarget ?
705
+ this.srgbTarget.width * this.srgbTarget.height + this.lutTarget.width * this.lutTarget.height : 0)) * 4 : 0) +
706
+ (this.lightTarget ? this.lightTarget.width * this.lightTarget.height * 8 : 0),
707
+ instanceBufferBytes: this.instanceCapacity * INSTANCE_FLOATS * 4,
708
+ };
709
+ }
710
+ updateHud(frame) {
711
+ if (frame.hud.length === 0) {
712
+ return undefined;
713
+ }
714
+ const key = JSON.stringify([this.canvas.width, this.canvas.height, frame.hud]);
715
+ if (key !== this.hudKey) {
716
+ const canvas = this.hudCanvas ?? document.createElement("canvas");
717
+ canvas.width = this.canvas.width;
718
+ canvas.height = this.canvas.height;
719
+ const context = canvas.getContext("2d");
720
+ if (!context) {
721
+ throw new Error("HUD canvas is unavailable");
722
+ }
723
+ context.clearRect(0, 0, canvas.width, canvas.height);
724
+ paintHud(context, frame.hud, 1, frame.gameId);
725
+ this.hudTexture?.texture.destroy();
726
+ this.hudTexture = this.upload(canvas);
727
+ this.hudCanvas = canvas;
728
+ this.hudKey = key;
729
+ }
730
+ return this.hudTexture;
731
+ }
732
+ appendBatch(batches, texture, blend, index) {
733
+ const last = batches[batches.length - 1];
734
+ if (last?.texture === texture && last.blend === blend) {
735
+ last.count++;
736
+ }
737
+ else {
738
+ batches.push({ texture, blend, first: index, count: 1 });
739
+ }
740
+ }
741
+ writeInstance(target, index, ...values) {
742
+ target.set(values, index * INSTANCE_FLOATS);
743
+ }
744
+ ensureInstanceCapacity(count) {
745
+ if (count <= this.instanceCapacity) {
746
+ return;
747
+ }
748
+ this.instanceBuffer?.destroy();
749
+ this.instanceCapacity = Math.max(count, this.instanceCapacity * 2, 64);
750
+ this.instanceBuffer = this.device.createBuffer({ size: this.instanceCapacity * INSTANCE_FLOATS * 4, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
751
+ }
752
+ resize(width, height) {
753
+ this.canvas.width = Math.max(1, Math.floor(width));
754
+ this.canvas.height = Math.max(1, Math.floor(height));
755
+ this.context.configure({ device: this.device, format: this.format, alphaMode: "premultiplied" });
756
+ this.sourceTarget?.destroy();
757
+ this.effectTarget?.destroy();
758
+ this.lightTarget?.destroy();
759
+ this.srgbTarget?.destroy();
760
+ this.lutTarget?.destroy();
761
+ this.effectContext.scratch.dispose();
762
+ this.sourceTarget = undefined;
763
+ this.effectTarget = undefined;
764
+ this.lightTarget = undefined;
765
+ this.srgbTarget = undefined;
766
+ this.lutTarget = undefined;
767
+ }
768
+ invalidateAsset(assetId) {
769
+ this.assets.invalidate(assetId);
770
+ const texture = this.textures.get(assetId);
771
+ texture?.texture.destroy();
772
+ this.textures.delete(assetId);
773
+ this.validatedLuts.delete(assetId);
774
+ }
775
+ dispose() {
776
+ if (this.disposed) {
777
+ return;
778
+ }
779
+ this.disposed = true;
780
+ this.instanceBuffer?.destroy();
781
+ this.sourceTarget?.destroy();
782
+ this.effectTarget?.destroy();
783
+ this.srgbTarget?.destroy();
784
+ this.lutTarget?.destroy();
785
+ this.effectContext.scratch.dispose();
786
+ this.effectContext.uniformRing.dispose();
787
+ this.effectContext.root.destroy();
788
+ this.cameraBuffer.destroy();
789
+ this.lightingBuffer.destroy();
790
+ this.lightTarget?.destroy();
791
+ this.lightFallback.destroy();
792
+ this.fallback.texture.destroy();
793
+ this.hudTexture?.texture.destroy();
794
+ for (const entry of this.placeholders.values()) {
795
+ entry.texture.destroy();
796
+ }
797
+ this.placeholders.clear();
798
+ for (const entry of this.textures.values()) {
799
+ entry.texture.destroy();
800
+ }
801
+ this.textures.clear();
802
+ this.assets.clear();
803
+ this.context.unconfigure();
804
+ this.device.destroy();
805
+ }
806
+ }
807
+ //# sourceMappingURL=webgpu.js.map