melonjs 20.9.0 → 20.10.0

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 (39) hide show
  1. package/build/application/settings.d.ts +12 -8
  2. package/build/application/settings.d.ts.map +1 -1
  3. package/build/index.d.ts +2 -1
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +1616 -225
  6. package/build/index.js.map +4 -4
  7. package/build/renderable/container.d.ts.map +1 -1
  8. package/build/renderable/renderable.d.ts +6 -0
  9. package/build/renderable/renderable.d.ts.map +1 -1
  10. package/build/renderable/sprite.d.ts.map +1 -1
  11. package/build/tweens/tween.d.ts.map +1 -1
  12. package/build/video/effects/shadereffect.d.ts.map +1 -1
  13. package/build/video/effects/squish.d.ts +74 -0
  14. package/build/video/effects/squish.d.ts.map +1 -0
  15. package/build/video/renderer.d.ts +16 -3
  16. package/build/video/renderer.d.ts.map +1 -1
  17. package/build/video/rendertarget/render_target_pool.d.ts.map +1 -1
  18. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  19. package/build/video/webgl/batchers/material_batcher.d.ts +5 -0
  20. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  21. package/build/video/webgl/batchers/mesh_batcher.d.ts +21 -0
  22. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  23. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  24. package/build/video/webgl/buffer/deletable.d.ts +15 -0
  25. package/build/video/webgl/buffer/deletable.d.ts.map +1 -0
  26. package/build/video/webgl/buffer/instance_buffer.d.ts.map +1 -1
  27. package/build/video/webgl/buffer/retained_geometry.d.ts.map +1 -1
  28. package/build/video/webgl/buffer/uniformblock.d.ts.map +1 -1
  29. package/build/video/webgl/buffer/vertexstate.d.ts.map +1 -1
  30. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  31. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  32. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  33. package/build/video/webgpu/texture/compressed.d.ts.map +1 -1
  34. package/build/video/webgpu/texture/store.d.ts.map +1 -1
  35. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/skills/melonjs-effects-and-shaders/SKILL.md +50 -6
  38. package/skills/melonjs-loading-assets/SKILL.md +22 -4
  39. package/skills/melonjs-performance/SKILL.md +7 -3
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 20.9.0
2
+ * melonJS Game Engine - 20.10.0
3
3
  * http://www.melonjs.org
4
4
  * melonjs is licensed under the MIT License.
5
5
  * http://www.opensource.org/licenses/mit-license
