@nodetool-ai/game-renderer 0.8.4 → 0.8.6

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.
@@ -1,6 +1,6 @@
1
- import type { Batch, TextureEntry, Blend } from "./types.js";
1
+ import type { Batch, TextureEntry, Blend, Sampling } from "./types.js";
2
2
  export declare const INSTANCE_FLOATS = 14;
3
3
  export declare const SHADER = "\nstruct Camera { center: vec2f, viewport: vec2f, zoom: f32, padding0: f32, padding1: f32, padding2: f32 };\noverride linearizeInput: bool = false;\nfn srgbToLinear(rgb: vec3f) -> vec3f {\n return select(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3f(2.4)), rgb > vec3f(0.04045));\n}\nfn linearToSrgb(rgb: vec3f) -> vec3f {\n return select(rgb * 12.92, 1.055 * pow(rgb, vec3f(1.0 / 2.4)) - 0.055, rgb > vec3f(0.0031308));\n}\n@group(0) @binding(0) var<uniform> camera: Camera;\n@group(0) @binding(1) var atlasSampler: sampler;\n@group(0) @binding(2) var atlas: texture_2d<f32>;\n@group(1) @binding(0) var lightSampler: sampler;\n@group(1) @binding(1) var lightTexture: texture_2d<f32>;\n\nstruct VertexInput {\n @builtin(vertex_index) corner: u32,\n @location(0) center: vec2f,\n @location(1) size: vec2f,\n @location(2) uv: vec4f,\n @location(3) color: vec4f,\n @location(4) rotation: f32,\n @location(5) unlit: f32,\n};\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n @location(3) @interpolate(flat) unlit: f32,\n @location(4) lightUv: vec2f,\n};\n@vertex fn vertexMain(input: VertexInput) -> VertexOutput {\n let corners = array<vec2f, 6>(\n vec2f(-0.5, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, 0.5),\n vec2f(0.5, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5));\n let local = corners[input.corner] * input.size;\n let sine = sin(input.rotation);\n let cosine = cos(input.rotation);\n let rotated = vec2f(local.x * cosine - local.y * sine, local.x * sine + local.y * cosine);\n let world = input.center + rotated;\n let clip = (world - camera.center) * camera.zoom * 2.0 / camera.viewport;\n var output: VertexOutput;\n output.position = vec4f(clip, 0.0, 1.0);\n let uvCorner = corners[input.corner] + vec2f(0.5, 0.5);\n output.uv = input.uv.xy + vec2f(uvCorner.x, 1.0 - uvCorner.y) * input.uv.zw;\n output.color = input.color;\n output.unlit = input.unlit;\n output.lightUv = clip * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);\n return output;\n}\n@fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n let sampled = textureSample(atlas, atlasSampler, input.uv) * input.color;\n var straight = select(sampled.rgb, srgbToLinear(sampled.rgb), linearizeInput);\n if (input.unlit < 0.5) {\n let irradiance = textureSampleLevel(lightTexture, lightSampler, input.lightUv, 0.0).rgb;\n let lit = clamp(srgbToLinear(sampled.rgb) * irradiance, vec3f(0.0), vec3f(1.0));\n straight = select(linearToSrgb(lit), lit, linearizeInput);\n }\n return vec4f(straight * sampled.a, sampled.a);\n}";
4
- export declare function appendBatch(batches: Batch[], texture: TextureEntry, blend: Blend, index: number): void;
4
+ export declare function appendBatch(batches: Batch[], texture: TextureEntry, blend: Blend, index: number, sampling: Sampling): void;
5
5
  export declare function writeInstance(target: Float32Array, index: number, ...values: number[]): void;
6
6
  //# sourceMappingURL=sprites.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sprites.d.ts","sourceRoot":"","sources":["../../src/webgpu/sprites.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC7D,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,MAAM,0iFA2DjB,CAAC;AAEH,wBAAgB,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAOpG;AAEH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAE1F"}
1
+ {"version":3,"file":"sprites.d.ts","sourceRoot":"","sources":["../../src/webgpu/sprites.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACvE,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,MAAM,0iFA2DjB,CAAC;AAEH,wBAAgB,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAQxH;AAEH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAE1F"}
@@ -59,13 +59,14 @@ struct VertexOutput {
59
59
  }
60
60
  return vec4f(straight * sampled.a, sampled.a);
61
61
  }`;
62
- export function appendBatch(batches, texture, blend, index) {
62
+ export function appendBatch(batches, texture, blend, index, sampling) {
63
+ const bindGroup = texture.bindGroups[sampling];
63
64
  const last = batches[batches.length - 1];
64
- if (last?.texture === texture && last.blend === blend) {
65
+ if (last?.texture === texture && last.bindGroup === bindGroup && last.blend === blend) {
65
66
  last.count++;
66
67
  }
67
68
  else {
68
- batches.push({ texture, blend, first: index, count: 1 });
69
+ batches.push({ texture, bindGroup, blend, first: index, count: 1 });
69
70
  }
70
71
  }
71
72
  export function writeInstance(target, index, ...values) {
@@ -1 +1 @@
1
- {"version":3,"file":"sprites.js","sourceRoot":"","sources":["../../src/webgpu/sprites.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAClC,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DpB,CAAC;AAEH,MAAM,UAAU,WAAW,CAAC,OAAgB,EAAE,OAAqB,EAAE,KAAY,EAAE,KAAa;IAC5F,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,IAAI,EAAE,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACtD,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAEH,MAAM,UAAU,aAAa,CAAC,MAAoB,EAAE,KAAa,EAAE,GAAG,MAAgB;IAClF,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,GAAG,eAAe,CAAC,CAAC;AAC9C,CAAC"}
1
+ {"version":3,"file":"sprites.js","sourceRoot":"","sources":["../../src/webgpu/sprites.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAClC,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DpB,CAAC;AAEH,MAAM,UAAU,WAAW,CAAC,OAAgB,EAAE,OAAqB,EAAE,KAAY,EAAE,KAAa,EAAE,QAAkB;IAChH,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,IAAI,EAAE,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACtF,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAEH,MAAM,UAAU,aAAa,CAAC,MAAoB,EAAE,KAAa,EAAE,GAAG,MAAgB;IAClF,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,GAAG,eAAe,CAAC,CAAC;AAC9C,CAAC"}
@@ -1,12 +1,14 @@
1
+ export type Sampling = "nearest" | "linear";
1
2
  export interface TextureEntry {
2
3
  readonly texture: GPUTexture;
3
- readonly bindGroup: GPUBindGroup;
4
+ readonly bindGroups: Readonly<Record<Sampling, GPUBindGroup>>;
4
5
  readonly width: number;
5
6
  readonly height: number;
6
7
  }
7
8
  export type Blend = "normal" | "additive";
8
9
  export interface Batch {
9
10
  readonly texture: TextureEntry;
11
+ readonly bindGroup: GPUBindGroup;
10
12
  readonly blend: Blend;
11
13
  readonly first: number;
12
14
  count: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/webgpu/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE1C,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/webgpu/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE1C,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf"}
package/dist/webgpu.d.ts CHANGED
@@ -43,7 +43,8 @@ export declare class WebGPUGameRenderer implements GameRenderer {
43
43
  private readonly lightFallback;
44
44
  private lightTarget;
45
45
  private readonly textures;
46
- private readonly validatedLuts;
46
+ private readonly assetGenerations;
47
+ private readonly liveAssetTextures;
47
48
  private readonly fallback;
48
49
  private readonly particleDot;
49
50
  private readonly placeholders;
@@ -52,6 +53,7 @@ export declare class WebGPUGameRenderer implements GameRenderer {
52
53
  private hudCanvas;
53
54
  private hudTexture;
54
55
  private hudKey;
56
+ private hudRevision;
55
57
  private disposed;
56
58
  private lost;
57
59
  constructor(canvas: HTMLCanvasElement, device: GPUDevice, context: GPUCanvasContext, format: GPUTextureFormat, assets: AssetCache, adapterType?: "hardware" | "software" | "unknown");
@@ -62,11 +64,18 @@ export declare class WebGPUGameRenderer implements GameRenderer {
62
64
  private convertColor;
63
65
  private textureEntry;
64
66
  private upload;
67
+ private assertUsable;
68
+ private assetGeneration;
69
+ private retireGeneration;
70
+ private releaseFrameAssets;
71
+ private uploadGeneration;
72
+ private generationTexture;
65
73
  private getTexture;
66
74
  private getLutTexture;
67
75
  private placeholder;
68
76
  render(frame: GameRenderFrame, interpolation: number, particles?: GameParticleField): Promise<GameRendererStats>;
69
77
  private renderFrame;
78
+ private renderAssetFrame;
70
79
  private updateHud;
71
80
  private ensureInstanceCapacity;
72
81
  resize(width: number, height: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"webgpu.d.ts","sourceRoot":"","sources":["../src/webgpu.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webgpu.d.ts","sourceRoot":"","sources":["../src/webgpu.ts"],"names":[],"mappings":"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"}
package/dist/webgpu.js CHANGED
@@ -49,7 +49,8 @@ export class WebGPUGameRenderer {
49
49
  lightFallback;
50
50
  lightTarget;
51
51
  textures = new Map();
52
- validatedLuts = new Set();
52
+ assetGenerations = new Set();
53
+ liveAssetTextures = new Set();
53
54
  fallback;
54
55
  particleDot;
55
56
  placeholders = new Map();
@@ -58,6 +59,7 @@ export class WebGPUGameRenderer {
58
59
  hudCanvas;
59
60
  hudTexture;
60
61
  hudKey = "";
62
+ hudRevision = 0;
61
63
  disposed = false;
62
64
  lost = false;
63
65
  constructor(canvas, device, context, format, assets, adapterType = "unknown") {
@@ -173,7 +175,7 @@ export class WebGPUGameRenderer {
173
175
  const dotTexture = device.createTexture({ size: [PARTICLE_DOT_SIZE, PARTICLE_DOT_SIZE], format: "rgba8unorm",
174
176
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
175
177
  device.queue.writeTexture({ texture: dotTexture }, particleDotPixels(), { bytesPerRow: PARTICLE_DOT_SIZE * 4 }, [PARTICLE_DOT_SIZE, PARTICLE_DOT_SIZE]);
176
- this.particleDot = this.textureEntry(dotTexture, PARTICLE_DOT_SIZE, PARTICLE_DOT_SIZE, "linear");
178
+ this.particleDot = this.textureEntry(dotTexture, PARTICLE_DOT_SIZE, PARTICLE_DOT_SIZE);
177
179
  void device.lost.then(() => { this.lost = true; });
178
180
  }
179
181
  get capabilities() {
@@ -290,22 +292,24 @@ export class WebGPUGameRenderer {
290
292
  pass.end();
291
293
  output.markWritten();
292
294
  }
293
- textureEntry(texture, width, height, sampling = "nearest") {
295
+ textureEntry(texture, width, height) {
296
+ const view = texture.createView();
297
+ const bindGroup = (sampler) => this.device.createBindGroup({
298
+ layout: this.pipeline.getBindGroupLayout(0),
299
+ entries: [
300
+ { binding: 0, resource: { buffer: this.cameraBuffer } },
301
+ { binding: 1, resource: sampler },
302
+ { binding: 2, resource: view },
303
+ ],
304
+ });
294
305
  return {
295
306
  texture,
296
307
  width,
297
308
  height,
298
- bindGroup: this.device.createBindGroup({
299
- layout: this.pipeline.getBindGroupLayout(0),
300
- entries: [
301
- { binding: 0, resource: { buffer: this.cameraBuffer } },
302
- { binding: 1, resource: sampling === "linear" ? this.linearSampler : this.sampler },
303
- { binding: 2, resource: texture.createView() },
304
- ],
305
- }),
309
+ bindGroups: { nearest: bindGroup(this.sampler), linear: bindGroup(this.linearSampler) },
306
310
  };
307
311
  }
308
- upload(image, sampling = "nearest") {
312
+ upload(image) {
309
313
  const width = image instanceof HTMLCanvasElement ? image.width : imageWidth(image);
310
314
  const height = image instanceof HTMLCanvasElement ? image.height : imageHeight(image);
311
315
  if (width <= 0 || height <= 0) {
@@ -314,31 +318,97 @@ export class WebGPUGameRenderer {
314
318
  // copyExternalImageToTexture requires RENDER_ATTACHMENT on the destination as well as COPY_DST.
315
319
  const texture = this.device.createTexture({ size: [width, height], format: "rgba8unorm",
316
320
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT });
317
- this.device.queue.copyExternalImageToTexture({ source: image }, { texture, premultipliedAlpha: false }, [width, height]);
318
- return this.textureEntry(texture, width, height, sampling);
321
+ try {
322
+ this.device.queue.copyExternalImageToTexture({ source: image }, { texture, premultipliedAlpha: false }, [width, height]);
323
+ return this.textureEntry(texture, width, height);
324
+ }
325
+ catch (error) {
326
+ texture.destroy();
327
+ throw error;
328
+ }
319
329
  }
320
- async getTexture(assetId, sampling) {
321
- if (assetId === PARTICLE_DOT_ASSET) {
322
- return { texture: this.particleDot, uploadedBytes: 0 };
330
+ assertUsable() {
331
+ if (this.disposed || this.lost) {
332
+ throw new Error(this.lost ? "WebGPU device was lost" : "Game renderer is disposed");
323
333
  }
324
- const cached = this.textures.get(assetId);
325
- if (cached) {
326
- return { texture: cached, uploadedBytes: 0 };
334
+ }
335
+ assetGeneration(assetId, frame) {
336
+ this.assertUsable();
337
+ let generation = this.textures.get(assetId);
338
+ if (!generation) {
339
+ generation = { image: this.assets.get(assetId), readers: 0, retired: false, opaque: false };
340
+ this.textures.set(assetId, generation);
341
+ this.assetGenerations.add(generation);
342
+ }
343
+ if (!frame.generations.has(generation)) {
344
+ frame.generations.add(generation);
345
+ generation.readers++;
346
+ }
347
+ return generation;
348
+ }
349
+ retireGeneration(generation) {
350
+ if (!generation.retired || generation.readers !== 0) {
351
+ return;
352
+ }
353
+ if (generation.entry && this.liveAssetTextures.delete(generation.entry)) {
354
+ generation.entry.texture.destroy();
355
+ }
356
+ generation.entry = undefined;
357
+ this.assetGenerations.delete(generation);
358
+ }
359
+ releaseFrameAssets(frame) {
360
+ for (const generation of frame.generations) {
361
+ generation.readers--;
362
+ this.retireGeneration(generation);
363
+ }
364
+ frame.generations.clear();
365
+ }
366
+ async uploadGeneration(assetId, generation, frame) {
367
+ const image = await generation.image;
368
+ this.assertUsable();
369
+ if (generation.retired && generation.readers === 0) {
370
+ throw new Error(`Asset ${assetId} was invalidated before upload`);
327
371
  }
328
- const image = await this.assets.get(assetId);
329
372
  if (!image) {
330
- return { texture: this.placeholder(assetId), uploadedBytes: 0 };
373
+ return this.placeholder(assetId);
374
+ }
375
+ const texture = this.upload(image);
376
+ if (texture !== this.fallback) {
377
+ generation.entry = texture;
378
+ this.liveAssetTextures.add(texture);
379
+ frame.uploadedBytes += texture.width * texture.height * 4;
380
+ }
381
+ return texture;
382
+ }
383
+ generationTexture(assetId, generation, frame) {
384
+ if (!generation.upload) {
385
+ const upload = this.uploadGeneration(assetId, generation, frame);
386
+ generation.upload = upload;
387
+ void upload.catch(() => {
388
+ if (generation.upload === upload && !generation.entry) {
389
+ generation.upload = undefined;
390
+ }
391
+ });
392
+ }
393
+ return generation.upload;
394
+ }
395
+ async getTexture(assetId, frame) {
396
+ if (assetId === PARTICLE_DOT_ASSET) {
397
+ return this.particleDot;
331
398
  }
332
- const texture = this.upload(image, sampling);
333
- this.textures.set(assetId, texture);
334
- return { texture, uploadedBytes: texture.width * texture.height * 4 };
399
+ const generation = this.assetGeneration(assetId, frame);
400
+ const texture = await this.generationTexture(assetId, generation, frame);
401
+ this.assertUsable();
402
+ return texture;
335
403
  }
336
- async getLutTexture(assetId, size) {
337
- const image = await this.assets.get(assetId);
404
+ async getLutTexture(assetId, size, frame) {
405
+ const generation = this.assetGeneration(assetId, frame);
406
+ const image = await generation.image;
407
+ this.assertUsable();
338
408
  if (!image || imageWidth(image) !== size * size || imageHeight(image) !== size) {
339
409
  throw new Error(`LUT ${assetId} is missing or has invalid dimensions`);
340
410
  }
341
- if (!this.validatedLuts.has(assetId)) {
411
+ if (!generation.opaque) {
342
412
  const canvas = document.createElement("canvas");
343
413
  canvas.width = imageWidth(image);
344
414
  canvas.height = imageHeight(image);
@@ -353,9 +423,10 @@ export class WebGPUGameRenderer {
353
423
  throw new Error(`LUT ${assetId} must be opaque`);
354
424
  }
355
425
  }
356
- this.validatedLuts.add(assetId);
426
+ generation.opaque = true;
357
427
  }
358
- const { texture } = await this.getTexture(assetId, "nearest");
428
+ const texture = await this.generationTexture(assetId, generation, frame);
429
+ this.assertUsable();
359
430
  return new LabeledTexture(texture.texture, { label: `game-lut-${assetId}`, format: "rgba8unorm",
360
431
  width: texture.width, height: texture.height,
361
432
  meta: { colorSpace: "srgb", alpha: "premultiplied", bindingKind: "texture_2d" } });
@@ -402,15 +473,26 @@ export class WebGPUGameRenderer {
402
473
  }
403
474
  }
404
475
  async renderFrame(frame, interpolation, particles) {
476
+ const assets = { generations: new Set(), uploadedBytes: 0 };
477
+ try {
478
+ return await this.renderAssetFrame(frame, interpolation, assets, particles);
479
+ }
480
+ finally {
481
+ this.releaseFrameAssets(assets);
482
+ }
483
+ }
484
+ async renderAssetFrame(frame, interpolation, assets, particles) {
405
485
  const bloomRadius = this.effects.reduce((maximum, effect) => effect.kind === "bloom" ? Math.max(maximum, effect.radius) : maximum, 0);
406
486
  const overscanPixels = Math.ceil(bloomRadius * 2);
407
487
  const overscanWorld = overscanPixels * frame.width / (Math.max(1, this.canvas.width) * projectedCamera(frame, interpolation).zoom);
408
488
  const items = visibleItems(frame, interpolation, overscanWorld, particles && { field: particles });
409
- const sampling = new Map(items.map((item) => [item.assetId, item.sampling]));
410
- const assetIds = [...sampling.keys()];
411
- const textureResults = await Promise.all(assetIds.map((assetId) => this.getTexture(assetId, sampling.get(assetId) ?? "nearest")));
489
+ const assetIds = [...new Set(items.map((item) => item.assetId))];
490
+ const textureResults = await Promise.all(assetIds.map((assetId) => this.getTexture(assetId, assets)));
412
491
  const textureById = new Map(assetIds.map((assetId, index) => [assetId, textureResults[index]]));
413
- const hudTexture = this.updateHud(frame, await loadGameUiImages(frame, (assetId) => this.assets.get(assetId)));
492
+ const hudRevision = this.hudRevision;
493
+ const hudImages = await loadGameUiImages(frame, (assetId) => this.assets.get(assetId));
494
+ this.assertUsable();
495
+ const hudTexture = this.updateHud(frame, hudImages, hudRevision);
414
496
  const count = items.length + (hudTexture ? 1 : 0);
415
497
  this.ensureInstanceCapacity(count);
416
498
  const instances = new Float32Array(count * INSTANCE_FLOATS);
@@ -421,7 +503,7 @@ export class WebGPUGameRenderer {
421
503
  if (!item || !result) {
422
504
  continue;
423
505
  }
424
- const texture = result.texture;
506
+ const texture = result;
425
507
  const rect = texture.width === 1 && texture.height === 1 ? { x: 0, y: 0, width: 1, height: 1 } : sourceRect(item, texture.width, texture.height);
426
508
  const tint = parseTint(item.tint);
427
509
  const subRect = item.frame !== undefined || item.cell !== undefined;
@@ -432,13 +514,13 @@ export class WebGPUGameRenderer {
432
514
  const du = (rect.width - 2 * insetX) / texture.width;
433
515
  const dv = (rect.height - 2 * insetY) / texture.height;
434
516
  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);
435
- appendBatch(batches, texture, item.blend, index);
517
+ appendBatch(batches, texture, item.blend, index, item.sampling);
436
518
  }
437
519
  if (hudTexture) {
438
520
  const scale = frame.camera.zoom;
439
521
  const projected = projectedCamera(frame, interpolation);
440
522
  writeInstance(instances, items.length, projected.x, projected.y, frame.width / scale, frame.height / scale, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1);
441
- appendBatch(batches, hudTexture, "normal", items.length);
523
+ appendBatch(batches, hudTexture, "normal", items.length, "nearest");
442
524
  }
443
525
  const projected = projectedCamera(frame, interpolation);
444
526
  const camera = new Float32Array([projected.x, projected.y,
@@ -504,7 +586,7 @@ export class WebGPUGameRenderer {
504
586
  ? hasEffects ? this.effectAdditivePipeline : this.additivePipeline
505
587
  : hasEffects ? this.effectSpritePipeline : this.pipeline);
506
588
  }
507
- pass.setBindGroup(0, batch.texture.bindGroup);
589
+ pass.setBindGroup(0, batch.bindGroup);
508
590
  pass.draw(6, batch.count, 0, batch.first);
509
591
  }
510
592
  }
@@ -518,7 +600,7 @@ export class WebGPUGameRenderer {
518
600
  effectContext: this.effectContext, effectExecutor: this.effectExecutor,
519
601
  effectRecipeRunner: this.effectRecipeRunner, effectRegistry: this.effectRegistry,
520
602
  srgbTarget: this.srgbTarget, lutTarget: this.lutTarget, toSrgbPipeline: this.toSrgbPipeline,
521
- toLinearPipeline: this.toLinearPipeline, getLutTexture: (id, size) => this.getLutTexture(id, size),
603
+ toLinearPipeline: this.toLinearPipeline, getLutTexture: (id, size) => this.getLutTexture(id, size, assets),
522
604
  convertColor: (command, input, target, pipeline) => this.convertColor(command, input, target, pipeline) });
523
605
  const presentGroup = this.device.createBindGroup({ layout: this.presentLayout, entries: [
524
606
  { binding: 0, resource: this.sampler }, { binding: 1, resource: source.createView() },
@@ -548,14 +630,16 @@ export class WebGPUGameRenderer {
548
630
  }
549
631
  }
550
632
  });
633
+ this.assertUsable();
551
634
  this.device.queue.submit([encoder.finish()]);
552
- const textureBytes = [...this.textures.values(), ...this.placeholders.values(), this.fallback, this.particleDot,
635
+ this.releaseFrameAssets(assets);
636
+ const textureBytes = [...this.liveAssetTextures, ...this.placeholders.values(), this.fallback, this.particleDot,
553
637
  ...(this.hudTexture ? [this.hudTexture] : [])].reduce((sum, entry) => sum + entry.width * entry.height * 4, 0);
554
638
  return {
555
639
  backend: this.backend,
556
640
  visibleSprites: items.length,
557
641
  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),
558
- uploadedBytes: textureResults.reduce((sum, result) => sum + result.uploadedBytes, 0),
642
+ uploadedBytes: assets.uploadedBytes,
559
643
  textureBytes,
560
644
  targetBytes: (this.sourceTarget && this.effectTarget ? (this.sourceTarget.width * this.sourceTarget.height +
561
645
  this.effectTarget.width * this.effectTarget.height + (this.effects.some((effect) => effect.kind === "bloom") ?
@@ -565,11 +649,12 @@ export class WebGPUGameRenderer {
565
649
  instanceBufferBytes: this.instanceCapacity * INSTANCE_FLOATS * 4,
566
650
  };
567
651
  }
568
- updateHud(frame, images) {
652
+ updateHud(frame, images, revision) {
569
653
  if (frame.hud.length === 0 && !frame.ui) {
570
654
  return undefined;
571
655
  }
572
- const key = JSON.stringify([this.canvas.width, this.canvas.height, frame.hud, frame.ui, [...images.keys()]]);
656
+ const key = JSON.stringify([this.canvas.width, this.canvas.height, frame.gameId, frame.fonts,
657
+ frame.hud, frame.ui, [...images.keys()], revision]);
573
658
  if (key !== this.hudKey) {
574
659
  const canvas = this.hudCanvas ?? document.createElement("canvas");
575
660
  paintWebGPUHud(canvas, this.canvas.width, this.canvas.height, frame, images);
@@ -605,11 +690,14 @@ export class WebGPUGameRenderer {
605
690
  this.lutTarget = undefined;
606
691
  }
607
692
  invalidateAsset(assetId) {
693
+ this.hudRevision++;
608
694
  this.assets.invalidate(assetId);
609
- const texture = this.textures.get(assetId);
610
- texture?.texture.destroy();
695
+ const generation = this.textures.get(assetId);
611
696
  this.textures.delete(assetId);
612
- this.validatedLuts.delete(assetId);
697
+ if (generation) {
698
+ generation.retired = true;
699
+ this.retireGeneration(generation);
700
+ }
613
701
  }
614
702
  dispose() {
615
703
  if (this.disposed) {
@@ -635,9 +723,14 @@ export class WebGPUGameRenderer {
635
723
  entry.texture.destroy();
636
724
  }
637
725
  this.placeholders.clear();
638
- for (const entry of this.textures.values()) {
726
+ for (const generation of this.assetGenerations) {
727
+ generation.retired = true;
728
+ }
729
+ for (const entry of this.liveAssetTextures) {
639
730
  entry.texture.destroy();
640
731
  }
732
+ this.liveAssetTextures.clear();
733
+ this.assetGenerations.clear();
641
734
  this.textures.clear();
642
735
  this.assets.clear();
643
736
  this.context.unconfigure();