@@ -10130,6 +10130,9 @@ var Tween = class {
10130
10130
  * @internal
10131
10131
  */
10132
10132
  _onTick(timestamp) {
10133
+ if (!this._isRunning) {
10134
+ return;
10135
+ }
10133
10136
  if (!this._isPaused || this.updateWhenPaused) {
10134
10137
  const dt = timestamp - this._lastTick;
10135
10138
  this._lastTick = timestamp;
@@ -13370,10 +13373,17 @@ var Renderable = class _Renderable extends Rect {
13370
13373
  this.postEffects.length = 0;
13371
13374
  } else {
13372
13375
  this.postEffects = [value];
13376
+ value._renderer?._queueWarm?.(this);
13373
13377
  }
13374
13378
  }
13375
13379
  /**
13376
13380
  * Add a post-processing shader effect to this renderable.
13381
+ *
13382
+ * With the `prewarm` application setting on (the default), what the
13383
+ * effect will need to draw here is compiled from this call, off the
13384
+ * frame, rather than on the frame the renderable first draws with it.
13385
+ * That takes a moment to finish, so attach effects when the renderable
13386
+ * is built (a pool, a stage starting) rather than when it is shown.
13377
13387
  * @param {GLShader|ShaderEffect} effect - the effect to add
13378
13388
  * @returns {GLShader|ShaderEffect} the added effect
13379
13389
  * @example
@@ -13381,6 +13391,7 @@ var Renderable = class _Renderable extends Rect {
13381
13391
  */
13382
13392
  addPostEffect(effect) {
13383
13393
  this.postEffects.push(effect);
13394
+ effect?._renderer?._queueWarm?.(this);
13384
13395
  return effect;
13385
13396
  }
13386
13397
  /**
@@ -15409,6 +15420,7 @@ var ShaderEffect = class _ShaderEffect {
15409
15420
  this._renderer = renderer;
15410
15421
  this._fragmentBody = body;
15411
15422
  this._precision = precision;
15423
+ this._vertexScale = { x: 1, y: 1 };
15412
15424
  const bodies = typeof body === "string" ? { glsl: body } : body ?? {};
15413
15425
  const language2 = renderer.shaderLanguage;
15414
15426
  const source = language2 !== null ? bodies[language2] : void 0;
@@ -17562,6 +17574,7 @@ var FrameAnimation = class {
17562
17574
 
17563
17575
  // src/renderable/sprite.js
17564
17576
  var FLICKER_INTERVAL_MS = 33;
17577
+ var VIDEO_GESTURES = ["touchstart", "touchend", "click", "keydown"];
17565
17578
  var Sprite = class extends Renderable {
17566
17579
  /**
17567
17580
  * @param {number} x - the x coordinates of the sprite object
@@ -17668,8 +17681,12 @@ var Sprite = class extends Renderable {
17668
17681
  }
17669
17682
  this.removeStatePauseListener = on(STATE_PAUSE, () => {
17670
17683
  this.image.pause();
17684
+ this._cancelVideoPlay();
17671
17685
  });
17672
17686
  this._lastVideoFrameVersion = -1;
17687
+ this._videoPlayPending = false;
17688
+ this._videoPlayRequest = 0;
17689
+ this._videoGestureListener = void 0;
17673
17690
  if (typeof this.image.requestVideoFrameCallback === "function") {
17674
17691
  this.image.version ??= 0;
17675
17692
  const tick2 = () => {
@@ -18144,8 +18161,8 @@ var Sprite = class extends Renderable {
18144
18161
  if (this.isVideo) {
18145
18162
  if (this.animationpause) {
18146
18163
  this.image.pause();
18147
- } else if (this.image.paused) {
18148
- this.image.play();
18164
+ } else if (this.image.paused && this._videoPlayPending !== true) {
18165
+ this._playVideo();
18149
18166
  }
18150
18167
  if (this.image.version === void 0) {
18151
18168
  if (!this.image.paused) {
@@ -18241,6 +18258,86 @@ var Sprite = class extends Renderable {
18241
18258
  // dw,dh
18242
18259
  );
18243
18260
  }
18261
+ /**
18262
+ * Start the video and handle the browser's answer. Before the user has
18263
+ * interacted with the page, browsers refuse playback: calling `play()`
18264
+ * again on every frame only logged an uncaught error per frame, so a
18265
+ * refusal now waits for the next gesture instead. Any other failure
18266
+ * (e.g. a `pause()` interrupting the request) retries on the next frame.
18267
+ * @ignore
18268
+ * @internal
18269
+ */
18270
+ _playVideo() {
18271
+ const request = ++this._videoPlayRequest;
18272
+ this._videoPlayPending = true;
18273
+ Promise.resolve(this.image.play()).then(
18274
+ () => {
18275
+ this._videoPlayPending = false;
18276
+ },
18277
+ (error) => {
18278
+ if (request !== this._videoPlayRequest) {
18279
+ return;
18280
+ }
18281
+ if (error?.name === "NotAllowedError") {
18282
+ this._awaitVideoGesture();
18283
+ } else {
18284
+ this._videoPlayPending = false;
18285
+ }
18286
+ }
18287
+ );
18288
+ }
18289
+ /**
18290
+ * Wait for the next user gesture and start the video from inside its
18291
+ * handler, the moment a browser that refused playback allows it.
18292
+ * @ignore
18293
+ * @internal
18294
+ */
18295
+ _awaitVideoGesture() {
18296
+ if (this._videoGestureListener !== void 0) {
18297
+ return;
18298
+ }
18299
+ this._videoGestureListener = () => {
18300
+ this._removeVideoGestureListener();
18301
+ this._videoPlayPending = false;
18302
+ if (this.animationpause !== true && this.image.paused) {
18303
+ this._playVideo();
18304
+ }
18305
+ };
18306
+ for (const type of VIDEO_GESTURES) {
18307
+ globalThis.document.addEventListener(
18308
+ type,
18309
+ this._videoGestureListener,
18310
+ true
18311
+ );
18312
+ }
18313
+ }
18314
+ /**
18315
+ * Drop a pending `play()` request and stop waiting for a gesture.
18316
+ * @ignore
18317
+ * @internal
18318
+ */
18319
+ _cancelVideoPlay() {
18320
+ this._videoPlayRequest++;
18321
+ this._videoPlayPending = false;
18322
+ this._removeVideoGestureListener();
18323
+ }
18324
+ /**
18325
+ * @ignore
18326
+ * @internal
18327
+ */
18328
+ _removeVideoGestureListener() {
18329
+ if (this._videoGestureListener === void 0) {
18330
+ return;
18331
+ }
18332
+ for (const type of VIDEO_GESTURES) {
18333
+ globalThis.document.removeEventListener(
18334
+ type,
18335
+ this._videoGestureListener,
18336
+ true
18337
+ );
18338
+ }
18339
+ this._videoGestureListener = void 0;
18340
+ }
18244
18341
  /**
18245
18342
  * Destroy function<br>
18246
18343
  * @ignore
@@ -18252,6 +18349,7 @@ var Sprite = class extends Renderable {
18252
18349
  this.offset = void 0;
18253
18350
  if (this.isVideo) {
18254
18351
  this.removeStatePauseListener();
18352
+ this._cancelVideoPlay();
18255
18353
  if (typeof this._videoFrameHandle !== "undefined") {
18256
18354
  this.image.cancelVideoFrameCallback(this._videoFrameHandle);
18257
18355
  delete this.image.version;
@@ -21693,6 +21791,85 @@ var Path2D = class {
21693
21791
  };
21694
21792
  var path2d_default = Path2D;
21695
21793
 
21794
+ // src/video/blendmodes.js
21795
+ var ADVANCED_BLEND_MODES = [
21796
+ "difference",
21797
+ "overlay",
21798
+ "hard-light",
21799
+ "color-dodge",
21800
+ "color-burn",
21801
+ "soft-light",
21802
+ // `darken` and `lighten` joined the shader path in 20.2: fixed-function
21803
+ // MIN/MAX compute `min(src, dst)` and nothing else, so there is nowhere to
21804
+ // put the `(1 - srcAlpha) * dst` term source-over contributes after the
21805
+ // blend. A translucent source lost the backdrop's share entirely — a white
21806
+ // `lighten` glow over a light backdrop rendered invisible. APPENDED, never
21807
+ // inserted: the index is the shader's branch value.
21808
+ "darken",
21809
+ "lighten"
21810
+ ];
21811
+ function isAdvancedBlendMode(mode) {
21812
+ return ADVANCED_BLEND_MODES.indexOf(mode) !== -1;
21813
+ }
21814
+ function advancedBlendModeIndex(mode) {
21815
+ return ADVANCED_BLEND_MODES.indexOf(mode);
21816
+ }
21817
+ function normalizeBlendMode(mode) {
21818
+ switch (mode) {
21819
+ case "none":
21820
+ case "multiply":
21821
+ case "screen":
21822
+ case "exclusion":
21823
+ case "normal":
21824
+ return mode;
21825
+ case "additive":
21826
+ case "add":
21827
+ case "lighter":
21828
+ return "additive";
21829
+ default:
21830
+ return "normal";
21831
+ }
21832
+ }
21833
+ function isSupportedBlendMode(mode) {
21834
+ return isAdvancedBlendMode(mode) || mode === "normal" || normalizeBlendMode(mode) !== "normal";
21835
+ }
21836
+ function reportedBlendMode(mode) {
21837
+ return isSupportedBlendMode(mode) ? mode : "normal";
21838
+ }
21839
+ function blendStateFor(mode, premultipliedAlpha) {
21840
+ const src = premultipliedAlpha ? "one" : "src-alpha";
21841
+ switch (mode) {
21842
+ case "none":
21843
+ return void 0;
21844
+ case "additive":
21845
+ return { operation: "add", srcFactor: src, dstFactor: "one" };
21846
+ case "multiply":
21847
+ return {
21848
+ operation: "add",
21849
+ srcFactor: "dst",
21850
+ dstFactor: "one-minus-src-alpha"
21851
+ };
21852
+ case "exclusion":
21853
+ return {
21854
+ operation: "add",
21855
+ srcFactor: "one-minus-dst",
21856
+ dstFactor: "one-minus-src"
21857
+ };
21858
+ case "screen":
21859
+ return {
21860
+ operation: "add",
21861
+ srcFactor: "one",
21862
+ dstFactor: "one-minus-src"
21863
+ };
21864
+ default:
21865
+ return {
21866
+ operation: "add",
21867
+ srcFactor: src,
21868
+ dstFactor: "one-minus-src-alpha"
21869
+ };
21870
+ }
21871
+ }
21872
+
21696
21873
  // src/video/effects/tonemap.js
21697
21874
  var CURVES = {
21698
21875
  /**
@@ -22171,12 +22348,25 @@ var Renderer = class _Renderer {
22171
22348
  *
22172
22349
  * Two consequences worth knowing:
22173
22350
  *
22174
- * - **A game that never calls `loader.preload()` is never warmed.** There
22175
- * is no other hook. Call this yourself if you load assets some other way
22176
- * and still want the warm-up.
22351
+ * - **A game that never calls `loader.preload()` never gets this part.**
22352
+ * Call it yourself if you load assets some other way and still want it.
22177
22353
  * - It is deliberately NOT called from `Application.init()`. That runs
22178
22354
  * before anything is on screen, so warming there would only lengthen a
22179
22355
  * blank page instead of hiding behind a progress bar.
22356
+ *
22357
+ * This is the half that needs no renderable. The other half cannot run
22358
+ * here, because what it compiles depends on the renderable drawing: under
22359
+ * WebGPU a pipeline is keyed by blend mode, culling, transparency and the
22360
+ * format of the target as well as by shader. So, behind the same
22361
+ * `prewarm` setting, the renderer also warms a renderable when it gains
22362
+ * a post effect ({@link Renderable#addPostEffect}) and when it is added
22363
+ * to the world ({@link Container#addChild}): its effect pipelines, its
22364
+ * own draw, the render targets its effect chain composites through, and
22365
+ * the advanced blend machinery if its blend mode needs it. That work is
22366
+ * started after the caller's code has run and finishes off the frame, so
22367
+ * it pays off when a renderable is set up at least a frame before it is
22368
+ * first drawn: build pools and attach their effects when a stage starts,
22369
+ * not when the object is first shown.
22180
22370
  * @returns {Promise<void>} settles when the built-in programs are ready
22181
22371
  * @example
22182
22372
  * // the usual case: nothing to write. `preload()` warms on the way through
@@ -23096,6 +23286,152 @@ var Renderer = class _Renderer {
23096
23286
  }
23097
23287
  return effects;
23098
23288
  }
23289
+ /**
23290
+ * How many effects a renderable's chain has to have render targets for:
23291
+ * every one attached, not just the ones enabled right now.
23292
+ *
23293
+ * Sizing the pool by the enabled count meant the ping-pong target was
23294
+ * only created on the frame a second effect was switched on, which is a
23295
+ * frame in the middle of play by construction (a hit flash, a power-up),
23296
+ * and creating a target there is a stall in that frame. A disabled effect
23297
+ * is one the game intends to turn on, so its target is made up front.
23298
+ * @param {Renderable} renderable - the renderable whose chain this is
23299
+ * @param {ShaderEffect[]} effects - its enabled chain, from `_effectChainFor`
23300
+ * @returns {number} the number of chain slots to provision
23301
+ * @ignore
23302
+ * @internal
23303
+ */
23304
+ _postEffectCapacity(renderable, effects) {
23305
+ return Math.max(effects.length, renderable.postEffects.length);
23306
+ }
23307
+ /**
23308
+ * Warm, ahead of its first draw, what a renderable will need: the
23309
+ * pipelines, programs and render targets that would otherwise be built
23310
+ * on the frame it first shows up.
23311
+ *
23312
+ * Called when a renderable gains a post effect
23313
+ * ({@link Renderable#addPostEffect}) and when it enters the world
23314
+ * ({@link Container#addChild}), the two moments its draw becomes
23315
+ * knowable. Deferred to a microtask so it runs once the caller's own code
23316
+ * has finished, which is when the rest of the picture exists: a
23317
+ * renderable usually gets its effect before it is added to the world,
23318
+ * sized, or given its blend mode, and those all decide which pipeline its
23319
+ * draw will ask for. Several requests in one task collapse into one
23320
+ * warm-up per renderable.
23321
+ *
23322
+ * Honours the `prewarm` setting, the same switch as the loading-screen
23323
+ * warm-up.
23324
+ * @param {Renderable} renderable - the renderable to warm for
23325
+ * @ignore
23326
+ * @internal
23327
+ */
23328
+ _queueWarm(renderable) {
23329
+ if (this.settings?.prewarm !== true || this.shaderLanguage == null) {
23330
+ return;
23331
+ }
23332
+ if (this._warmHosts === void 0) {
23333
+ this._warmHosts = /* @__PURE__ */ new Set();
23334
+ }
23335
+ if (this._warmHosts.size === 0) {
23336
+ queueMicrotask(() => {
23337
+ this._flushWarm();
23338
+ });
23339
+ }
23340
+ this._warmHosts.add(renderable);
23341
+ }
23342
+ /**
23343
+ * Run the warm-ups queued by {@link Renderer#_queueWarm}.
23344
+ * @ignore
23345
+ * @internal
23346
+ */
23347
+ _flushWarm() {
23348
+ const hosts = [...this._warmHosts];
23349
+ this._warmHosts.clear();
23350
+ for (const host of hosts) {
23351
+ try {
23352
+ this._warmRenderable(host);
23353
+ } catch {
23354
+ }
23355
+ }
23356
+ }
23357
+ /**
23358
+ * Warm one renderable, and everything under it when it is a container:
23359
+ * adding a container to the world brings its whole subtree in with it,
23360
+ * without an `addChild` per descendant.
23361
+ * @param {Renderable} renderable - the renderable to warm for
23362
+ * @ignore
23363
+ * @internal
23364
+ */
23365
+ _warmRenderable(renderable) {
23366
+ if (renderable == null) {
23367
+ return;
23368
+ }
23369
+ if (renderable.postEffects?.length > 0) {
23370
+ this._warmPostEffects(renderable);
23371
+ }
23372
+ this._warmDraw(renderable);
23373
+ if (isAdvancedBlendMode(renderable.blendMode)) {
23374
+ this._warmAdvancedBlend();
23375
+ }
23376
+ const children = renderable.children;
23377
+ if (Array.isArray(children)) {
23378
+ for (const child of children) {
23379
+ this._warmRenderable(child);
23380
+ }
23381
+ }
23382
+ }
23383
+ /**
23384
+ * Warm everything one renderable's post effects will need. Each backend
23385
+ * knows what that is; this base has no shaders, so nothing.
23386
+ * @param {Renderable} renderable - the renderable to warm for
23387
+ * @ignore
23388
+ * @internal
23389
+ */
23390
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
23391
+ _warmPostEffects(renderable) {
23392
+ }
23393
+ /**
23394
+ * Warm the renderable's own draw, the one it makes without any effect.
23395
+ * Only meaningful where that is a pipeline keyed by draw state; nothing
23396
+ * to do on this base.
23397
+ * @param {Renderable} renderable - the renderable to warm for
23398
+ * @ignore
23399
+ * @internal
23400
+ */
23401
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
23402
+ _warmDraw(renderable) {
23403
+ }
23404
+ /**
23405
+ * Warm the advanced blend modes' machinery: the blend shader, built on
23406
+ * the first draw that uses one of those modes, and its offscreen target.
23407
+ * Nothing to do on this base.
23408
+ * @ignore
23409
+ * @internal
23410
+ */
23411
+ _warmAdvancedBlend() {
23412
+ }
23413
+ /**
23414
+ * Whether a renderable draws through `drawMesh`, where a lone effect is
23415
+ * spliced into the mesh shader instead of being drawn as a quad.
23416
+ * Duck-typed on fields only a `Mesh` carries, so the renderer does not
23417
+ * have to import the renderable classes. An instanced mesh never hosts
23418
+ * an effect: its shading is a separate family that knows nothing of one.
23419
+ * @param {Renderable} renderable - the renderable to test
23420
+ * @returns {boolean} true for a non-instanced mesh
23421
+ * @ignore
23422
+ * @internal
23423
+ */
23424
+ _hostsMeshEffect(renderable) {
23425
+ return typeof renderable.cullBackFaces === "boolean" && renderable.indices !== void 0 && renderable.instanceLayout === void 0;
23426
+ }
23427
+ /**
23428
+ * Warm the tone curve's pipeline once it exists. Nothing to do on a
23429
+ * backend that compiles it in its constructor.
23430
+ * @ignore
23431
+ * @internal
23432
+ */
23433
+ _warmToneMap() {
23434
+ }
23099
23435
  /**
23100
23436
  * Set the tone curve applied at the end of the camera's chain.
23101
23437
  *
@@ -23172,6 +23508,7 @@ var Renderer = class _Renderer {
23172
23508
  this._toneMapEffect?.destroy?.();
23173
23509
  this._toneMapEffect = new ToneMappingEffect(this, { mode });
23174
23510
  this._toneMapEffect.shared = true;
23511
+ this._warmToneMap();
23175
23512
  }
23176
23513
  this._toneMapEffect.setExposure(this.settings.toneMappingExposure ?? 1);
23177
23514
  this._toneMapEffect.setWhite(this.settings.toneMappingWhite ?? 0);
@@ -28585,6 +28922,7 @@ var Container = class _Container extends Renderable {
28585
28922
  registerChildBody(cchild, worldContainer);
28586
28923
  });
28587
28924
  }
28925
+ worldContainer?.app?.renderer?._queueWarm?.(child);
28588
28926
  }
28589
28927
  if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
28590
28928
  child.onActivateEvent();
@@ -28627,6 +28965,7 @@ var Container = class _Container extends Renderable {
28627
28965
  registerChildBody(cchild, worldContainer);
28628
28966
  });
28629
28967
  }
28968
+ worldContainer?.app?.renderer?._queueWarm?.(child);
28630
28969
  }
28631
28970
  if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
28632
28971
  child.onActivateEvent();
@@ -41909,6 +42248,23 @@ var WebGLIndexBuffer = class extends IndexBuffer {
41909
42248
  }
41910
42249
  };
41911
42250
 
42251
+ // src/video/webgl/buffer/deletable.js
42252
+ var lostOnce = /* @__PURE__ */ new WeakSet();
42253
+ function markContextLost(gl) {
42254
+ if (gl) {
42255
+ lostOnce.add(gl);
42256
+ }
42257
+ }
42258
+ function deletable(gl, probe, handle) {
42259
+ if (gl.isContextLost() === true) {
42260
+ return false;
42261
+ }
42262
+ if (lostOnce.has(gl) === false) {
42263
+ return true;
42264
+ }
42265
+ return probe.call(gl, handle) === true;
42266
+ }
42267
+
41912
42268
  // src/video/webgl/buffer/vertexstate.js
41913
42269
  function bufferGroups(descriptor) {
41914
42270
  if (Array.isArray(descriptor.buffers)) {
@@ -42057,12 +42413,15 @@ var WebGLVertexState = class {
42057
42413
  /**
42058
42414
  * Delete the underlying vertex array object, if any. Idempotent, and
42059
42415
  * safe on a lost context: deleting an object belonging to a lost
42060
- * context would queue `INVALID_OPERATION` (deletion counts as *using*
42061
- * it — only the `is*` queries are exempt), so probe first.
42416
+ * context would queue `INVALID_OPERATION`, because deletion counts as
42417
+ * *using* it.
42418
+ *
42419
+ * The probe behind `deletable` is synchronous, so it is skipped unless
42420
+ * this context has actually lost one — see `buffer/deletable.js`.
42062
42421
  */
42063
42422
  release() {
42064
42423
  if (this.handle !== null) {
42065
- if (this.gl.isVertexArray(this.handle)) {
42424
+ if (deletable(this.gl, this.gl.isVertexArray, this.handle)) {
42066
42425
  this.gl.deleteVertexArray(this.handle);
42067
42426
  }
42068
42427
  this.handle = null;
@@ -43054,6 +43413,14 @@ function transformQuadCorners(m, x, y, w, h, z) {
43054
43413
  }
43055
43414
  return corners;
43056
43415
  }
43416
+ var scaled = new Float64Array(4);
43417
+ function scaleQuad(x, y, w, h, sx, sy) {
43418
+ scaled[0] = x + w * (1 - sx) / 2;
43419
+ scaled[1] = y + h * (1 - sy) / 2;
43420
+ scaled[2] = w * sx;
43421
+ scaled[3] = h * sy;
43422
+ return scaled;
43423
+ }
43057
43424
 
43058
43425
  // src/video/webgl/shaders/multitexture.js
43059
43426
  function buildMultiTextureFragment(maxTextures) {
@@ -43092,6 +43459,8 @@ var immutableStorage = /* @__PURE__ */ new WeakMap();
43092
43459
  function mipLevels(w, h) {
43093
43460
  return Math.floor(Math.log2(Math.max(w, h))) + 1;
43094
43461
  }
43462
+ var COMPRESSED_RGB_ETC1_WEBGL3 = 36196;
43463
+ var COMPRESSED_RGB8_ETC22 = 37492;
43095
43464
  var MaterialBatcher = class extends WebGLBatcher {
43096
43465
  /**
43097
43466
  * Initialize the textured batcher
@@ -43201,11 +43570,20 @@ var MaterialBatcher = class extends WebGLBatcher {
43201
43570
  this.currentTextureUnit = unit;
43202
43571
  const resourceOwned = pixels !== null && typeof pixels.upload === "function";
43203
43572
  const compressed = pixels !== null && pixels.compressed === true;
43573
+ let compressedFormat = compressed ? pixels.format : void 0;
43574
+ let mutableETC1 = false;
43575
+ if (compressedFormat === COMPRESSED_RGB_ETC1_WEBGL3) {
43576
+ if (this.renderer.getSupportedCompressedTextureFormats().etc2) {
43577
+ compressedFormat = COMPRESSED_RGB8_ETC22;
43578
+ } else {
43579
+ mutableETC1 = true;
43580
+ }
43581
+ }
43204
43582
  if (!resourceOwned) {
43205
43583
  const storageW = Math.max(1, w | 0);
43206
43584
  const storageH = Math.max(1, h | 0);
43207
43585
  const levels2 = compressed ? pixels.mipmaps.length : mipmap === true ? mipLevels(storageW, storageH) : 1;
43208
- const format = compressed ? pixels.format : gl.RGBA8;
43586
+ const format = compressed ? compressedFormat : gl.RGBA8;
43209
43587
  let storage = immutableStorage.get(currentTexture);
43210
43588
  if (storage && (storage.width !== storageW || storage.height !== storageH || storage.levels !== levels2 || storage.format !== format)) {
43211
43589
  gl.deleteTexture(currentTexture);
@@ -43214,7 +43592,7 @@ var MaterialBatcher = class extends WebGLBatcher {
43214
43592
  this.boundTextures[unit] = currentTexture;
43215
43593
  storage = void 0;
43216
43594
  }
43217
- if (!storage) {
43595
+ if (!storage && !mutableETC1) {
43218
43596
  gl.texStorage2D(gl.TEXTURE_2D, levels2, format, storageW, storageH);
43219
43597
  immutableStorage.set(currentTexture, {
43220
43598
  width: storageW,
@@ -43233,18 +43611,37 @@ var MaterialBatcher = class extends WebGLBatcher {
43233
43611
  pixels.upload(gl, gl.TEXTURE_2D);
43234
43612
  } else if (compressed) {
43235
43613
  const mipmaps = pixels.mipmaps;
43236
- for (let i = 0; i < mipmaps.length; i++) {
43237
- gl.compressedTexSubImage2D(
43614
+ if (mutableETC1) {
43615
+ gl.texParameteri(
43238
43616
  gl.TEXTURE_2D,
43239
- i,
43240
- 0,
43241
- 0,
43242
- mipmaps[i].width,
43243
- mipmaps[i].height,
43244
- pixels.format,
43245
- mipmaps[i].data
43617
+ gl.TEXTURE_MAX_LEVEL,
43618
+ mipmaps.length - 1
43246
43619
  );
43247
43620
  }
43621
+ for (let i = 0; i < mipmaps.length; i++) {
43622
+ if (mutableETC1) {
43623
+ gl.compressedTexImage2D(
43624
+ gl.TEXTURE_2D,
43625
+ i,
43626
+ compressedFormat,
43627
+ mipmaps[i].width,
43628
+ mipmaps[i].height,
43629
+ 0,
43630
+ mipmaps[i].data
43631
+ );
43632
+ } else {
43633
+ gl.compressedTexSubImage2D(
43634
+ gl.TEXTURE_2D,
43635
+ i,
43636
+ 0,
43637
+ 0,
43638
+ mipmaps[i].width,
43639
+ mipmaps[i].height,
43640
+ compressedFormat,
43641
+ mipmaps[i].data
43642
+ );
43643
+ }
43644
+ }
43248
43645
  } else if (pixels === null) {
43249
43646
  } else if (typeof pixels.byteLength !== "undefined") {
43250
43647
  gl.texSubImage2D(
@@ -43653,6 +44050,14 @@ var QuadBatcher = class extends MaterialBatcher {
43653
44050
  );
43654
44051
  }
43655
44052
  this.currentShader._setUVYDir?.(1);
44053
+ const scale2 = this.currentShader._vertexScale;
44054
+ if (scale2 !== void 0 && (scale2.x !== 1 || scale2.y !== 1)) {
44055
+ const q = scaleQuad(x, y, w, h, scale2.x, scale2.y);
44056
+ x = q[0];
44057
+ y = q[1];
44058
+ w = q[2];
44059
+ h = q[3];
44060
+ }
43656
44061
  }
43657
44062
  const [vec0, vec1, vec2, vec3] = transformQuadCorners(
43658
44063
  this.viewMatrix,
@@ -43670,85 +44075,6 @@ var QuadBatcher = class extends MaterialBatcher {
43670
44075
  }
43671
44076
  };
43672
44077
 
43673
- // src/video/blendmodes.js
43674
- var ADVANCED_BLEND_MODES = [
43675
- "difference",
43676
- "overlay",
43677
- "hard-light",
43678
- "color-dodge",
43679
- "color-burn",
43680
- "soft-light",
43681
- // `darken` and `lighten` joined the shader path in 20.2: fixed-function
43682
- // MIN/MAX compute `min(src, dst)` and nothing else, so there is nowhere to
43683
- // put the `(1 - srcAlpha) * dst` term source-over contributes after the
43684
- // blend. A translucent source lost the backdrop's share entirely — a white
43685
- // `lighten` glow over a light backdrop rendered invisible. APPENDED, never
43686
- // inserted: the index is the shader's branch value.
43687
- "darken",
43688
- "lighten"
43689
- ];
43690
- function isAdvancedBlendMode(mode) {
43691
- return ADVANCED_BLEND_MODES.indexOf(mode) !== -1;
43692
- }
43693
- function advancedBlendModeIndex(mode) {
43694
- return ADVANCED_BLEND_MODES.indexOf(mode);
43695
- }
43696
- function normalizeBlendMode(mode) {
43697
- switch (mode) {
43698
- case "none":
43699
- case "multiply":
43700
- case "screen":
43701
- case "exclusion":
43702
- case "normal":
43703
- return mode;
43704
- case "additive":
43705
- case "add":
43706
- case "lighter":
43707
- return "additive";
43708
- default:
43709
- return "normal";
43710
- }
43711
- }
43712
- function isSupportedBlendMode(mode) {
43713
- return isAdvancedBlendMode(mode) || mode === "normal" || normalizeBlendMode(mode) !== "normal";
43714
- }
43715
- function reportedBlendMode(mode) {
43716
- return isSupportedBlendMode(mode) ? mode : "normal";
43717
- }
43718
- function blendStateFor(mode, premultipliedAlpha) {
43719
- const src = premultipliedAlpha ? "one" : "src-alpha";
43720
- switch (mode) {
43721
- case "none":
43722
- return void 0;
43723
- case "additive":
43724
- return { operation: "add", srcFactor: src, dstFactor: "one" };
43725
- case "multiply":
43726
- return {
43727
- operation: "add",
43728
- srcFactor: "dst",
43729
- dstFactor: "one-minus-src-alpha"
43730
- };
43731
- case "exclusion":
43732
- return {
43733
- operation: "add",
43734
- srcFactor: "one-minus-dst",
43735
- dstFactor: "one-minus-src"
43736
- };
43737
- case "screen":
43738
- return {
43739
- operation: "add",
43740
- srcFactor: "one",
43741
- dstFactor: "one-minus-src"
43742
- };
43743
- default:
43744
- return {
43745
- operation: "add",
43746
- srcFactor: src,
43747
- dstFactor: "one-minus-src-alpha"
43748
- };
43749
- }
43750
- }
43751
-
43752
44078
  // src/video/effects/blendEffect.js
43753
44079
  var wgslFragment2 = `
43754
44080
  struct BlendUniforms {
@@ -44032,6 +44358,26 @@ var RenderTargetPool = class {
44032
44358
  }
44033
44359
  return rt;
44034
44360
  }
44361
+ /**
44362
+ * Create, ahead of the pass that will use them, the targets a top-level
44363
+ * chain needs: the camera pair, or the first sprite pair. Targets that
44364
+ * already exist are left as they are (the pass resizes them when it
44365
+ * begins), and the active-pass stack is not touched, so this is safe
44366
+ * outside any pass.
44367
+ * @param {boolean} isCamera - true for the camera pair (0/1), false for the first sprite pair (2/3)
44368
+ * @param {number} effectCount - the number of effects in the chain
44369
+ * @param {number} width - target width in pixels
44370
+ * @param {number} height - target height in pixels
44371
+ */
44372
+ reserve(isCamera, effectCount, width, height) {
44373
+ const base = isCamera ? 0 : 2;
44374
+ if (!this._pool[base]) {
44375
+ this.get(base, width, height);
44376
+ }
44377
+ if (effectCount > 1 && !this._pool[base + 1]) {
44378
+ this.get(base + 1, width, height);
44379
+ }
44380
+ }
44035
44381
  /**
44036
44382
  * Get the capture render target for the current active pass.
44037
44383
  * @returns {RenderTarget|undefined} the capture target, or undefined if no active pass
@@ -44751,7 +45097,7 @@ var UniformBlock = class {
44751
45097
  destroy() {
44752
45098
  const gl = this.gl;
44753
45099
  if (this.buffer !== null) {
44754
- if (gl.isBuffer(this.buffer)) {
45100
+ if (deletable(gl, gl.isBuffer, this.buffer)) {
44755
45101
  gl.deleteBuffer(this.buffer);
44756
45102
  }
44757
45103
  this.buffer = null;
@@ -45004,14 +45350,13 @@ var WebGLInstanceBuffer = class {
45004
45350
  );
45005
45351
  }
45006
45352
  /**
45007
- * Release the GL buffer. Safe to call twice, and on a lost context — the
45008
- * `isBuffer` probe is itself error-free, whereas deleting a handle whose
45009
- * context died raises INVALID_OPERATION.
45353
+ * Release the GL buffer. Safe to call twice, and on a lost context:
45354
+ * deleting a handle whose context died raises INVALID_OPERATION.
45010
45355
  */
45011
45356
  destroy() {
45012
45357
  const gl = this.gl;
45013
45358
  if (this.buffer !== null) {
45014
- if (gl.isBuffer(this.buffer)) {
45359
+ if (deletable(gl, gl.isBuffer, this.buffer)) {
45015
45360
  gl.deleteBuffer(this.buffer);
45016
45361
  }
45017
45362
  this.buffer = null;
@@ -45097,13 +45442,13 @@ var RetainedGeometry = class {
45097
45442
  this.vertexState = null;
45098
45443
  }
45099
45444
  if (this.vertexBuffer !== null) {
45100
- if (gl.isBuffer(this.vertexBuffer)) {
45445
+ if (deletable(gl, gl.isBuffer, this.vertexBuffer)) {
45101
45446
  gl.deleteBuffer(this.vertexBuffer);
45102
45447
  }
45103
45448
  this.vertexBuffer = null;
45104
45449
  }
45105
45450
  if (this.glIndexBuffer !== null) {
45106
- if (gl.isBuffer(this.glIndexBuffer)) {
45451
+ if (deletable(gl, gl.isBuffer, this.glIndexBuffer)) {
45107
45452
  gl.deleteBuffer(this.glIndexBuffer);
45108
45453
  }
45109
45454
  this.glIndexBuffer = null;
@@ -45142,6 +45487,14 @@ var _EYE_POSITION = new Float32Array(3);
45142
45487
  var _FOG_COLOR = new Float32Array(3);
45143
45488
  var _FOG_PARAMS = new Float32Array(4);
45144
45489
  var _FOG_HEIGHT = new Float32Array(4);
45490
+ function prewarmYield() {
45491
+ if (globalThis.document?.hidden === true) {
45492
+ return Promise.resolve();
45493
+ }
45494
+ return new Promise((resolve) => {
45495
+ setTimeout(resolve, 0);
45496
+ });
45497
+ }
45145
45498
  var MeshBatcher = class extends MaterialBatcher {
45146
45499
  /**
45147
45500
  * Initialize the mesh batcher
@@ -45725,7 +46078,21 @@ var MeshBatcher = class extends MaterialBatcher {
45725
46078
  * @internal
45726
46079
  */
45727
46080
  effectShaderFor(effect) {
45728
- const fog = this._fogDefine();
46081
+ return this._effectShader(effect, this._fogDefine());
46082
+ }
46083
+ /**
46084
+ * The spliced program for one effect under one fog state.
46085
+ *
46086
+ * Split out of {@link MeshBatcher#effectShaderFor} so `prewarm` can build
46087
+ * BOTH fog variants at load, rather than only the one the current scene
46088
+ * happens to be using.
46089
+ * @param {object} effect - the hosted ShaderEffect
46090
+ * @param {string} fog - the fog define, or ""
46091
+ * @returns {GLShader|null} the spliced program, or null if it will not build
46092
+ * @ignore
46093
+ * @internal
46094
+ */
46095
+ _effectShader(effect, fog) {
45729
46096
  const sources = this._shaderSources();
45730
46097
  const fragment2 = spliceEffect(sources.fragment, effect._meshBody);
45731
46098
  if (fragment2 === null) {
@@ -45858,17 +46225,14 @@ var MeshBatcher = class extends MaterialBatcher {
45858
46225
  * @ignore
45859
46226
  * @internal
45860
46227
  */
45861
- async prewarm() {
45862
- const yieldToFrame = () => {
45863
- if (globalThis.document?.hidden === true) {
45864
- return Promise.resolve();
45865
- }
45866
- return new Promise((resolve) => {
45867
- setTimeout(resolve, 0);
45868
- });
45869
- };
46228
+ /**
46229
+ * @param {object[]} [effects] - hosted ShaderEffects to warm the spliced
46230
+ * mesh variants for, normally every preloaded `{type: "shader"}` asset
46231
+ */
46232
+ async prewarm(effects = []) {
46233
+ const yieldToFrame = prewarmYield;
45870
46234
  const alive = () => {
45871
- return this.gl !== null && this.gl?.isContextLost?.() !== true;
46235
+ return this._prewarmAlive();
45872
46236
  };
45873
46237
  const sources = this._shaderSources();
45874
46238
  const instanced = this._instancedShaderSources();
@@ -45906,6 +46270,54 @@ var MeshBatcher = class extends MaterialBatcher {
45906
46270
  return;
45907
46271
  }
45908
46272
  }
46273
+ await this.prewarmEffects(effects);
46274
+ }
46275
+ /**
46276
+ * Build the spliced mesh variant of each effect, in both fog states.
46277
+ *
46278
+ * A ShaderEffect hosted on a mesh does NOT draw with the program its
46279
+ * asset compiled to: the body is spliced into the mesh shader, which is a
46280
+ * different program again, and it was built on the frame the effect first
46281
+ * drew. Preloading the shader looked like it covered this and did not, so
46282
+ * a game that declared every effect in its manifest still stuttered the
46283
+ * first time each one appeared. Warming them here is what makes the
46284
+ * preload mean what it looks like it means.
46285
+ *
46286
+ * Separate from {@link MeshBatcher#prewarm} because the two halves have
46287
+ * different lifetimes: the batcher's own variants are built once, while
46288
+ * the effects grow with every preload.
46289
+ * @param {object[]} [effects] - hosted ShaderEffects, normally the
46290
+ * preloaded `{type: "shader"}` assets
46291
+ * @returns {Promise<void>} settles once every variant is linked
46292
+ * @ignore
46293
+ * @internal
46294
+ */
46295
+ async prewarmEffects(effects = []) {
46296
+ for (const fogDefine of ["", "#define FOG\n"]) {
46297
+ for (const effect of effects) {
46298
+ if (effect?._meshBody === void 0) {
46299
+ continue;
46300
+ }
46301
+ this._effectShader(effect, fogDefine);
46302
+ await prewarmYield();
46303
+ if (!this._prewarmAlive()) {
46304
+ return;
46305
+ }
46306
+ }
46307
+ }
46308
+ }
46309
+ /**
46310
+ * Whether a warm-up may keep building into this batcher. A yield is a
46311
+ * window in which the application can be destroyed or the context lost,
46312
+ * and building into a batcher that has been torn down leaks a `GLShader`
46313
+ * per remaining variant, each subscribed to the global context-loss
46314
+ * events with nothing left to destroy it.
46315
+ * @returns {boolean} true while the context is live
46316
+ * @ignore
46317
+ * @internal
46318
+ */
46319
+ _prewarmAlive() {
46320
+ return this.gl !== null && this.gl?.isContextLost?.() !== true;
45909
46321
  }
45910
46322
  /**
45911
46323
  * Whether the bound program is one this batcher would pick for a plain
@@ -47418,6 +47830,14 @@ var LitQuadBatcher = class extends QuadBatcher {
47418
47830
  );
47419
47831
  }
47420
47832
  this.currentShader._setUVYDir?.(1);
47833
+ const scale2 = this.currentShader._vertexScale;
47834
+ if (scale2 !== void 0 && (scale2.x !== 1 || scale2.y !== 1)) {
47835
+ const q = scaleQuad(x, y, w, h, scale2.x, scale2.y);
47836
+ x = q[0];
47837
+ y = q[1];
47838
+ w = q[2];
47839
+ h = q[3];
47840
+ }
47421
47841
  }
47422
47842
  let normalTextureId = -1;
47423
47843
  let shininess = 0;
@@ -48123,6 +48543,7 @@ function initBlendEnums(gl) {
48123
48543
  "one-minus-dst-alpha": gl.ONE_MINUS_DST_ALPHA
48124
48544
  };
48125
48545
  }
48546
+ var MESH_EFFECT_LINK_BUDGET = 4;
48126
48547
  var WebGLRenderer = class _WebGLRenderer extends Renderer {
48127
48548
  /**
48128
48549
  * @param {ApplicationSettings} [options] - optional parameters for the renderer
@@ -48214,6 +48635,7 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48214
48635
  (e) => {
48215
48636
  e.preventDefault();
48216
48637
  this.isContextValid = false;
48638
+ markContextLost(this.gl);
48217
48639
  this.vertexBuffer = null;
48218
48640
  emit(ONCONTEXT_LOST, this);
48219
48641
  },
@@ -48252,6 +48674,11 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48252
48674
  if (renderer === this) {
48253
48675
  this.currentProgram = void 0;
48254
48676
  this._prewarmed = void 0;
48677
+ this._prewarmedEffects = /* @__PURE__ */ new WeakSet();
48678
+ if (this.settings.prewarm === true) {
48679
+ this.prewarm().catch(() => {
48680
+ });
48681
+ }
48255
48682
  }
48256
48683
  };
48257
48684
  this.onCanvasResize = (width, height) => {
@@ -48407,18 +48834,181 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48407
48834
  * @see {@link Renderer#prewarm}
48408
48835
  */
48409
48836
  prewarm() {
48837
+ this._prewarmedEffects ??= /* @__PURE__ */ new WeakSet();
48838
+ const effects = Object.values(shaderList).filter((effect) => {
48839
+ return effect != null && !this._prewarmedEffects.has(effect);
48840
+ });
48841
+ for (const effect of effects) {
48842
+ this._prewarmedEffects.add(effect);
48843
+ }
48844
+ const batchers = [...this.batchers.values()];
48410
48845
  if (this._prewarmed === void 0) {
48411
48846
  this._prewarmed = Promise.all(
48412
- [...this.batchers.values()].map((batcher) => {
48413
- return batcher.prewarm();
48847
+ batchers.map((batcher) => {
48848
+ return batcher.prewarm(effects);
48414
48849
  })
48415
48850
  ).then(() => {
48416
48851
  this.setBatcher("quad");
48417
48852
  });
48853
+ } else if (effects.length > 0) {
48854
+ this._prewarmed = this._prewarmed.then(() => {
48855
+ return Promise.all(
48856
+ batchers.map((batcher) => {
48857
+ return batcher.prewarmEffects?.(effects);
48858
+ })
48859
+ ).then(() => {
48860
+ this.setBatcher("quad");
48861
+ });
48862
+ });
48418
48863
  }
48419
48864
  return this._prewarmed;
48420
48865
  }
48866
+ /**
48867
+ * Warm what one renderable's post effects will need on this backend.
48868
+ *
48869
+ * An effect's own program is linked when the effect is constructed, so
48870
+ * what is left is what depends on where it is drawn: a mesh does not
48871
+ * draw an effect with that program but with the effect spliced into the
48872
+ * mesh shader, built on first draw; and a chain composites through
48873
+ * render targets, built on the frame the chain first runs or grows a
48874
+ * second effect.
48875
+ * @param {Renderable} renderable - the renderable to warm for
48876
+ * @override
48877
+ * @ignore
48878
+ * @internal
48879
+ */
48880
+ _warmPostEffects(renderable) {
48881
+ const gl = this.gl;
48882
+ if (gl === null || gl.isContextLost?.() === true) {
48883
+ return;
48884
+ }
48885
+ const effects = renderable.postEffects.filter((fx) => {
48886
+ return fx != null;
48887
+ });
48888
+ if (effects.length === 0) {
48889
+ return;
48890
+ }
48891
+ const isCamera = renderable._postEffectManaged === true;
48892
+ if (this._effectPassDepth === 0 && (isCamera || effects.length > 1 || renderable.postEffectNeedsCapture === true)) {
48893
+ const { width, height } = this.getCanvas();
48894
+ this.flush();
48895
+ this._renderTargetPool.reserve(
48896
+ isCamera,
48897
+ this._postEffectCapacity(renderable, effects),
48898
+ width,
48899
+ height
48900
+ );
48901
+ this.invalidateTextureUnit(0);
48902
+ }
48903
+ if (!this._hostsMeshEffect(renderable)) {
48904
+ return;
48905
+ }
48906
+ const batcher = this.batchers.get(
48907
+ renderable.lit === true ? "litMesh" : "mesh"
48908
+ );
48909
+ if (batcher === void 0) {
48910
+ return;
48911
+ }
48912
+ this._meshEffectWarmQueue ??= [];
48913
+ for (const fx of effects) {
48914
+ this._meshEffectWarmQueue.push(batcher, fx);
48915
+ }
48916
+ if (this._meshEffectWarmScheduled !== true) {
48917
+ this._meshEffectWarmScheduled = true;
48918
+ queueMicrotask(() => {
48919
+ this._linkQueuedMeshEffects();
48920
+ });
48921
+ }
48922
+ }
48923
+ /**
48924
+ * Link the spliced mesh programs queued by `_warmPostEffects`, a few
48925
+ * milliseconds' worth per task, so a pool's worth spreads over the next
48926
+ * frames instead of landing in one. A draw that comes first simply links
48927
+ * its own program, as it would have without the warm-up.
48928
+ * @ignore
48929
+ * @internal
48930
+ */
48931
+ _linkQueuedMeshEffects() {
48932
+ const queue = this._meshEffectWarmQueue;
48933
+ const gl = this.gl;
48934
+ if (gl === null || gl.isContextLost?.() === true) {
48935
+ queue.length = 0;
48936
+ this._meshEffectWarmScheduled = false;
48937
+ return;
48938
+ }
48939
+ const start = globalThis.performance.now();
48940
+ const previous = this.currentProgram;
48941
+ try {
48942
+ while (queue.length > 0 && globalThis.performance.now() - start < MESH_EFFECT_LINK_BUDGET) {
48943
+ const batcher = queue.shift();
48944
+ const fx = queue.shift();
48945
+ if (fx.destroyed !== true && batcher.canHostEffect?.(fx) === true) {
48946
+ batcher.effectShaderFor(fx);
48947
+ }
48948
+ }
48949
+ } finally {
48950
+ this._restoreProgram(previous);
48951
+ }
48952
+ if (queue.length === 0) {
48953
+ this._meshEffectWarmScheduled = false;
48954
+ } else if (globalThis.document?.hidden === true) {
48955
+ this._linkQueuedMeshEffects();
48956
+ } else {
48957
+ setTimeout(() => {
48958
+ this._linkQueuedMeshEffects();
48959
+ }, 0);
48960
+ }
48961
+ }
48962
+ /**
48963
+ * Build the advanced blend shader and its offscreen target before the
48964
+ * first draw in one of those modes, which otherwise compiles the shader
48965
+ * and creates the target in the middle of that frame.
48966
+ * @override
48967
+ * @ignore
48968
+ * @internal
48969
+ */
48970
+ _warmAdvancedBlend() {
48971
+ const gl = this.gl;
48972
+ if (gl === null || gl.isContextLost?.() === true) {
48973
+ return;
48974
+ }
48975
+ if (typeof this._advancedBlendEffect === "undefined") {
48976
+ const previous = this.currentProgram;
48977
+ try {
48978
+ this._advancedBlendEffect = new BlendEffect(this);
48979
+ } finally {
48980
+ this._restoreProgram(previous);
48981
+ }
48982
+ }
48983
+ if (typeof this._advancedBlendTarget === "undefined" && this._effectPassDepth === 0 && this._advancedBlendOpen !== true) {
48984
+ const { width, height } = this.getCanvas();
48985
+ this.flush();
48986
+ this._advancedBlendTarget = new WebGLRenderTarget(gl, width, height, {
48987
+ samples: 0,
48988
+ format: gl.RGBA8
48989
+ });
48990
+ this.invalidateTextureUnit(0);
48991
+ }
48992
+ }
48993
+ /**
48994
+ * Put back the program a warm-up found bound. Linking leaves the new
48995
+ * program bound, and the batchers trust what they last bound, so a
48996
+ * warm-up between two draws would otherwise hand the next one a
48997
+ * stranger's program.
48998
+ * @param {WebGLProgram|undefined} previous - what was bound before
48999
+ * @ignore
49000
+ * @internal
49001
+ */
49002
+ _restoreProgram(previous) {
49003
+ if (previous != null && this.currentProgram !== previous) {
49004
+ this.gl.useProgram(previous);
49005
+ this.currentProgram = previous;
49006
+ }
49007
+ }
48421
49008
  destroy() {
49009
+ if (this._meshEffectWarmQueue !== void 0) {
49010
+ this._meshEffectWarmQueue.length = 0;
49011
+ }
48422
49012
  this._toneMapEffect?.destroy?.();
48423
49013
  this._toneMapEffect = void 0;
48424
49014
  off(GAME_RESET, this.onGameReset);
@@ -48956,7 +49546,12 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48956
49546
  }
48957
49547
  savedProjection.copy(this.projectionMatrix);
48958
49548
  this._effectPassDepth++;
48959
- const rt = this._renderTargetPool.begin(isCamera, effects.length, w, h);
49549
+ const rt = this._renderTargetPool.begin(
49550
+ isCamera,
49551
+ this._postEffectCapacity(renderable, effects),
49552
+ w,
49553
+ h
49554
+ );
48960
49555
  this.invalidateTextureUnit(0);
48961
49556
  rt.bind();
48962
49557
  this.setViewport(0, 0, w, h);
@@ -56056,7 +56651,7 @@ var Trigger = class extends Renderable {
56056
56651
  };
56057
56652
 
56058
56653
  // src/version.ts
56059
- var version = "20.9.0";
56654
+ var version = "20.10.0";
56060
56655
 
56061
56656
  // src/system/bootstrap.ts
56062
56657
  var initialized = false;
@@ -58499,6 +59094,8 @@ function gpuBlendState(mode, premultipliedAlpha) {
58499
59094
  return typeof component === "undefined" ? void 0 : { color: component, alpha: component };
58500
59095
  }
58501
59096
  var MAX_QUAD_TEXTURES = 8;
59097
+ var MAX_WARM_IN_FLIGHT = 16;
59098
+ var MISS_KEYS_KEPT = 64;
58502
59099
  var DEPTH_STENCIL_FORMAT = "depth24plus-stencil8";
58503
59100
  var STENCIL_STATES = {
58504
59101
  none: {
@@ -58575,6 +59172,15 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58575
59172
  this.sampleCount = 1;
58576
59173
  this.pipelines = /* @__PURE__ */ new Map();
58577
59174
  this.vertexLayouts = /* @__PURE__ */ new Map();
59175
+ this.inFrame = false;
59176
+ this.inFrameMisses = 0;
59177
+ this.inFrameMissKeys = [];
59178
+ this.pending = /* @__PURE__ */ new Map();
59179
+ this.warmQueue = [];
59180
+ this.warmInFlight = 0;
59181
+ this.touchQueue = /* @__PURE__ */ new Map();
59182
+ this.scratchTargets = /* @__PURE__ */ new Map();
59183
+ this.generation = 0;
58578
59184
  this.frameLayout = device.createBindGroupLayout({
58579
59185
  label: "melonJS frame globals layout",
58580
59186
  entries: [
@@ -58800,24 +59406,33 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58800
59406
  );
58801
59407
  }
58802
59408
  /**
58803
- * Fetch (building on first use) the pipeline for the given state tuple.
58804
- * @param {string} shaderKey - "quad" | "primitive" | "clear"
59409
+ * The cache key for a pipeline state tuple.
59410
+ *
59411
+ * The ONE place a key is composed. A draw looks its pipeline up through
59412
+ * {@link WebGPUPipelineCache#get} and a warm-up builds one ahead of time
59413
+ * through {@link WebGPUPipelineCache#warm}, and both come here, so the two
59414
+ * cannot drift into building the same pipeline under two names: a warm-up
59415
+ * whose key differs from the draw's by one character compiles a pipeline
59416
+ * nothing will ever ask for, and the draw still compiles its own.
59417
+ *
59418
+ * The colour format and sample count default to the cache's CURRENT ones,
59419
+ * which the renderer rewrites per render target. A warm-up runs outside
59420
+ * any pass, where those describe whatever was bound last, so it passes
59421
+ * the target it is warming for explicitly.
59422
+ * @param {string} shaderKey - "quad" | "primitive" | "clear" | a registered family
58805
59423
  * @param {string} topology - portable topology name ("triangle-list", …)
58806
59424
  * @param {string} blendMode - blend mode (normalized internally)
58807
59425
  * @param {boolean} premultipliedAlpha - source premultiplication flag
58808
59426
  * @param {string} [stencilMode="none"] - "none" | "write" | "test" | "tag" | "mark"
58809
- * @param {{cullMode: string, frontFace: string, depthWrite?: boolean, depthTest?: boolean, fog?: boolean}} [meshState] - mesh pass
58810
- * state: its presence switches the depth half of the attachment on —
58811
- * depth writes enabled, "less-equal" testing (the GL mesh mode's
58812
- * LEQUAL, keeping coplanar geometry stable) — and sets the per-mesh
58813
- * face-culling axes. Omitted by the whole 2D tier, whose keys and
58814
- * descriptors stay byte-identical to a build without a mesh path.
58815
- * @returns {GPURenderPipeline} the pipeline
59427
+ * @param {object} [meshState] - the mesh pass state, see {@link WebGPUPipelineCache#get}
59428
+ * @param {string} [format] - colour target format, the current one by default
59429
+ * @param {number} [sampleCount] - pass sample count, the current one by default
59430
+ * @returns {string} the key
58816
59431
  */
58817
- get(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode = "none", meshState) {
59432
+ keyFor(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode = "none", meshState, format = this.format, sampleCount = this.sampleCount) {
58818
59433
  const blend = normalizeBlendMode(blendMode);
58819
- const pma = premultipliedAlpha !== false;
58820
- let key = `${shaderKey}|${topology}|${blend}|${pma ? 1 : 0}|${stencilMode}|${this.format}|${this.sampleCount}`;
59434
+ const pma = blend === "none" || premultipliedAlpha !== false;
59435
+ let key = `${shaderKey}|${topology}|${blend}|${pma ? 1 : 0}|${stencilMode}|${format}|${sampleCount}`;
58821
59436
  if (meshState) {
58822
59437
  key += `|mesh:${meshState.cullMode}:${meshState.frontFace}`;
58823
59438
  if (meshState.depthWrite === false) {
@@ -58830,32 +59445,257 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58830
59445
  key += "|fog";
58831
59446
  }
58832
59447
  }
58833
- const fogFlag = meshState?.fog === true ? 1 : 0;
59448
+ return key;
59449
+ }
59450
+ /**
59451
+ * Fetch (building on first use) the pipeline for the given state tuple.
59452
+ * @param {string} shaderKey - "quad" | "primitive" | "clear"
59453
+ * @param {string} topology - portable topology name ("triangle-list", …)
59454
+ * @param {string} blendMode - blend mode (normalized internally)
59455
+ * @param {boolean} premultipliedAlpha - source premultiplication flag
59456
+ * @param {string} [stencilMode="none"] - "none" | "write" | "test" | "tag" | "mark"
59457
+ * @param {{cullMode: string, frontFace: string, depthWrite?: boolean, depthTest?: boolean, fog?: boolean}} [meshState] - mesh pass
59458
+ * state: its presence switches the depth half of the attachment on —
59459
+ * depth writes enabled, "less-equal" testing (the GL mesh mode's
59460
+ * LEQUAL, keeping coplanar geometry stable) — and sets the per-mesh
59461
+ * face-culling axes. Omitted by the whole 2D tier, whose keys and
59462
+ * descriptors stay byte-identical to a build without a mesh path.
59463
+ * @returns {GPURenderPipeline} the pipeline
59464
+ */
59465
+ get(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode = "none", meshState) {
59466
+ const key = this.keyFor(
59467
+ shaderKey,
59468
+ topology,
59469
+ blendMode,
59470
+ premultipliedAlpha,
59471
+ stencilMode,
59472
+ meshState
59473
+ );
58834
59474
  let pipeline = this.pipelines.get(key);
58835
59475
  if (typeof pipeline === "undefined") {
58836
- const stencil = STENCIL_STATES[stencilMode] ?? STENCIL_STATES.none;
58837
- const vertexLayout = this.vertexLayouts.get(
58838
- this.vertexLayoutAliases.get(shaderKey) ?? shaderKey
58839
- );
58840
- const primitive = {
58841
- topology,
58842
- stripIndexFormat: topology.endsWith("-strip") ? "uint32" : void 0
58843
- };
58844
- if (meshState) {
58845
- primitive.cullMode = meshState.cullMode;
58846
- primitive.frontFace = meshState.frontFace;
58847
- }
58848
59476
  pipeline = this.device.createRenderPipeline(
58849
- this._descriptor(key, shaderKey, blend, pma, stencil, primitive, {
58850
- vertexLayout,
59477
+ this._descriptor(
59478
+ key,
59479
+ shaderKey,
59480
+ topology,
59481
+ blendMode,
59482
+ premultipliedAlpha,
59483
+ stencilMode,
58851
59484
  meshState,
58852
- fogFlag
58853
- })
59485
+ this.format,
59486
+ this.sampleCount
59487
+ )
58854
59488
  );
58855
59489
  this.pipelines.set(key, pipeline);
59490
+ if (this.inFrame === true) {
59491
+ this.inFrameMisses++;
59492
+ this.inFrameMissKeys.push(key);
59493
+ if (this.inFrameMissKeys.length > MISS_KEYS_KEPT) {
59494
+ this.inFrameMissKeys.shift();
59495
+ }
59496
+ }
58856
59497
  }
58857
59498
  return pipeline;
58858
59499
  }
59500
+ /**
59501
+ * Build a pipeline ahead of the draw that will need it, without blocking.
59502
+ *
59503
+ * Takes the same tuple as {@link WebGPUPipelineCache#get}, plus the colour
59504
+ * format and sample count of the target the draw will land on, since a
59505
+ * warm-up runs outside any pass. Compiles through
59506
+ * `createRenderPipelineAsync` where the device has it, so the work
59507
+ * happens off the frame, and files the result under the same key `get`
59508
+ * will look up. Asking twice for one pipeline compiles it once.
59509
+ *
59510
+ * Never rejects. A pipeline that fails to build is left for the draw to
59511
+ * build and report, exactly as it would have without the warm-up.
59512
+ * @param {string} shaderKey - the family, already registered
59513
+ * @param {string} topology - portable topology name
59514
+ * @param {string} blendMode - blend mode
59515
+ * @param {boolean} premultipliedAlpha - source premultiplication flag
59516
+ * @param {string} stencilMode - stencil mode
59517
+ * @param {object|undefined} meshState - mesh pass state, read NOW: a later
59518
+ * change to the object does not reach the pipeline being built
59519
+ * @param {string} format - colour target format the draw will use
59520
+ * @param {number} sampleCount - sample count the draw will use
59521
+ * @returns {Promise<void>} settles once the pipeline is in the cache, or
59522
+ * has failed to get there
59523
+ */
59524
+ warm(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode, meshState, format, sampleCount) {
59525
+ const key = this.keyFor(
59526
+ shaderKey,
59527
+ topology,
59528
+ blendMode,
59529
+ premultipliedAlpha,
59530
+ stencilMode,
59531
+ meshState,
59532
+ format,
59533
+ sampleCount
59534
+ );
59535
+ if (this.pipelines.has(key)) {
59536
+ return Promise.resolve();
59537
+ }
59538
+ const inFlight = this.pending.get(key);
59539
+ if (inFlight !== void 0) {
59540
+ return inFlight;
59541
+ }
59542
+ const descriptor = this._descriptor(
59543
+ key,
59544
+ shaderKey,
59545
+ topology,
59546
+ blendMode,
59547
+ premultipliedAlpha,
59548
+ stencilMode,
59549
+ meshState,
59550
+ format,
59551
+ sampleCount
59552
+ );
59553
+ const generation = this.generation;
59554
+ const promise = new Promise((resolve) => {
59555
+ this.warmQueue.push({
59556
+ run: () => {
59557
+ return typeof this.device.createRenderPipelineAsync === "function" ? this.device.createRenderPipelineAsync(descriptor) : this.device.createRenderPipeline(descriptor);
59558
+ },
59559
+ resolve: (pipeline) => {
59560
+ this.pending.delete(key);
59561
+ if (pipeline !== void 0 && generation === this.generation && !this.pipelines.has(key)) {
59562
+ this.pipelines.set(key, pipeline);
59563
+ this._touch(pipeline, format, sampleCount);
59564
+ }
59565
+ resolve();
59566
+ }
59567
+ });
59568
+ });
59569
+ this.pending.set(key, promise);
59570
+ this._pumpWarmQueue();
59571
+ return promise;
59572
+ }
59573
+ /**
59574
+ * Start queued warm-ups until {@link MAX_WARM_IN_FLIGHT} are compiling.
59575
+ * @ignore
59576
+ * @internal
59577
+ */
59578
+ _pumpWarmQueue() {
59579
+ while (this.warmInFlight < MAX_WARM_IN_FLIGHT && this.warmQueue.length > 0) {
59580
+ const job = this.warmQueue.shift();
59581
+ this.warmInFlight++;
59582
+ Promise.resolve().then(job.run).then(job.resolve, () => {
59583
+ job.resolve(void 0);
59584
+ }).finally(() => {
59585
+ this.warmInFlight--;
59586
+ this._pumpWarmQueue();
59587
+ });
59588
+ }
59589
+ }
59590
+ /**
59591
+ * Bind a freshly warmed pipeline once, in a pass of its own, so it is
59592
+ * compiled now rather than at the first draw that uses it.
59593
+ *
59594
+ * Some implementations accept `createRenderPipelineAsync` and still defer
59595
+ * the native compile to the first `setPipeline`, which puts it right back
59596
+ * in the frame the warm-up was meant to keep it out of. Setting it on a
59597
+ * throwaway 1×1 pass makes that first use happen here, while nothing is
59598
+ * on screen waiting for it. Where the compile already happened this is
59599
+ * one cheap bind.
59600
+ * @param {GPURenderPipeline} pipeline - the pipeline to bind
59601
+ * @param {string} format - its colour format
59602
+ * @param {number} sampleCount - its sample count
59603
+ * @ignore
59604
+ * @internal
59605
+ */
59606
+ _touch(pipeline, format, sampleCount) {
59607
+ const target = `${format}|${sampleCount}`;
59608
+ let batch = this.touchQueue.get(target);
59609
+ if (batch === void 0) {
59610
+ batch = [];
59611
+ this.touchQueue.set(target, batch);
59612
+ }
59613
+ batch.push(pipeline);
59614
+ if (this.touchQueue.size === 1 && batch.length === 1) {
59615
+ queueMicrotask(() => {
59616
+ this._flushTouches();
59617
+ });
59618
+ }
59619
+ }
59620
+ /**
59621
+ * Record and submit the passes queued by {@link WebGPUPipelineCache#_touch}.
59622
+ * @ignore
59623
+ * @internal
59624
+ */
59625
+ _flushTouches() {
59626
+ const device = this.device;
59627
+ const batches = [...this.touchQueue];
59628
+ this.touchQueue.clear();
59629
+ if (typeof device.createCommandEncoder !== "function") {
59630
+ return;
59631
+ }
59632
+ try {
59633
+ const encoder = device.createCommandEncoder({
59634
+ label: "melonJS pipeline warm-up"
59635
+ });
59636
+ for (const [target, pipelines] of batches) {
59637
+ const [format, samples] = target.split("|");
59638
+ const scratch = this._scratchTarget(format, Number(samples));
59639
+ const pass = encoder.beginRenderPass({
59640
+ label: "melonJS pipeline warm-up",
59641
+ colorAttachments: [
59642
+ { view: scratch.colorView, loadOp: "clear", storeOp: "discard" }
59643
+ ],
59644
+ depthStencilAttachment: {
59645
+ view: scratch.depthView,
59646
+ depthClearValue: 1,
59647
+ depthLoadOp: "clear",
59648
+ depthStoreOp: "discard",
59649
+ stencilLoadOp: "clear",
59650
+ stencilStoreOp: "discard"
59651
+ }
59652
+ });
59653
+ for (const pipeline of pipelines) {
59654
+ pass.setPipeline(pipeline);
59655
+ }
59656
+ pass.end();
59657
+ }
59658
+ device.queue.submit([encoder.finish()]);
59659
+ } catch {
59660
+ }
59661
+ }
59662
+ /**
59663
+ * The 1×1 colour + depth-stencil pair a warm-up pass binds for one
59664
+ * target shape, built once per shape.
59665
+ * @param {string} format - colour format
59666
+ * @param {number} sampleCount - sample count
59667
+ * @returns {{colorView: GPUTextureView, depthView: GPUTextureView}} the views
59668
+ * @ignore
59669
+ * @internal
59670
+ */
59671
+ _scratchTarget(format, sampleCount) {
59672
+ const key = `${format}|${sampleCount}`;
59673
+ let scratch = this.scratchTargets.get(key);
59674
+ if (scratch === void 0) {
59675
+ const color = this.device.createTexture({
59676
+ label: "melonJS warm-up target",
59677
+ size: [1, 1],
59678
+ format,
59679
+ sampleCount,
59680
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
59681
+ });
59682
+ const depth = this.device.createTexture({
59683
+ label: "melonJS warm-up depth",
59684
+ size: [1, 1],
59685
+ format: DEPTH_STENCIL_FORMAT,
59686
+ sampleCount,
59687
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
59688
+ });
59689
+ scratch = {
59690
+ color,
59691
+ depth,
59692
+ colorView: color.createView(),
59693
+ depthView: depth.createView()
59694
+ };
59695
+ this.scratchTargets.set(key, scratch);
59696
+ }
59697
+ return scratch;
59698
+ }
58859
59699
  /**
58860
59700
  * Assemble a pipeline descriptor.
58861
59701
  *
@@ -58866,17 +59706,33 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58866
59706
  * silently builds a second one under a key nothing will hit again.
58867
59707
  * @param {string} key - the cache key, used as the label
58868
59708
  * @param {string} shaderKey - the module family
58869
- * @param {string} blend - normalized blend mode
58870
- * @param {boolean} pma - premultiplied alpha
58871
- * @param {object} stencil - the resolved stencil state
58872
- * @param {object} primitive - the resolved primitive state
58873
- * @param {object} opts - `{vertexLayout, meshState, fogFlag}`
59709
+ * @param {string} topology - portable topology name
59710
+ * @param {string} blendMode - blend mode (normalized here)
59711
+ * @param {boolean} premultipliedAlpha - source premultiplication flag
59712
+ * @param {string} stencilMode - stencil mode
59713
+ * @param {object|undefined} meshState - mesh pass state
59714
+ * @param {string} format - colour target format
59715
+ * @param {number} sampleCount - pass sample count
58874
59716
  * @returns {GPURenderPipelineDescriptor} the descriptor
58875
59717
  * @ignore
58876
59718
  * @internal
58877
59719
  */
58878
- _descriptor(key, shaderKey, blend, pma, stencil, primitive, opts) {
58879
- const { vertexLayout, meshState, fogFlag } = opts;
59720
+ _descriptor(key, shaderKey, topology, blendMode, premultipliedAlpha, stencilMode, meshState, format, sampleCount) {
59721
+ const blend = normalizeBlendMode(blendMode);
59722
+ const pma = premultipliedAlpha !== false;
59723
+ const stencil = STENCIL_STATES[stencilMode] ?? STENCIL_STATES.none;
59724
+ const vertexLayout = this.vertexLayouts.get(
59725
+ this.vertexLayoutAliases.get(shaderKey) ?? shaderKey
59726
+ );
59727
+ const primitive = {
59728
+ topology,
59729
+ stripIndexFormat: topology.endsWith("-strip") ? "uint32" : void 0
59730
+ };
59731
+ if (meshState) {
59732
+ primitive.cullMode = meshState.cullMode;
59733
+ primitive.frontFace = meshState.frontFace;
59734
+ }
59735
+ const fogFlag = meshState?.fog === true ? 1 : 0;
58880
59736
  return {
58881
59737
  label: `melonJS ${key}`,
58882
59738
  layout: this.pipelineLayouts[shaderKey],
@@ -58892,7 +59748,7 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58892
59748
  ...meshState ? { constants: { enable_fog: fogFlag } } : {},
58893
59749
  targets: [
58894
59750
  {
58895
- format: this.format,
59751
+ format,
58896
59752
  blend: shaderKey === "clear" ? void 0 : gpuBlendState(blend, pma),
58897
59753
  writeMask: stencil.colorWriteMask
58898
59754
  }
@@ -58915,7 +59771,7 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58915
59771
  stencilReadMask: stencil.readMask,
58916
59772
  stencilWriteMask: stencil.writeMask
58917
59773
  },
58918
- multisample: { count: this.sampleCount }
59774
+ multisample: { count: sampleCount }
58919
59775
  };
58920
59776
  }
58921
59777
  /**
@@ -58924,6 +59780,17 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58924
59780
  */
58925
59781
  clear() {
58926
59782
  this.pipelines.clear();
59783
+ this.generation++;
59784
+ for (const job of this.warmQueue.splice(0)) {
59785
+ job.resolve(void 0);
59786
+ }
59787
+ this.pending.clear();
59788
+ this.touchQueue.clear();
59789
+ for (const scratch of this.scratchTargets.values()) {
59790
+ scratch.color.destroy?.();
59791
+ scratch.depth.destroy?.();
59792
+ }
59793
+ this.scratchTargets.clear();
58927
59794
  }
58928
59795
  };
58929
59796
 
@@ -59049,7 +59916,13 @@ var WebGPUQuadBatcher = class extends WebGPUBatcher {
59049
59916
  Math.min(u0, u1),
59050
59917
  Math.min(v0, v1)
59051
59918
  );
59052
- this.pushQuadVertices(x, y, w, h, u0, v0, u1, v1, tint, 0);
59919
+ const scale2 = effect._vertexScale;
59920
+ if (scale2 !== void 0 && (scale2.x !== 1 || scale2.y !== 1)) {
59921
+ const q = scaleQuad(x, y, w, h, scale2.x, scale2.y);
59922
+ this.pushQuadVertices(q[0], q[1], q[2], q[3], u0, v0, u1, v1, tint, 0);
59923
+ } else {
59924
+ this.pushQuadVertices(x, y, w, h, u0, v0, u1, v1, tint, 0);
59925
+ }
59053
59926
  this.flush();
59054
59927
  return;
59055
59928
  }
@@ -60708,7 +61581,11 @@ function uploadCompressedTexture(device, texture, image, metrics) {
60708
61581
  bytesPerRow: blocksPerRow * metrics.bytes,
60709
61582
  rowsPerImage: rows
60710
61583
  },
60711
- [mip.width, mip.height]
61584
+ // whole blocks: a level smaller than one block (the 2×2 and 1×1
61585
+ // tail of a 4×4-block chain) is stored padded to a full block,
61586
+ // and a copy of its logical size fails validation, leaving the
61587
+ // level empty
61588
+ [blocksPerRow * metrics.blockW, rows * metrics.blockH]
60712
61589
  );
60713
61590
  }
60714
61591
  }
@@ -60978,6 +61855,71 @@ var WebGPUTextureStore = class extends TextureStore {
60978
61855
  this.lastRecord = record;
60979
61856
  return this.bindGroupFor(record, texture, wrap, options.mipmaps === true);
60980
61857
  }
61858
+ /**
61859
+ * Build the mip-chain blitter, once. Split out of
61860
+ * {@link WebGPUTextureStore#generateMipmaps} so the warm-up can build it
61861
+ * before the first mipmapped texture is uploaded, which is otherwise in
61862
+ * the middle of the frame that first draws a mesh.
61863
+ * @ignore
61864
+ * @internal
61865
+ */
61866
+ ensureMipPipeline() {
61867
+ if (typeof this.mipPipeline !== "undefined") {
61868
+ return;
61869
+ }
61870
+ const device = this.device;
61871
+ const module = device.createShaderModule({
61872
+ label: "melonJS mip blit",
61873
+ code: mipblit_default
61874
+ });
61875
+ this.mipLayout = device.createBindGroupLayout({
61876
+ label: "melonJS mip blit layout",
61877
+ entries: [
61878
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
61879
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
61880
+ ]
61881
+ });
61882
+ this.mipPipeline = device.createRenderPipeline({
61883
+ label: "melonJS mip blit",
61884
+ layout: device.createPipelineLayout({
61885
+ bindGroupLayouts: [this.mipLayout]
61886
+ }),
61887
+ vertex: { module, entryPoint: "vertex_main" },
61888
+ fragment: {
61889
+ module,
61890
+ entryPoint: "fragment_main",
61891
+ targets: [{ format: "rgba8unorm" }]
61892
+ },
61893
+ primitive: { topology: "triangle-list" }
61894
+ });
61895
+ this.mipSampler = device.createSampler({
61896
+ label: "melonJS mip blit sampler",
61897
+ magFilter: "linear",
61898
+ minFilter: "linear"
61899
+ });
61900
+ }
61901
+ /**
61902
+ * Build the mip-chain blitter and run it once, on a throwaway 2×2
61903
+ * texture, so its native compile has happened before the first real
61904
+ * texture needs it. Creating a pipeline is not enough on every
61905
+ * implementation: some defer the compile to first use, so this uses it.
61906
+ * @ignore
61907
+ * @internal
61908
+ */
61909
+ warmMipmaps() {
61910
+ if (typeof this.mipPipeline !== "undefined") {
61911
+ return;
61912
+ }
61913
+ const texture = this.device.createTexture({
61914
+ label: "melonJS mip warm-up",
61915
+ size: [2, 2],
61916
+ format: "rgba8unorm",
61917
+ mipLevelCount: 2,
61918
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
61919
+ });
61920
+ this.generateMipmaps({ texture, mipLevelCount: 2 });
61921
+ texture.destroy();
61922
+ }
60981
61923
  /**
60982
61924
  * Build the full mip chain of a freshly-uploaded record: one blit pass
60983
61925
  * per level, each rendering the previous level with linear filtering.
@@ -60990,37 +61932,7 @@ var WebGPUTextureStore = class extends TextureStore {
60990
61932
  */
60991
61933
  generateMipmaps(record) {
60992
61934
  const device = this.device;
60993
- if (typeof this.mipPipeline === "undefined") {
60994
- const module = device.createShaderModule({
60995
- label: "melonJS mip blit",
60996
- code: mipblit_default
60997
- });
60998
- this.mipLayout = device.createBindGroupLayout({
60999
- label: "melonJS mip blit layout",
61000
- entries: [
61001
- { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
61002
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
61003
- ]
61004
- });
61005
- this.mipPipeline = device.createRenderPipeline({
61006
- label: "melonJS mip blit",
61007
- layout: device.createPipelineLayout({
61008
- bindGroupLayouts: [this.mipLayout]
61009
- }),
61010
- vertex: { module, entryPoint: "vertex_main" },
61011
- fragment: {
61012
- module,
61013
- entryPoint: "fragment_main",
61014
- targets: [{ format: "rgba8unorm" }]
61015
- },
61016
- primitive: { topology: "triangle-list" }
61017
- });
61018
- this.mipSampler = device.createSampler({
61019
- label: "melonJS mip blit sampler",
61020
- magFilter: "linear",
61021
- minFilter: "linear"
61022
- });
61023
- }
61935
+ this.ensureMipPipeline();
61024
61936
  const encoder = device.createCommandEncoder({ label: "melonJS mipgen" });
61025
61937
  for (let level2 = 1; level2 < record.mipLevelCount; level2++) {
61026
61938
  const bindGroup = device.createBindGroup({
@@ -61541,6 +62453,7 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
61541
62453
  this.uniformRing.reset();
61542
62454
  this.effectUniformArena.reset();
61543
62455
  this.frameId++;
62456
+ this.pipelineCache.inFrame = true;
61544
62457
  this.currentRenderTarget = null;
61545
62458
  this.pendingColorClear = false;
61546
62459
  this.effectPassDepth = 0;
@@ -61935,6 +62848,361 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
61935
62848
  );
61936
62849
  return frame;
61937
62850
  }
62851
+ /**
62852
+ * The post-effect render-target pool, created on first use.
62853
+ * @returns {RenderTargetPool} the pool
62854
+ * @ignore
62855
+ * @internal
62856
+ */
62857
+ _postEffectPool() {
62858
+ this._renderTargetPool ??= new RenderTargetPool(
62859
+ (w, h, isCapture, index) => {
62860
+ return new WebGPURenderTarget(this, w, h, {
62861
+ sampleCount: isCapture === true ? this.canvasSampleCount : 1,
62862
+ format: index < 2 ? this._colorFormat : this.preferredFormat
62863
+ });
62864
+ }
62865
+ );
62866
+ return this._renderTargetPool;
62867
+ }
62868
+ /**
62869
+ * The pipeline-cache family of an effect's WGSL realization, building its
62870
+ * module and layouts first if this device has not seen it yet.
62871
+ * @param {ShaderEffect} effect - an effect with a valid WGSL realization
62872
+ * @returns {string} the family key
62873
+ * @ignore
62874
+ * @internal
62875
+ */
62876
+ _effectFamily(effect) {
62877
+ const realization = effect.wgslRealization;
62878
+ if (realization.gpu === null || realization.gpu.epoch !== this.pipelineCache.epoch) {
62879
+ buildEffectGPU(this, effect, realization);
62880
+ }
62881
+ return realization.gpu.key;
62882
+ }
62883
+ /**
62884
+ * Build, off the frame, every pipeline a renderable's post effects can
62885
+ * draw with, and the render targets its chain will composite through.
62886
+ *
62887
+ * Which pipeline an effect draws with depends on where it sits: an
62888
+ * effect alone on a sprite draws the sprite itself (blended with the
62889
+ * sprite's own mode), one alone on a mesh is spliced into the mesh
62890
+ * shader, and one in a chain is a full-screen blit into either the next
62891
+ * offscreen target or the one below. Its colour format depends on that
62892
+ * target too, and under `hdr` the camera's targets are half-float while
62893
+ * the canvas is not. So this warms every position the effect can reach
62894
+ * with the effects ATTACHED, enabled or not, since turning one on moves
62895
+ * the others: enabling a camera's second effect sends its first one
62896
+ * from the canvas into the ping-pong target, a different pipeline.
62897
+ *
62898
+ * Each key is composed by the same function the draw uses, from the
62899
+ * renderable's own state (blend mode, culling, transparency, fog), so
62900
+ * what is built here is what the draw will ask for.
62901
+ * @param {Renderable} renderable - the renderable to warm for
62902
+ * @override
62903
+ * @ignore
62904
+ * @internal
62905
+ */
62906
+ _warmPostEffects(renderable) {
62907
+ if (!this._canWarm()) {
62908
+ return;
62909
+ }
62910
+ const cache3 = this.pipelineCache;
62911
+ const effects = renderable.postEffects.filter((fx) => {
62912
+ return fx?.wgslRealization?.valid === true;
62913
+ });
62914
+ const count = effects.length;
62915
+ if (count === 0) {
62916
+ return;
62917
+ }
62918
+ const isCamera = renderable._postEffectManaged === true;
62919
+ const needsCapture = renderable.postEffectNeedsCapture === true;
62920
+ const pooled = isCamera || count > 1 || needsCapture;
62921
+ const canvas = [this.canvasFormat, this.canvasSampleCount];
62922
+ const scene = this._sceneTargets();
62923
+ const blit = (family, blendMode2, [format, sampleCount]) => {
62924
+ void cache3.warm(
62925
+ family,
62926
+ "triangle-list",
62927
+ blendMode2,
62928
+ true,
62929
+ "none",
62930
+ void 0,
62931
+ format,
62932
+ sampleCount
62933
+ );
62934
+ };
62935
+ if (pooled) {
62936
+ const pool2 = this._postEffectPool();
62937
+ const { width, height } = this.getCanvas();
62938
+ pool2.reserve(
62939
+ isCamera,
62940
+ this._postEffectCapacity(renderable, effects),
62941
+ width,
62942
+ height
62943
+ );
62944
+ }
62945
+ if (isCamera) {
62946
+ for (const fx of effects) {
62947
+ const family = this._effectFamily(fx);
62948
+ blit(family, "none", [this._colorFormat, 1]);
62949
+ blit(family, "none", canvas);
62950
+ }
62951
+ this._warmToneMap();
62952
+ return;
62953
+ }
62954
+ const blendMode = renderable.blendMode ?? "normal";
62955
+ const meshHost = this._hostsMeshEffect(renderable);
62956
+ for (let i = 0; i < count; i++) {
62957
+ const fx = effects[i];
62958
+ const family = this._effectFamily(fx);
62959
+ if (!needsCapture) {
62960
+ if (meshHost) {
62961
+ this._warmHostedEffect(renderable, fx, scene);
62962
+ } else {
62963
+ for (const target of scene) {
62964
+ blit(family, blendMode, target);
62965
+ }
62966
+ }
62967
+ }
62968
+ if (pooled) {
62969
+ for (const target of scene) {
62970
+ blit(family, blendMode, target);
62971
+ }
62972
+ if (i < count - 1) {
62973
+ blit(family, "none", [this.preferredFormat, 1]);
62974
+ }
62975
+ }
62976
+ }
62977
+ if (pooled && !meshHost) {
62978
+ blit("quad", blendMode, [this.preferredFormat, this.canvasSampleCount]);
62979
+ }
62980
+ }
62981
+ /**
62982
+ * The `[format, sampleCount]` targets an ordinary renderable can be drawn
62983
+ * into: the canvas, or the camera's capture target when the camera runs
62984
+ * an effect chain of its own. The two differ only in format, and only
62985
+ * under `hdr`, so this is usually one target.
62986
+ * @returns {Array<Array<string|number>>} the targets
62987
+ * @ignore
62988
+ * @internal
62989
+ */
62990
+ _sceneTargets() {
62991
+ const canvas = [this.canvasFormat, this.canvasSampleCount];
62992
+ return this._colorFormat === this.canvasFormat ? [canvas] : [canvas, [this._colorFormat, this.canvasSampleCount]];
62993
+ }
62994
+ /**
62995
+ * Whether the warm-ups can run: a live device and a pipeline cache.
62996
+ * @returns {boolean} true when ready
62997
+ * @ignore
62998
+ * @internal
62999
+ */
63000
+ _canWarm() {
63001
+ return this.pipelineCache != null && typeof this.device !== "undefined" && this.isContextValid !== false;
63002
+ }
63003
+ /**
63004
+ * The mesh batcher a mesh draws through.
63005
+ * @param {object} mesh - the mesh
63006
+ * @returns {object|undefined} the batcher
63007
+ * @ignore
63008
+ * @internal
63009
+ */
63010
+ _meshBatcherFor(mesh) {
63011
+ return this.batchers.get(
63012
+ mesh.lit === true && this.batchers.has("litMesh") ? "litMesh" : "mesh"
63013
+ );
63014
+ }
63015
+ /**
63016
+ * Warm the pipelines a mesh will draw with when it hosts an effect: the
63017
+ * effect spliced into the mesh shader.
63018
+ * @param {object} mesh - the hosting mesh
63019
+ * @param {ShaderEffect} effect - the effect it hosts
63020
+ * @param {Array<Array<string|number>>} scene - the targets it can land on
63021
+ * @ignore
63022
+ * @internal
63023
+ */
63024
+ _warmHostedEffect(mesh, effect, scene) {
63025
+ const realization = effect.wgslRealization;
63026
+ if (meshHostingBlocker(realization.builtins, realization.textures, "wgsl") !== null) {
63027
+ return;
63028
+ }
63029
+ const batcher = this._meshBatcherFor(mesh);
63030
+ if (batcher === void 0 || realization.meshModule(batcher.shaderSource?.() ?? "") === null) {
63031
+ return;
63032
+ }
63033
+ const previous = batcher.hostedEffect;
63034
+ let family;
63035
+ batcher.hostedEffect = effect;
63036
+ try {
63037
+ family = batcher.activeShaderKey();
63038
+ } finally {
63039
+ batcher.hostedEffect = previous;
63040
+ }
63041
+ this._warmMeshDraw(mesh, family, scene);
63042
+ }
63043
+ /**
63044
+ * Warm the pipelines one mesh draw resolves to, keyed on the mesh's own
63045
+ * culling, depth test and transparency exactly as `drawMesh` keys them:
63046
+ * the opaque pass, the transparent pass, or both when `transparent` is
63047
+ * left unset (blended whenever the mesh is faded).
63048
+ * @param {object} mesh - the mesh
63049
+ * @param {string} family - the pipeline family it draws with
63050
+ * @param {Array<Array<string|number>>} scene - the targets it can land on
63051
+ * @ignore
63052
+ * @internal
63053
+ */
63054
+ _warmMeshDraw(mesh, family, scene) {
63055
+ const culling = mesh.cullBackFaces === true;
63056
+ const state4 = {
63057
+ cullMode: culling ? "back" : "none",
63058
+ // the retained path's winding rule, see `drawMesh`
63059
+ frontFace: culling && this.supportsRetainedMesh === true && mesh.rightHanded !== true ? "cw" : "ccw",
63060
+ depthWrite: void 0,
63061
+ depthTest: mesh.depthTest === false ? false : void 0,
63062
+ fog: this._fog3d != null ? true : void 0
63063
+ };
63064
+ const blendedOnly = mesh.transparent === true || mesh._blendedDraw === true;
63065
+ const blended = mesh.transparent !== false || mesh._blendedDraw === true;
63066
+ for (const [format, sampleCount] of scene) {
63067
+ if (!blendedOnly) {
63068
+ void this.pipelineCache.warm(
63069
+ family,
63070
+ "triangle-list",
63071
+ "none",
63072
+ true,
63073
+ "none",
63074
+ state4,
63075
+ format,
63076
+ sampleCount
63077
+ );
63078
+ }
63079
+ if (blended) {
63080
+ void this.pipelineCache.warm(
63081
+ family,
63082
+ "triangle-list",
63083
+ mesh._blendedDraw === true ? "normal" : mesh.blendMode,
63084
+ true,
63085
+ "none",
63086
+ { ...state4, depthWrite: false },
63087
+ format,
63088
+ sampleCount
63089
+ );
63090
+ }
63091
+ }
63092
+ }
63093
+ /**
63094
+ * Warm the renderable's own draw: a mesh through its mesh family, and
63095
+ * everything else that draws a texture through the quad family, each with
63096
+ * the renderable's blend mode. Containers draw nothing of their own, and
63097
+ * a camera is a chain, warmed with its effects.
63098
+ * @param {Renderable} renderable - the renderable to warm for
63099
+ * @override
63100
+ * @ignore
63101
+ * @internal
63102
+ */
63103
+ _warmDraw(renderable) {
63104
+ if (!this._canWarm() || renderable._postEffectManaged === true || Array.isArray(renderable.children)) {
63105
+ return;
63106
+ }
63107
+ const scene = this._sceneTargets();
63108
+ if (this._hostsMeshEffect(renderable)) {
63109
+ const batcher = this._meshBatcherFor(renderable);
63110
+ if (batcher?.shaderKey !== void 0) {
63111
+ this._warmMeshDraw(renderable, batcher.shaderKey, scene);
63112
+ }
63113
+ return;
63114
+ }
63115
+ if (renderable.instanceLayout !== void 0) {
63116
+ return;
63117
+ }
63118
+ const blendMode = renderable.blendMode ?? "normal";
63119
+ for (const [format, sampleCount] of scene) {
63120
+ void this.pipelineCache.warm(
63121
+ "quad",
63122
+ "triangle-list",
63123
+ blendMode,
63124
+ true,
63125
+ "none",
63126
+ void 0,
63127
+ format,
63128
+ sampleCount
63129
+ );
63130
+ }
63131
+ }
63132
+ /**
63133
+ * Build the advanced blend shader and its offscreen target before the
63134
+ * first draw in one of those modes, and warm the two pipelines a bracket
63135
+ * uses: the draw itself into the 8-bit offscreen, and the composite back
63136
+ * onto the scene.
63137
+ * @override
63138
+ * @ignore
63139
+ * @internal
63140
+ */
63141
+ _warmAdvancedBlend() {
63142
+ if (!this._canWarm()) {
63143
+ return;
63144
+ }
63145
+ if (typeof this._advancedBlendEffect === "undefined") {
63146
+ this._advancedBlendEffect = new BlendEffect(this);
63147
+ }
63148
+ const effect = this._advancedBlendEffect;
63149
+ if (effect.enabled !== true || effect.wgslRealization?.valid !== true) {
63150
+ return;
63151
+ }
63152
+ const { width, height } = this.getCanvas();
63153
+ if (typeof this._advancedBlendTarget === "undefined") {
63154
+ this._advancedBlendTarget = new WebGPURenderTarget(this, width, height, {
63155
+ sampleCount: 1,
63156
+ format: this.preferredFormat
63157
+ });
63158
+ }
63159
+ void this.pipelineCache.warm(
63160
+ "quad",
63161
+ "triangle-list",
63162
+ "normal",
63163
+ true,
63164
+ "none",
63165
+ void 0,
63166
+ this.preferredFormat,
63167
+ 1
63168
+ );
63169
+ const family = this._effectFamily(effect);
63170
+ for (const [format, sampleCount] of this._sceneTargets()) {
63171
+ void this.pipelineCache.warm(
63172
+ family,
63173
+ "triangle-list",
63174
+ "normal",
63175
+ true,
63176
+ "none",
63177
+ void 0,
63178
+ format,
63179
+ sampleCount
63180
+ );
63181
+ }
63182
+ }
63183
+ /**
63184
+ * Warm the tone curve's pipeline: always the last step of a camera chain,
63185
+ * so a full-screen blit onto the canvas with blending off.
63186
+ * @override
63187
+ * @ignore
63188
+ * @internal
63189
+ */
63190
+ _warmToneMap() {
63191
+ const toneMap = this._toneMapEffect;
63192
+ if (this.settings?.prewarm !== true || toneMap?.wgslRealization?.valid !== true || !this._canWarm()) {
63193
+ return;
63194
+ }
63195
+ void this.pipelineCache.warm(
63196
+ this._effectFamily(toneMap),
63197
+ "triangle-list",
63198
+ "none",
63199
+ true,
63200
+ "none",
63201
+ void 0,
63202
+ this.canvasFormat,
63203
+ this.canvasSampleCount
63204
+ );
63205
+ }
61938
63206
  /**
61939
63207
  * Begin a post-effect pass for the given renderable — the WebGPU
61940
63208
  * realization of the WebGL FBO path: the renderable's whole content
@@ -61967,17 +63235,9 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
61967
63235
  }
61968
63236
  savedProjection.copy(this.projectionMatrix);
61969
63237
  this.effectPassDepth++;
61970
- this._renderTargetPool ??= new RenderTargetPool(
61971
- (w, h, isCapture, index) => {
61972
- return new WebGPURenderTarget(this, w, h, {
61973
- sampleCount: isCapture === true ? this.canvasSampleCount : 1,
61974
- format: index < 2 ? this._colorFormat : this.preferredFormat
61975
- });
61976
- }
61977
- );
61978
- const rt = this._renderTargetPool.begin(
63238
+ const rt = this._postEffectPool().begin(
61979
63239
  isCamera,
61980
- effects.length,
63240
+ this._postEffectCapacity(renderable, effects),
61981
63241
  canvas.width,
61982
63242
  canvas.height
61983
63243
  );
@@ -62222,23 +63482,24 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62222
63482
  * WebGPU splits what WebGL fuses into one program. The **modules** for
62223
63483
  * the engine's own families — quad, blit, primitive, clear, and the mesh
62224
63484
  * tiers — are already created when the pipeline cache and the batchers
62225
- * are constructed, so there is nothing lazy there to warm. A **pipeline**
62226
- * is lazy, but it is keyed by draw state (blend, cull, depth-write) as
62227
- * well as by shader, so the set a scene will ask for cannot be known from
62228
- * here — and creating one costs almost nothing anyway, because WebGPU
62229
- * records the intent and leaves the work to first use.
62230
- *
62231
- * What IS both lazy and knowable is a `ShaderEffect`'s module:
62232
- * `prepareEffectBinding` builds it, and it is only ever called from
62233
- * inside the draw path. Every effect the loader compiled from a
62234
- * `{type: "shader"}` asset is already in `shaderList`, so those are
62235
- * exactly the ones that can be brought forward — which is what makes
62236
- * declaring a level's effects alongside that level's assets worth doing.
62237
- *
62238
- * Effects constructed inline when a stage builds are not here yet and
62239
- * cannot be warmed; that is a property of when they come into existence,
62240
- * not a limitation of this method.
62241
- * @returns {Promise<void>} settles once those modules are built
63485
+ * are constructed. A **pipeline** is where the real compile happens, and
63486
+ * it is the expensive part: tens of milliseconds each, paid in the frame
63487
+ * that first draws with it. But it is keyed by draw state (blend, cull,
63488
+ * depth write) and by the format of the target being drawn into, as well
63489
+ * as by shader, so the set a scene will ask for cannot be known from
63490
+ * here. Those are warmed when an effect is attached to a renderable
63491
+ * ({@link Renderable#addPostEffect}), which is the first moment the
63492
+ * renderable that decides them is known.
63493
+ *
63494
+ * What this does warm is what needs no renderable. A `ShaderEffect`'s
63495
+ * module: `prepareEffectBinding` builds it, and only ever from inside the
63496
+ * draw path. Every effect the loader compiled from a `{type: "shader"}`
63497
+ * asset is already in `shaderList`, so those can be brought forward —
63498
+ * which is what makes declaring a level's effects alongside that level's
63499
+ * assets worth doing. And the mip-chain blitter every mipmapped texture
63500
+ * goes through on upload, when textures are filtered (`textureFilter`
63501
+ * resolving to `"linear"`; nearest filtering never builds a chain).
63502
+ * @returns {Promise<void>} settles once those are built
62242
63503
  * @see {@link Renderer#prewarm}
62243
63504
  */
62244
63505
  prewarm() {
@@ -62252,6 +63513,23 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62252
63513
  buildEffectGPU(this, effect, realization);
62253
63514
  }
62254
63515
  }
63516
+ if (this.getDefaultTextureFilter() === "linear") {
63517
+ this.textureStore.warmMipmaps();
63518
+ }
63519
+ if (this._canWarm()) {
63520
+ for (const [format, sampleCount] of this._sceneTargets()) {
63521
+ void this.pipelineCache.warm(
63522
+ "clear",
63523
+ "triangle-list",
63524
+ "none",
63525
+ true,
63526
+ "none",
63527
+ void 0,
63528
+ format,
63529
+ sampleCount
63530
+ );
63531
+ }
63532
+ }
62255
63533
  });
62256
63534
  }
62257
63535
  /**
@@ -62273,6 +63551,9 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62273
63551
  this.frameTextureView = null;
62274
63552
  this.frameTexture = null;
62275
63553
  }
63554
+ if (this.pipelineCache != null) {
63555
+ this.pipelineCache.inFrame = false;
63556
+ }
62276
63557
  this.destroyRetiredTextures();
62277
63558
  }
62278
63559
  /**
@@ -62292,6 +63573,9 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62292
63573
  }
62293
63574
  this.frameTextureView = null;
62294
63575
  this.frameTexture = null;
63576
+ if (this.pipelineCache != null) {
63577
+ this.pipelineCache.inFrame = false;
63578
+ }
62295
63579
  this.currentRenderTarget = null;
62296
63580
  this.pendingColorClear = false;
62297
63581
  this.pendingDepthClear = false;
@@ -64050,6 +65334,10 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
64050
65334
  batcher.init(this);
64051
65335
  }
64052
65336
  emit(ONCONTEXT_RESTORED, this);
65337
+ if (this.settings.prewarm === true) {
65338
+ this.prewarm().catch(() => {
65339
+ });
65340
+ }
64053
65341
  }
64054
65342
  /**
64055
65343
  * Returns the WebGPU canvas context
@@ -69963,8 +71251,110 @@ var ShineEffect = class extends ShaderEffect {
69963
71251
  }
69964
71252
  };
69965
71253
 
69966
- // src/video/effects/tintPulse.js
71254
+ // src/video/effects/squish.js
69967
71255
  var wgslFragment15 = `
71256
+ struct SquishUniforms {
71257
+ uSquishFrame : vec4f,
71258
+ uSquishXFactor : f32,
71259
+ uSquishBulge : f32,
71260
+ };
71261
+ @group(3) @binding(0) var<uniform> fx : SquishUniforms;
71262
+
71263
+ fn apply(color : vec4f, uv : vec2f) -> vec4f {
71264
+ // -1..1 across the quad: the sprite's frame, stretched by the vertex scale
71265
+ var p = (uv - fx.uSquishFrame.xy) / fx.uSquishFrame.zw * 2.0 - 1.0;
71266
+ // undo the stretch: the sprite keeps its width, in the middle of the
71267
+ // wider quad, with room on both sides
71268
+ p.x = p.x * fx.uSquishXFactor;
71269
+ // each row widens by its share of the bulge: all of it across the
71270
+ // middle, none at the top and bottom
71271
+ p.x = p.x / max(1.0 + fx.uSquishBulge * sqrt(max(1.0 - p.y * p.y, 0.0)), 0.0001);
71272
+ let local = (p + 1.0) * 0.5;
71273
+ // sampled before the test, since textureSample needs uniform control flow
71274
+ let texel = textureSample(uTexture, uSampler, fx.uSquishFrame.xy + clamp(local, vec2f(0.0), vec2f(1.0)) * fx.uSquishFrame.zw) * vColor;
71275
+ let inside = all(local >= vec2f(0.0)) && all(local <= vec2f(1.0));
71276
+ return select(vec4f(0.0), texel, inside);
71277
+ }
71278
+ `;
71279
+ var SquishEffect = class _SquishEffect extends ShaderEffect {
71280
+ /**
71281
+ * @param {Renderer} renderer - the current renderer instance
71282
+ * @param {object} [options] - effect options
71283
+ * @param {number} [options.bulge=0] - how far the middle bulges out (positive) or pinches in (negative), from -1 to 1
71284
+ */
71285
+ constructor(renderer, options = {}) {
71286
+ super(renderer, {
71287
+ glsl: `
71288
+ uniform vec4 uSquishFrame;
71289
+ uniform float uSquishXFactor;
71290
+ uniform float uSquishBulge;
71291
+ vec4 apply(vec4 color, vec2 uv) {
71292
+ // -1..1 across the quad: the sprite's frame, stretched by the vertex scale
71293
+ vec2 p = (uv - uSquishFrame.xy) / uSquishFrame.zw * 2.0 - 1.0;
71294
+ // undo the stretch: the sprite keeps its width, in the middle
71295
+ // of the wider quad, with room on both sides
71296
+ p.x *= uSquishXFactor;
71297
+ // each row widens by its share of the bulge: all of it across
71298
+ // the middle, none at the top and bottom
71299
+ p.x /= max(1.0 + uSquishBulge * sqrt(max(1.0 - p.y * p.y, 0.0)), 0.0001);
71300
+ vec2 local = (p + 1.0) * 0.5;
71301
+ // transparent past the sprite: the room around it, or the
71302
+ // indent of a pinch
71303
+ if (local.x < 0.0 || local.x > 1.0 || local.y < 0.0 || local.y > 1.0) {
71304
+ return vec4(0.0);
71305
+ }
71306
+ return texture2D(uSampler, uSquishFrame.xy + local * uSquishFrame.zw) * vColor;
71307
+ }
71308
+ `,
71309
+ wgsl: wgslFragment15
71310
+ });
71311
+ this._bulge = 0;
71312
+ this._frame = new Float32Array([0, 0, 1, 1]);
71313
+ this.setUniform("uSquishFrame", this._frame);
71314
+ this.setBulge(options.bulge ?? 0);
71315
+ }
71316
+ /**
71317
+ * set how far the middle of the sprite bulges out, or pinches in
71318
+ * @param {number} bulge - from -1 to 1: positive bulges out, up to twice the width across the middle; negative pinches in, down to nothing at -1; 0 leaves the sprite as it is
71319
+ */
71320
+ setBulge(bulge) {
71321
+ const value = Math.min(Math.max(bulge, -1), 1);
71322
+ this._bulge = value;
71323
+ const xFactor = 1 + Math.max(value, 0);
71324
+ this._vertexScale.x = xFactor;
71325
+ this.setUniform("uSquishXFactor", xFactor);
71326
+ this.setUniform("uSquishBulge", value);
71327
+ }
71328
+ /**
71329
+ * The batchers hand every effect the frame it draws through here (it is
71330
+ * what `noise_uv` runs across), and the bulge is measured against the
71331
+ * same frame: where the sprite sits in its atlas, in UV units. A render
71332
+ * target blit hands over the whole target.
71333
+ * @ignore
71334
+ * @internal
71335
+ */
71336
+ _setNoiseUVRect(sourceWidth, sourceHeight, objectWidth, objectHeight, u0, v0) {
71337
+ const frame = this._frame;
71338
+ frame[0] = u0;
71339
+ frame[1] = v0;
71340
+ frame[2] = Math.max(Math.abs(objectWidth), 1) / Math.max(sourceWidth, 1);
71341
+ frame[3] = Math.max(Math.abs(objectHeight), 1) / Math.max(sourceHeight, 1);
71342
+ this.setUniform("uSquishFrame", frame);
71343
+ }
71344
+ /**
71345
+ * Create an independent copy of this effect, with the same bulge.
71346
+ * @returns {SquishEffect} a new effect
71347
+ */
71348
+ clone() {
71349
+ if (this.destroyed) {
71350
+ throw new Error("SquishEffect.clone: effect has been destroyed");
71351
+ }
71352
+ return new _SquishEffect(this._renderer, { bulge: this._bulge });
71353
+ }
71354
+ };
71355
+
71356
+ // src/video/effects/tintPulse.js
71357
+ var wgslFragment16 = `
69968
71358
  struct TintPulseUniforms {
69969
71359
  uPulseColor : vec3f,
69970
71360
  uPulseSpeed : f32,
@@ -69998,7 +71388,7 @@ var TintPulseEffect = class extends ShaderEffect {
69998
71388
  return vec4(mix(color.rgb, uPulseColor * color.a, pulse), color.a);
69999
71389
  }
70000
71390
  `,
70001
- wgsl: wgslFragment15
71391
+ wgsl: wgslFragment16
70002
71392
  });
70003
71393
  const color = options.color ?? [1, 0, 0];
70004
71394
  this.setUniform("uPulseColor", new Float32Array(color));
@@ -70025,7 +71415,7 @@ var fragment = `
70025
71415
  return vec4(color.rgb * v, color.a);
70026
71416
  }
70027
71417
  `;
70028
- var wgslFragment16 = `
71418
+ var wgslFragment17 = `
70029
71419
  struct VignetteUniforms {
70030
71420
  uStrength : f32,
70031
71421
  uSize : f32,
@@ -70046,7 +71436,7 @@ var VignetteEffect = class extends ShaderEffect {
70046
71436
  * @param {number} [options.size=25.0] - vignette spread multiplier (higher = smaller dark area)
70047
71437
  */
70048
71438
  constructor(renderer, options = {}) {
70049
- super(renderer, { glsl: fragment, wgsl: wgslFragment16 });
71439
+ super(renderer, { glsl: fragment, wgsl: wgslFragment17 });
70050
71440
  this.strength = options.strength ?? 0.15;
70051
71441
  this.size = options.size ?? 25;
70052
71442
  this.setUniform("uStrength", this.strength);
@@ -70071,7 +71461,7 @@ var VignetteEffect = class extends ShaderEffect {
70071
71461
  };
70072
71462
 
70073
71463
  // src/video/effects/wave.js
70074
- var wgslFragment17 = `
71464
+ var wgslFragment18 = `
70075
71465
  struct WaveUniforms {
70076
71466
  uAmplitude : f32,
70077
71467
  uFrequency : f32,
@@ -70107,7 +71497,7 @@ var WaveEffect = class extends ShaderEffect {
70107
71497
  return texture2D(uSampler, distorted) * vColor;
70108
71498
  }
70109
71499
  `,
70110
- wgsl: wgslFragment17
71500
+ wgsl: wgslFragment18
70111
71501
  });
70112
71502
  this.setUniform("uAmplitude", options.amplitude ?? 0.01);
70113
71503
  this.setUniform("uFrequency", options.frequency ?? 10);
@@ -71704,6 +73094,7 @@ export {
71704
73094
  Sphere,
71705
73095
  Sprite,
71706
73096
  Sprite3d,
73097
+ SquishEffect,
71707
73098
  Stage,
71708
73099
  TMXHexagonalRenderer,
71709
73100
  TMXIsometricRenderer,