melonjs 20.9.0 → 20.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/build/application/settings.d.ts +12 -8
  2. package/build/application/settings.d.ts.map +1 -1
  3. package/build/camera/camera2d.d.ts.map +1 -1
  4. package/build/index.d.ts +2 -1
  5. package/build/index.d.ts.map +1 -1
  6. package/build/index.js +1981 -237
  7. package/build/index.js.map +4 -4
  8. package/build/loader/loader.d.ts +62 -17
  9. package/build/loader/loader.d.ts.map +1 -1
  10. package/build/renderable/container.d.ts.map +1 -1
  11. package/build/renderable/renderable.d.ts +6 -0
  12. package/build/renderable/renderable.d.ts.map +1 -1
  13. package/build/renderable/sprite.d.ts.map +1 -1
  14. package/build/state/state.d.ts.map +1 -1
  15. package/build/tweens/tween.d.ts.map +1 -1
  16. package/build/video/effects/shadereffect.d.ts.map +1 -1
  17. package/build/video/effects/squish.d.ts +74 -0
  18. package/build/video/effects/squish.d.ts.map +1 -0
  19. package/build/video/renderer.d.ts +16 -3
  20. package/build/video/renderer.d.ts.map +1 -1
  21. package/build/video/rendertarget/render_target_pool.d.ts.map +1 -1
  22. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  23. package/build/video/webgl/batchers/material_batcher.d.ts +5 -0
  24. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  25. package/build/video/webgl/batchers/mesh_batcher.d.ts +21 -0
  26. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  27. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  28. package/build/video/webgl/buffer/deletable.d.ts +15 -0
  29. package/build/video/webgl/buffer/deletable.d.ts.map +1 -0
  30. package/build/video/webgl/buffer/instance_buffer.d.ts.map +1 -1
  31. package/build/video/webgl/buffer/retained_geometry.d.ts.map +1 -1
  32. package/build/video/webgl/buffer/uniformblock.d.ts.map +1 -1
  33. package/build/video/webgl/buffer/vertexstate.d.ts.map +1 -1
  34. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  35. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  36. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  37. package/build/video/webgpu/texture/compressed.d.ts.map +1 -1
  38. package/build/video/webgpu/texture/store.d.ts.map +1 -1
  39. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/skills/melonjs-effects-and-shaders/SKILL.md +50 -6
  42. package/skills/melonjs-loading-assets/SKILL.md +122 -13
  43. package/skills/melonjs-performance/SKILL.md +11 -3
  44. package/skills/melonjs-scenes-and-state/SKILL.md +7 -0
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 20.9.0
2
+ * melonJS Game Engine - 20.10.1
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;
@@ -16928,6 +16940,29 @@ var Camera2d = class extends Renderable {
16928
16940
  fx.draw(renderer, this.width, this.height);
16929
16941
  }
16930
16942
  }
16943
+ /**
16944
+ * The built-in {@link Camera2d#colorMatrix} as the effect that applies it,
16945
+ * made on first need and kept, and set to the current matrix.
16946
+ *
16947
+ * `draw` appends it last in the camera's chain for the frame. The
16948
+ * renderer's warm-up calls this too, once a stage has been built, so the
16949
+ * effect exists, and is warmed where `draw` will run it, before the frame
16950
+ * that first needs it rather than inside that frame.
16951
+ * @param renderer - the renderer the camera draws with
16952
+ * @returns the effect, or null while the matrix is identity
16953
+ * @ignore
16954
+ * @internal
16955
+ */
16956
+ _syncColorMatrixEffect(renderer) {
16957
+ if (this.colorMatrix.isIdentity()) {
16958
+ return null;
16959
+ }
16960
+ if (!this._colorMatrixEffect) {
16961
+ this._colorMatrixEffect = new ColorMatrixEffect(renderer);
16962
+ }
16963
+ this._colorMatrixEffect.reset().multiply(this.colorMatrix);
16964
+ return this._colorMatrixEffect;
16965
+ }
16931
16966
  /**
16932
16967
  * draw all objects visible in this viewport
16933
16968
  * @ignore
@@ -16943,12 +16978,9 @@ var Camera2d = class extends Renderable {
16943
16978
  const containerOffsetY = isNonDefault ? container.pos.y : 0;
16944
16979
  const translateX = this.pos.x + this.offset.x + containerOffsetX;
16945
16980
  const translateY = this.pos.y + this.offset.y + containerOffsetY;
16946
- if (!this.colorMatrix.isIdentity()) {
16947
- if (!this._colorMatrixEffect) {
16948
- this._colorMatrixEffect = new ColorMatrixEffect(renderer);
16949
- }
16950
- this._colorMatrixEffect.reset().multiply(this.colorMatrix);
16951
- this.postEffects.push(this._colorMatrixEffect);
16981
+ const colorMatrixEffect = this._syncColorMatrixEffect(renderer);
16982
+ if (colorMatrixEffect !== null) {
16983
+ this.postEffects.push(colorMatrixEffect);
16952
16984
  }
16953
16985
  const usePostEffect = r.beginPostEffect(this);
16954
16986
  this._applyContainerViewTransform(container, translateX, translateY);
@@ -17562,6 +17594,7 @@ var FrameAnimation = class {
17562
17594
 
17563
17595
  // src/renderable/sprite.js
17564
17596
  var FLICKER_INTERVAL_MS = 33;
17597
+ var VIDEO_GESTURES = ["touchstart", "touchend", "click", "keydown"];
17565
17598
  var Sprite = class extends Renderable {
17566
17599
  /**
17567
17600
  * @param {number} x - the x coordinates of the sprite object
@@ -17668,8 +17701,12 @@ var Sprite = class extends Renderable {
17668
17701
  }
17669
17702
  this.removeStatePauseListener = on(STATE_PAUSE, () => {
17670
17703
  this.image.pause();
17704
+ this._cancelVideoPlay();
17671
17705
  });
17672
17706
  this._lastVideoFrameVersion = -1;
17707
+ this._videoPlayPending = false;
17708
+ this._videoPlayRequest = 0;
17709
+ this._videoGestureListener = void 0;
17673
17710
  if (typeof this.image.requestVideoFrameCallback === "function") {
17674
17711
  this.image.version ??= 0;
17675
17712
  const tick2 = () => {
@@ -18144,8 +18181,8 @@ var Sprite = class extends Renderable {
18144
18181
  if (this.isVideo) {
18145
18182
  if (this.animationpause) {
18146
18183
  this.image.pause();
18147
- } else if (this.image.paused) {
18148
- this.image.play();
18184
+ } else if (this.image.paused && this._videoPlayPending !== true) {
18185
+ this._playVideo();
18149
18186
  }
18150
18187
  if (this.image.version === void 0) {
18151
18188
  if (!this.image.paused) {
@@ -18241,6 +18278,86 @@ var Sprite = class extends Renderable {
18241
18278
  // dw,dh
18242
18279
  );
18243
18280
  }
18281
+ /**
18282
+ * Start the video and handle the browser's answer. Before the user has
18283
+ * interacted with the page, browsers refuse playback: calling `play()`
18284
+ * again on every frame only logged an uncaught error per frame, so a
18285
+ * refusal now waits for the next gesture instead. Any other failure
18286
+ * (e.g. a `pause()` interrupting the request) retries on the next frame.
18287
+ * @ignore
18288
+ * @internal
18289
+ */
18290
+ _playVideo() {
18291
+ const request = ++this._videoPlayRequest;
18292
+ this._videoPlayPending = true;
18293
+ Promise.resolve(this.image.play()).then(
18294
+ () => {
18295
+ this._videoPlayPending = false;
18296
+ },
18297
+ (error) => {
18298
+ if (request !== this._videoPlayRequest) {
18299
+ return;
18300
+ }
18301
+ if (error?.name === "NotAllowedError") {
18302
+ this._awaitVideoGesture();
18303
+ } else {
18304
+ this._videoPlayPending = false;
18305
+ }
18306
+ }
18307
+ );
18308
+ }
18309
+ /**
18310
+ * Wait for the next user gesture and start the video from inside its
18311
+ * handler, the moment a browser that refused playback allows it.
18312
+ * @ignore
18313
+ * @internal
18314
+ */
18315
+ _awaitVideoGesture() {
18316
+ if (this._videoGestureListener !== void 0) {
18317
+ return;
18318
+ }
18319
+ this._videoGestureListener = () => {
18320
+ this._removeVideoGestureListener();
18321
+ this._videoPlayPending = false;
18322
+ if (this.animationpause !== true && this.image.paused) {
18323
+ this._playVideo();
18324
+ }
18325
+ };
18326
+ for (const type of VIDEO_GESTURES) {
18327
+ globalThis.document.addEventListener(
18328
+ type,
18329
+ this._videoGestureListener,
18330
+ true
18331
+ );
18332
+ }
18333
+ }
18334
+ /**
18335
+ * Drop a pending `play()` request and stop waiting for a gesture.
18336
+ * @ignore
18337
+ * @internal
18338
+ */
18339
+ _cancelVideoPlay() {
18340
+ this._videoPlayRequest++;
18341
+ this._videoPlayPending = false;
18342
+ this._removeVideoGestureListener();
18343
+ }
18344
+ /**
18345
+ * @ignore
18346
+ * @internal
18347
+ */
18348
+ _removeVideoGestureListener() {
18349
+ if (this._videoGestureListener === void 0) {
18350
+ return;
18351
+ }
18352
+ for (const type of VIDEO_GESTURES) {
18353
+ globalThis.document.removeEventListener(
18354
+ type,
18355
+ this._videoGestureListener,
18356
+ true
18357
+ );
18358
+ }
18359
+ this._videoGestureListener = void 0;
18360
+ }
18244
18361
  /**
18245
18362
  * Destroy function<br>
18246
18363
  * @ignore
@@ -18252,6 +18369,7 @@ var Sprite = class extends Renderable {
18252
18369
  this.offset = void 0;
18253
18370
  if (this.isVideo) {
18254
18371
  this.removeStatePauseListener();
18372
+ this._cancelVideoPlay();
18255
18373
  if (typeof this._videoFrameHandle !== "undefined") {
18256
18374
  this.image.cancelVideoFrameCallback(this._videoFrameHandle);
18257
18375
  delete this.image.version;
@@ -21693,6 +21811,85 @@ var Path2D = class {
21693
21811
  };
21694
21812
  var path2d_default = Path2D;
21695
21813
 
21814
+ // src/video/blendmodes.js
21815
+ var ADVANCED_BLEND_MODES = [
21816
+ "difference",
21817
+ "overlay",
21818
+ "hard-light",
21819
+ "color-dodge",
21820
+ "color-burn",
21821
+ "soft-light",
21822
+ // `darken` and `lighten` joined the shader path in 20.2: fixed-function
21823
+ // MIN/MAX compute `min(src, dst)` and nothing else, so there is nowhere to
21824
+ // put the `(1 - srcAlpha) * dst` term source-over contributes after the
21825
+ // blend. A translucent source lost the backdrop's share entirely — a white
21826
+ // `lighten` glow over a light backdrop rendered invisible. APPENDED, never
21827
+ // inserted: the index is the shader's branch value.
21828
+ "darken",
21829
+ "lighten"
21830
+ ];
21831
+ function isAdvancedBlendMode(mode) {
21832
+ return ADVANCED_BLEND_MODES.indexOf(mode) !== -1;
21833
+ }
21834
+ function advancedBlendModeIndex(mode) {
21835
+ return ADVANCED_BLEND_MODES.indexOf(mode);
21836
+ }
21837
+ function normalizeBlendMode(mode) {
21838
+ switch (mode) {
21839
+ case "none":
21840
+ case "multiply":
21841
+ case "screen":
21842
+ case "exclusion":
21843
+ case "normal":
21844
+ return mode;
21845
+ case "additive":
21846
+ case "add":
21847
+ case "lighter":
21848
+ return "additive";
21849
+ default:
21850
+ return "normal";
21851
+ }
21852
+ }
21853
+ function isSupportedBlendMode(mode) {
21854
+ return isAdvancedBlendMode(mode) || mode === "normal" || normalizeBlendMode(mode) !== "normal";
21855
+ }
21856
+ function reportedBlendMode(mode) {
21857
+ return isSupportedBlendMode(mode) ? mode : "normal";
21858
+ }
21859
+ function blendStateFor(mode, premultipliedAlpha) {
21860
+ const src = premultipliedAlpha ? "one" : "src-alpha";
21861
+ switch (mode) {
21862
+ case "none":
21863
+ return void 0;
21864
+ case "additive":
21865
+ return { operation: "add", srcFactor: src, dstFactor: "one" };
21866
+ case "multiply":
21867
+ return {
21868
+ operation: "add",
21869
+ srcFactor: "dst",
21870
+ dstFactor: "one-minus-src-alpha"
21871
+ };
21872
+ case "exclusion":
21873
+ return {
21874
+ operation: "add",
21875
+ srcFactor: "one-minus-dst",
21876
+ dstFactor: "one-minus-src"
21877
+ };
21878
+ case "screen":
21879
+ return {
21880
+ operation: "add",
21881
+ srcFactor: "one",
21882
+ dstFactor: "one-minus-src"
21883
+ };
21884
+ default:
21885
+ return {
21886
+ operation: "add",
21887
+ srcFactor: src,
21888
+ dstFactor: "one-minus-src-alpha"
21889
+ };
21890
+ }
21891
+ }
21892
+
21696
21893
  // src/video/effects/tonemap.js
21697
21894
  var CURVES = {
21698
21895
  /**
@@ -22171,12 +22368,25 @@ var Renderer = class _Renderer {
22171
22368
  *
22172
22369
  * Two consequences worth knowing:
22173
22370
  *
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.
22371
+ * - **A game that never calls `loader.preload()` never gets this part.**
22372
+ * Call it yourself if you load assets some other way and still want it.
22177
22373
  * - It is deliberately NOT called from `Application.init()`. That runs
22178
22374
  * before anything is on screen, so warming there would only lengthen a
22179
22375
  * blank page instead of hiding behind a progress bar.
22376
+ *
22377
+ * This is the half that needs no renderable. The other half cannot run
22378
+ * here, because what it compiles depends on the renderable drawing: under
22379
+ * WebGPU a pipeline is keyed by blend mode, culling, transparency and the
22380
+ * format of the target as well as by shader. So, behind the same
22381
+ * `prewarm` setting, the renderer also warms a renderable when it gains
22382
+ * a post effect ({@link Renderable#addPostEffect}) and when it is added
22383
+ * to the world ({@link Container#addChild}): its effect pipelines, its
22384
+ * own draw, the render targets its effect chain composites through, and
22385
+ * the advanced blend machinery if its blend mode needs it. That work is
22386
+ * started after the caller's code has run and finishes off the frame, so
22387
+ * it pays off when a renderable is set up at least a frame before it is
22388
+ * first drawn: build pools and attach their effects when a stage starts,
22389
+ * not when the object is first shown.
22180
22390
  * @returns {Promise<void>} settles when the built-in programs are ready
22181
22391
  * @example
22182
22392
  * // the usual case: nothing to write. `preload()` warms on the way through
@@ -23096,6 +23306,290 @@ var Renderer = class _Renderer {
23096
23306
  }
23097
23307
  return effects;
23098
23308
  }
23309
+ /**
23310
+ * How many effects a renderable's chain has to have render targets for:
23311
+ * every one attached, not just the ones enabled right now.
23312
+ *
23313
+ * Sizing the pool by the enabled count meant the ping-pong target was
23314
+ * only created on the frame a second effect was switched on, which is a
23315
+ * frame in the middle of play by construction (a hit flash, a power-up),
23316
+ * and creating a target there is a stall in that frame. A disabled effect
23317
+ * is one the game intends to turn on, so its target is made up front.
23318
+ * @param {Renderable} renderable - the renderable whose chain this is
23319
+ * @param {ShaderEffect[]} effects - its enabled chain, from `_effectChainFor`
23320
+ * @returns {number} the number of chain slots to provision
23321
+ * @ignore
23322
+ * @internal
23323
+ */
23324
+ _postEffectCapacity(renderable, effects) {
23325
+ return Math.max(effects.length, renderable.postEffects.length);
23326
+ }
23327
+ /**
23328
+ * Warm, ahead of its first draw, what a renderable will need: the
23329
+ * pipelines, programs and render targets that would otherwise be built
23330
+ * on the frame it first shows up.
23331
+ *
23332
+ * Called when a renderable gains a post effect
23333
+ * ({@link Renderable#addPostEffect}) and when it enters the world
23334
+ * ({@link Container#addChild}), the two moments its draw becomes
23335
+ * knowable. Deferred to a microtask so it runs once the caller's own code
23336
+ * has finished, which is when the rest of the picture exists: a
23337
+ * renderable usually gets its effect before it is added to the world,
23338
+ * sized, or given its blend mode, and those all decide which pipeline its
23339
+ * draw will ask for. Several requests in one task collapse into one
23340
+ * warm-up per renderable.
23341
+ *
23342
+ * Honours the `prewarm` setting, the same switch as the loading-screen
23343
+ * warm-up.
23344
+ * @param {Renderable} renderable - the renderable to warm for
23345
+ * @ignore
23346
+ * @internal
23347
+ */
23348
+ _queueWarm(renderable) {
23349
+ if (this.settings?.prewarm !== true || this.shaderLanguage == null) {
23350
+ return;
23351
+ }
23352
+ if (this._warmHosts === void 0) {
23353
+ this._warmHosts = /* @__PURE__ */ new Set();
23354
+ }
23355
+ if (this._warmHosts.size === 0) {
23356
+ queueMicrotask(() => {
23357
+ this._flushWarm();
23358
+ });
23359
+ }
23360
+ this._warmHosts.add(renderable);
23361
+ }
23362
+ /**
23363
+ * Run the warm-ups queued by {@link Renderer#_queueWarm}.
23364
+ * @param {boolean} [stage=false] - true when a stage is being entered,
23365
+ * which warms its cameras' grading as well
23366
+ * @returns {Promise<void>[]} what they started that is still being built
23367
+ * @ignore
23368
+ * @internal
23369
+ */
23370
+ _flushWarm(stage = false) {
23371
+ const hosts = this._warmHosts === void 0 ? [] : [...this._warmHosts];
23372
+ if (hosts.length === 0 && !stage) {
23373
+ return [];
23374
+ }
23375
+ this._warmHosts?.clear();
23376
+ const cameras = stage || hosts.some((host) => {
23377
+ return host?._postEffectManaged === true;
23378
+ });
23379
+ this._warmFog = this._warmFogStates();
23380
+ try {
23381
+ return this._collectWarms(() => {
23382
+ for (const host of hosts) {
23383
+ try {
23384
+ this._warmRenderable(host);
23385
+ } catch {
23386
+ }
23387
+ }
23388
+ if (cameras) {
23389
+ try {
23390
+ this._warmCameraColorMatrices();
23391
+ } catch {
23392
+ }
23393
+ }
23394
+ });
23395
+ } finally {
23396
+ this._warmFog = void 0;
23397
+ }
23398
+ }
23399
+ /**
23400
+ * Run `warm` and hand back what it started that is still being built,
23401
+ * as one promise each. Every warm-up on this base is done by the time it
23402
+ * returns, so nothing.
23403
+ * @param {Function} warm - issues the warm-ups
23404
+ * @returns {Promise<void>[]} the warm-ups still on their way
23405
+ * @ignore
23406
+ * @internal
23407
+ */
23408
+ _collectWarms(warm) {
23409
+ warm();
23410
+ return [];
23411
+ }
23412
+ /**
23413
+ * Run the queued warm-ups now, and say when they are built.
23414
+ *
23415
+ * What a state change waits on before drawing the stage it has just built
23416
+ * (see `state.change`). A warm-up on its own does not keep the first
23417
+ * frame from compiling: the frame comes some milliseconds after the stage
23418
+ * is built, and a pipeline still compiling then is compiled again, on the
23419
+ * spot, by the draw that needs it. Holding the frame until they land is
23420
+ * what lets them do their job.
23421
+ *
23422
+ * Only what this flush asked for is waited on, not every warm-up in
23423
+ * flight: one a game started for later (an effect attached ahead of
23424
+ * time, a level's own preload) is not this stage's to wait for.
23425
+ * @returns {Promise<void>|undefined} settles once they are built, or
23426
+ * `undefined` when nothing is being built and the frame can draw now
23427
+ * @ignore
23428
+ * @internal
23429
+ */
23430
+ _whenWarmed() {
23431
+ let waits;
23432
+ try {
23433
+ waits = this._flushWarm(true);
23434
+ } catch {
23435
+ return void 0;
23436
+ }
23437
+ if (waits.length === 0) {
23438
+ return void 0;
23439
+ }
23440
+ return Promise.all(waits).then(() => {
23441
+ });
23442
+ }
23443
+ /**
23444
+ * Warm each of the current stage's cameras for its built-in colour
23445
+ * matrix.
23446
+ *
23447
+ * A camera applies `colorMatrix` as one more effect at the end of its
23448
+ * chain, but it built that effect inside its own `draw`, on the first
23449
+ * frame the matrix was not identity, and appends it for that frame only.
23450
+ * Nothing ever attached it, so nothing warmed it: its program (WebGL) or
23451
+ * its pipelines (WebGPU) were built in the first frame of a stage that
23452
+ * grades its camera. Made here instead, once the stage has been built,
23453
+ * and warmed where `draw` runs it, last in the camera's chain.
23454
+ * @ignore
23455
+ * @internal
23456
+ */
23457
+ _warmCameraColorMatrices() {
23458
+ const cameras = state_default.current()?.cameras;
23459
+ if (cameras === void 0) {
23460
+ return;
23461
+ }
23462
+ for (const camera of cameras.values()) {
23463
+ const effect = camera._syncColorMatrixEffect?.(this);
23464
+ if (effect == null) {
23465
+ continue;
23466
+ }
23467
+ const effects = camera.postEffects;
23468
+ effects.push(effect);
23469
+ try {
23470
+ this._warmPostEffects(camera);
23471
+ } finally {
23472
+ effects.splice(effects.indexOf(effect), 1);
23473
+ }
23474
+ }
23475
+ }
23476
+ /**
23477
+ * Whether the meshes being warmed will draw with fog, without it, or both.
23478
+ *
23479
+ * Fog is a pipeline variant, and which one a mesh draws with is decided by
23480
+ * the camera drawing it: each camera installs its own fog on the renderer
23481
+ * as it starts drawing ({@link Renderer#setFog}), and the frame clears it.
23482
+ * So the renderer's own value is no answer here. A warm-up runs between
23483
+ * frames, while the renderer still holds the fog of whichever camera drew
23484
+ * LAST, which after a state change is the previous screen's: a loading
23485
+ * screen has none, so every mesh of a fogged scene was warmed without it
23486
+ * and then compiled with it, synchronously, on its first frame.
23487
+ *
23488
+ * Asked of the current stage's cameras instead, through the same
23489
+ * `_fog3dState` the draw installs, so the warm-up and the draw cannot
23490
+ * disagree. A stage with a fogged camera and a clear one (a minimap, say)
23491
+ * warms both variants. Fog switched on in a later frame than the scene was
23492
+ * built in is not seen here, and is built on that frame as before.
23493
+ * @returns {boolean[]} the fog states to warm, each `true` for fogged
23494
+ * @ignore
23495
+ * @internal
23496
+ */
23497
+ _warmFogStates() {
23498
+ let fogged = false;
23499
+ let clear = false;
23500
+ const cameras = state_default.current()?.cameras;
23501
+ if (cameras !== void 0) {
23502
+ for (const camera of cameras.values()) {
23503
+ if (camera._fog3dState?.(this) != null) {
23504
+ fogged = true;
23505
+ } else {
23506
+ clear = true;
23507
+ }
23508
+ }
23509
+ }
23510
+ if (!fogged && !clear) {
23511
+ return [this._fog3d != null];
23512
+ }
23513
+ return clear && fogged ? [false, true] : [fogged];
23514
+ }
23515
+ /**
23516
+ * Warm one renderable, and everything under it when it is a container:
23517
+ * adding a container to the world brings its whole subtree in with it,
23518
+ * without an `addChild` per descendant.
23519
+ * @param {Renderable} renderable - the renderable to warm for
23520
+ * @ignore
23521
+ * @internal
23522
+ */
23523
+ _warmRenderable(renderable) {
23524
+ if (renderable == null) {
23525
+ return;
23526
+ }
23527
+ if (renderable.postEffects?.length > 0) {
23528
+ this._warmPostEffects(renderable);
23529
+ }
23530
+ this._warmDraw(renderable);
23531
+ if (isAdvancedBlendMode(renderable.blendMode)) {
23532
+ this._warmAdvancedBlend();
23533
+ }
23534
+ const children = renderable.children;
23535
+ if (Array.isArray(children)) {
23536
+ for (const child of children) {
23537
+ this._warmRenderable(child);
23538
+ }
23539
+ }
23540
+ }
23541
+ /**
23542
+ * Warm everything one renderable's post effects will need. Each backend
23543
+ * knows what that is; this base has no shaders, so nothing.
23544
+ * @param {Renderable} renderable - the renderable to warm for
23545
+ * @ignore
23546
+ * @internal
23547
+ */
23548
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
23549
+ _warmPostEffects(renderable) {
23550
+ }
23551
+ /**
23552
+ * Warm the renderable's own draw, the one it makes without any effect.
23553
+ * Only meaningful where that is a pipeline keyed by draw state; nothing
23554
+ * to do on this base.
23555
+ * @param {Renderable} renderable - the renderable to warm for
23556
+ * @ignore
23557
+ * @internal
23558
+ */
23559
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
23560
+ _warmDraw(renderable) {
23561
+ }
23562
+ /**
23563
+ * Warm the advanced blend modes' machinery: the blend shader, built on
23564
+ * the first draw that uses one of those modes, and its offscreen target.
23565
+ * Nothing to do on this base.
23566
+ * @ignore
23567
+ * @internal
23568
+ */
23569
+ _warmAdvancedBlend() {
23570
+ }
23571
+ /**
23572
+ * Whether a renderable draws through `drawMesh`, where a lone effect is
23573
+ * spliced into the mesh shader instead of being drawn as a quad.
23574
+ * Duck-typed on fields only a `Mesh` carries, so the renderer does not
23575
+ * have to import the renderable classes. An instanced mesh never hosts
23576
+ * an effect: its shading is a separate family that knows nothing of one.
23577
+ * @param {Renderable} renderable - the renderable to test
23578
+ * @returns {boolean} true for a non-instanced mesh
23579
+ * @ignore
23580
+ * @internal
23581
+ */
23582
+ _hostsMeshEffect(renderable) {
23583
+ return typeof renderable.cullBackFaces === "boolean" && renderable.indices !== void 0 && renderable.instanceLayout === void 0;
23584
+ }
23585
+ /**
23586
+ * Warm the tone curve's pipeline once it exists. Nothing to do on a
23587
+ * backend that compiles it in its constructor.
23588
+ * @ignore
23589
+ * @internal
23590
+ */
23591
+ _warmToneMap() {
23592
+ }
23099
23593
  /**
23100
23594
  * Set the tone curve applied at the end of the camera's chain.
23101
23595
  *
@@ -23172,6 +23666,7 @@ var Renderer = class _Renderer {
23172
23666
  this._toneMapEffect?.destroy?.();
23173
23667
  this._toneMapEffect = new ToneMappingEffect(this, { mode });
23174
23668
  this._toneMapEffect.shared = true;
23669
+ this._warmToneMap();
23175
23670
  }
23176
23671
  this._toneMapEffect.setExposure(this.settings.toneMappingExposure ?? 1);
23177
23672
  this._toneMapEffect.setWhite(this.settings.toneMappingWhite ?? 0);
@@ -28585,6 +29080,7 @@ var Container = class _Container extends Renderable {
28585
29080
  registerChildBody(cchild, worldContainer);
28586
29081
  });
28587
29082
  }
29083
+ worldContainer?.app?.renderer?._queueWarm?.(child);
28588
29084
  }
28589
29085
  if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
28590
29086
  child.onActivateEvent();
@@ -28627,6 +29123,7 @@ var Container = class _Container extends Renderable {
28627
29123
  registerChildBody(cchild, worldContainer);
28628
29124
  });
28629
29125
  }
29126
+ worldContainer?.app?.renderer?._queueWarm?.(child);
28630
29127
  }
28631
29128
  if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
28632
29129
  child.onActivateEvent();
@@ -39865,6 +40362,8 @@ var _transitionConfig = {
39865
40362
  duration: 0
39866
40363
  };
39867
40364
  var _onSwitchComplete = null;
40365
+ var MAX_WARM_HOLD = 1500;
40366
+ var _heldSwitch = null;
39868
40367
  var _extraArgs = null;
39869
40368
  var _pauseTime = 0;
39870
40369
  var _freezeTimer = null;
@@ -39918,9 +40417,17 @@ function _renderFrame(time) {
39918
40417
  }
39919
40418
  }
39920
40419
  function _stopRunLoop() {
40420
+ _endHeldSwitches();
39921
40421
  globalThis.cancelAnimationFrame(_animFrameId);
39922
40422
  _animFrameId = -1;
39923
40423
  }
40424
+ function _endHeldSwitches() {
40425
+ while (_heldSwitch !== null) {
40426
+ const finish = _heldSwitch;
40427
+ _heldSwitch = null;
40428
+ finish();
40429
+ }
40430
+ }
39924
40431
  function _switchState(stateId) {
39925
40432
  _stopRunLoop();
39926
40433
  if (_stages[_state]) {
@@ -39928,17 +40435,45 @@ function _switchState(stateId) {
39928
40435
  }
39929
40436
  if (_stages[stateId]) {
39930
40437
  _state = stateId;
40438
+ const onSwitchComplete = _onSwitchComplete;
40439
+ _onSwitchComplete = null;
40440
+ const finish = () => {
40441
+ try {
40442
+ emit(STATE_CHANGE);
40443
+ } finally {
40444
+ onSwitchComplete?.();
40445
+ }
40446
+ };
39931
40447
  if (_extraArgs) {
39932
40448
  _stages[_state].stage.reset(_app2, ..._extraArgs);
39933
40449
  } else {
39934
40450
  _stages[_state].stage.reset(_app2);
39935
40451
  }
39936
- _startRunLoop();
39937
- emit(STATE_CHANGE);
39938
- if (_onSwitchComplete) {
39939
- _onSwitchComplete();
39940
- _onSwitchComplete = null;
40452
+ const renderer = _app2?.renderer;
40453
+ const warmed = renderer?._whenWarmed?.();
40454
+ _endHeldSwitches();
40455
+ if (warmed === void 0) {
40456
+ _startRunLoop();
40457
+ finish();
40458
+ return;
39941
40459
  }
40460
+ _heldSwitch = finish;
40461
+ let cap;
40462
+ const start = () => {
40463
+ clearTimeout(cap);
40464
+ if (_heldSwitch !== finish) {
40465
+ return;
40466
+ }
40467
+ _heldSwitch = null;
40468
+ _startRunLoop();
40469
+ finish();
40470
+ };
40471
+ void Promise.race([
40472
+ warmed,
40473
+ new Promise((resolve) => {
40474
+ cap = setTimeout(resolve, MAX_WARM_HOLD);
40475
+ })
40476
+ ]).then(start, start);
39942
40477
  }
39943
40478
  }
39944
40479
  var _app2;
@@ -40122,7 +40657,7 @@ var state3 = {
40122
40657
  * @returns true if a "process is running"
40123
40658
  */
40124
40659
  isRunning() {
40125
- return _animFrameId !== -1;
40660
+ return _animFrameId !== -1 || _heldSwitch !== null;
40126
40661
  },
40127
40662
  /**
40128
40663
  * Return the pause state of the state manager
@@ -41909,6 +42444,23 @@ var WebGLIndexBuffer = class extends IndexBuffer {
41909
42444
  }
41910
42445
  };
41911
42446
 
42447
+ // src/video/webgl/buffer/deletable.js
42448
+ var lostOnce = /* @__PURE__ */ new WeakSet();
42449
+ function markContextLost(gl) {
42450
+ if (gl) {
42451
+ lostOnce.add(gl);
42452
+ }
42453
+ }
42454
+ function deletable(gl, probe, handle) {
42455
+ if (gl.isContextLost() === true) {
42456
+ return false;
42457
+ }
42458
+ if (lostOnce.has(gl) === false) {
42459
+ return true;
42460
+ }
42461
+ return probe.call(gl, handle) === true;
42462
+ }
42463
+
41912
42464
  // src/video/webgl/buffer/vertexstate.js
41913
42465
  function bufferGroups(descriptor) {
41914
42466
  if (Array.isArray(descriptor.buffers)) {
@@ -42057,12 +42609,15 @@ var WebGLVertexState = class {
42057
42609
  /**
42058
42610
  * Delete the underlying vertex array object, if any. Idempotent, and
42059
42611
  * 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.
42612
+ * context would queue `INVALID_OPERATION`, because deletion counts as
42613
+ * *using* it.
42614
+ *
42615
+ * The probe behind `deletable` is synchronous, so it is skipped unless
42616
+ * this context has actually lost one — see `buffer/deletable.js`.
42062
42617
  */
42063
42618
  release() {
42064
42619
  if (this.handle !== null) {
42065
- if (this.gl.isVertexArray(this.handle)) {
42620
+ if (deletable(this.gl, this.gl.isVertexArray, this.handle)) {
42066
42621
  this.gl.deleteVertexArray(this.handle);
42067
42622
  }
42068
42623
  this.handle = null;
@@ -43054,6 +43609,14 @@ function transformQuadCorners(m, x, y, w, h, z) {
43054
43609
  }
43055
43610
  return corners;
43056
43611
  }
43612
+ var scaled = new Float64Array(4);
43613
+ function scaleQuad(x, y, w, h, sx, sy) {
43614
+ scaled[0] = x + w * (1 - sx) / 2;
43615
+ scaled[1] = y + h * (1 - sy) / 2;
43616
+ scaled[2] = w * sx;
43617
+ scaled[3] = h * sy;
43618
+ return scaled;
43619
+ }
43057
43620
 
43058
43621
  // src/video/webgl/shaders/multitexture.js
43059
43622
  function buildMultiTextureFragment(maxTextures) {
@@ -43092,6 +43655,8 @@ var immutableStorage = /* @__PURE__ */ new WeakMap();
43092
43655
  function mipLevels(w, h) {
43093
43656
  return Math.floor(Math.log2(Math.max(w, h))) + 1;
43094
43657
  }
43658
+ var COMPRESSED_RGB_ETC1_WEBGL3 = 36196;
43659
+ var COMPRESSED_RGB8_ETC22 = 37492;
43095
43660
  var MaterialBatcher = class extends WebGLBatcher {
43096
43661
  /**
43097
43662
  * Initialize the textured batcher
@@ -43201,11 +43766,20 @@ var MaterialBatcher = class extends WebGLBatcher {
43201
43766
  this.currentTextureUnit = unit;
43202
43767
  const resourceOwned = pixels !== null && typeof pixels.upload === "function";
43203
43768
  const compressed = pixels !== null && pixels.compressed === true;
43769
+ let compressedFormat = compressed ? pixels.format : void 0;
43770
+ let mutableETC1 = false;
43771
+ if (compressedFormat === COMPRESSED_RGB_ETC1_WEBGL3) {
43772
+ if (this.renderer.getSupportedCompressedTextureFormats().etc2) {
43773
+ compressedFormat = COMPRESSED_RGB8_ETC22;
43774
+ } else {
43775
+ mutableETC1 = true;
43776
+ }
43777
+ }
43204
43778
  if (!resourceOwned) {
43205
43779
  const storageW = Math.max(1, w | 0);
43206
43780
  const storageH = Math.max(1, h | 0);
43207
43781
  const levels2 = compressed ? pixels.mipmaps.length : mipmap === true ? mipLevels(storageW, storageH) : 1;
43208
- const format = compressed ? pixels.format : gl.RGBA8;
43782
+ const format = compressed ? compressedFormat : gl.RGBA8;
43209
43783
  let storage = immutableStorage.get(currentTexture);
43210
43784
  if (storage && (storage.width !== storageW || storage.height !== storageH || storage.levels !== levels2 || storage.format !== format)) {
43211
43785
  gl.deleteTexture(currentTexture);
@@ -43214,7 +43788,7 @@ var MaterialBatcher = class extends WebGLBatcher {
43214
43788
  this.boundTextures[unit] = currentTexture;
43215
43789
  storage = void 0;
43216
43790
  }
43217
- if (!storage) {
43791
+ if (!storage && !mutableETC1) {
43218
43792
  gl.texStorage2D(gl.TEXTURE_2D, levels2, format, storageW, storageH);
43219
43793
  immutableStorage.set(currentTexture, {
43220
43794
  width: storageW,
@@ -43233,18 +43807,37 @@ var MaterialBatcher = class extends WebGLBatcher {
43233
43807
  pixels.upload(gl, gl.TEXTURE_2D);
43234
43808
  } else if (compressed) {
43235
43809
  const mipmaps = pixels.mipmaps;
43236
- for (let i = 0; i < mipmaps.length; i++) {
43237
- gl.compressedTexSubImage2D(
43810
+ if (mutableETC1) {
43811
+ gl.texParameteri(
43238
43812
  gl.TEXTURE_2D,
43239
- i,
43240
- 0,
43241
- 0,
43242
- mipmaps[i].width,
43243
- mipmaps[i].height,
43244
- pixels.format,
43245
- mipmaps[i].data
43813
+ gl.TEXTURE_MAX_LEVEL,
43814
+ mipmaps.length - 1
43246
43815
  );
43247
43816
  }
43817
+ for (let i = 0; i < mipmaps.length; i++) {
43818
+ if (mutableETC1) {
43819
+ gl.compressedTexImage2D(
43820
+ gl.TEXTURE_2D,
43821
+ i,
43822
+ compressedFormat,
43823
+ mipmaps[i].width,
43824
+ mipmaps[i].height,
43825
+ 0,
43826
+ mipmaps[i].data
43827
+ );
43828
+ } else {
43829
+ gl.compressedTexSubImage2D(
43830
+ gl.TEXTURE_2D,
43831
+ i,
43832
+ 0,
43833
+ 0,
43834
+ mipmaps[i].width,
43835
+ mipmaps[i].height,
43836
+ compressedFormat,
43837
+ mipmaps[i].data
43838
+ );
43839
+ }
43840
+ }
43248
43841
  } else if (pixels === null) {
43249
43842
  } else if (typeof pixels.byteLength !== "undefined") {
43250
43843
  gl.texSubImage2D(
@@ -43653,6 +44246,14 @@ var QuadBatcher = class extends MaterialBatcher {
43653
44246
  );
43654
44247
  }
43655
44248
  this.currentShader._setUVYDir?.(1);
44249
+ const scale2 = this.currentShader._vertexScale;
44250
+ if (scale2 !== void 0 && (scale2.x !== 1 || scale2.y !== 1)) {
44251
+ const q = scaleQuad(x, y, w, h, scale2.x, scale2.y);
44252
+ x = q[0];
44253
+ y = q[1];
44254
+ w = q[2];
44255
+ h = q[3];
44256
+ }
43656
44257
  }
43657
44258
  const [vec0, vec1, vec2, vec3] = transformQuadCorners(
43658
44259
  this.viewMatrix,
@@ -43670,85 +44271,6 @@ var QuadBatcher = class extends MaterialBatcher {
43670
44271
  }
43671
44272
  };
43672
44273
 
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
44274
  // src/video/effects/blendEffect.js
43753
44275
  var wgslFragment2 = `
43754
44276
  struct BlendUniforms {
@@ -44032,6 +44554,26 @@ var RenderTargetPool = class {
44032
44554
  }
44033
44555
  return rt;
44034
44556
  }
44557
+ /**
44558
+ * Create, ahead of the pass that will use them, the targets a top-level
44559
+ * chain needs: the camera pair, or the first sprite pair. Targets that
44560
+ * already exist are left as they are (the pass resizes them when it
44561
+ * begins), and the active-pass stack is not touched, so this is safe
44562
+ * outside any pass.
44563
+ * @param {boolean} isCamera - true for the camera pair (0/1), false for the first sprite pair (2/3)
44564
+ * @param {number} effectCount - the number of effects in the chain
44565
+ * @param {number} width - target width in pixels
44566
+ * @param {number} height - target height in pixels
44567
+ */
44568
+ reserve(isCamera, effectCount, width, height) {
44569
+ const base = isCamera ? 0 : 2;
44570
+ if (!this._pool[base]) {
44571
+ this.get(base, width, height);
44572
+ }
44573
+ if (effectCount > 1 && !this._pool[base + 1]) {
44574
+ this.get(base + 1, width, height);
44575
+ }
44576
+ }
44035
44577
  /**
44036
44578
  * Get the capture render target for the current active pass.
44037
44579
  * @returns {RenderTarget|undefined} the capture target, or undefined if no active pass
@@ -44751,7 +45293,7 @@ var UniformBlock = class {
44751
45293
  destroy() {
44752
45294
  const gl = this.gl;
44753
45295
  if (this.buffer !== null) {
44754
- if (gl.isBuffer(this.buffer)) {
45296
+ if (deletable(gl, gl.isBuffer, this.buffer)) {
44755
45297
  gl.deleteBuffer(this.buffer);
44756
45298
  }
44757
45299
  this.buffer = null;
@@ -45004,14 +45546,13 @@ var WebGLInstanceBuffer = class {
45004
45546
  );
45005
45547
  }
45006
45548
  /**
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.
45549
+ * Release the GL buffer. Safe to call twice, and on a lost context:
45550
+ * deleting a handle whose context died raises INVALID_OPERATION.
45010
45551
  */
45011
45552
  destroy() {
45012
45553
  const gl = this.gl;
45013
45554
  if (this.buffer !== null) {
45014
- if (gl.isBuffer(this.buffer)) {
45555
+ if (deletable(gl, gl.isBuffer, this.buffer)) {
45015
45556
  gl.deleteBuffer(this.buffer);
45016
45557
  }
45017
45558
  this.buffer = null;
@@ -45097,13 +45638,13 @@ var RetainedGeometry = class {
45097
45638
  this.vertexState = null;
45098
45639
  }
45099
45640
  if (this.vertexBuffer !== null) {
45100
- if (gl.isBuffer(this.vertexBuffer)) {
45641
+ if (deletable(gl, gl.isBuffer, this.vertexBuffer)) {
45101
45642
  gl.deleteBuffer(this.vertexBuffer);
45102
45643
  }
45103
45644
  this.vertexBuffer = null;
45104
45645
  }
45105
45646
  if (this.glIndexBuffer !== null) {
45106
- if (gl.isBuffer(this.glIndexBuffer)) {
45647
+ if (deletable(gl, gl.isBuffer, this.glIndexBuffer)) {
45107
45648
  gl.deleteBuffer(this.glIndexBuffer);
45108
45649
  }
45109
45650
  this.glIndexBuffer = null;
@@ -45142,6 +45683,14 @@ var _EYE_POSITION = new Float32Array(3);
45142
45683
  var _FOG_COLOR = new Float32Array(3);
45143
45684
  var _FOG_PARAMS = new Float32Array(4);
45144
45685
  var _FOG_HEIGHT = new Float32Array(4);
45686
+ function prewarmYield() {
45687
+ if (globalThis.document?.hidden === true) {
45688
+ return Promise.resolve();
45689
+ }
45690
+ return new Promise((resolve) => {
45691
+ setTimeout(resolve, 0);
45692
+ });
45693
+ }
45145
45694
  var MeshBatcher = class extends MaterialBatcher {
45146
45695
  /**
45147
45696
  * Initialize the mesh batcher
@@ -45725,7 +46274,21 @@ var MeshBatcher = class extends MaterialBatcher {
45725
46274
  * @internal
45726
46275
  */
45727
46276
  effectShaderFor(effect) {
45728
- const fog = this._fogDefine();
46277
+ return this._effectShader(effect, this._fogDefine());
46278
+ }
46279
+ /**
46280
+ * The spliced program for one effect under one fog state.
46281
+ *
46282
+ * Split out of {@link MeshBatcher#effectShaderFor} so `prewarm` can build
46283
+ * BOTH fog variants at load, rather than only the one the current scene
46284
+ * happens to be using.
46285
+ * @param {object} effect - the hosted ShaderEffect
46286
+ * @param {string} fog - the fog define, or ""
46287
+ * @returns {GLShader|null} the spliced program, or null if it will not build
46288
+ * @ignore
46289
+ * @internal
46290
+ */
46291
+ _effectShader(effect, fog) {
45729
46292
  const sources = this._shaderSources();
45730
46293
  const fragment2 = spliceEffect(sources.fragment, effect._meshBody);
45731
46294
  if (fragment2 === null) {
@@ -45858,17 +46421,14 @@ var MeshBatcher = class extends MaterialBatcher {
45858
46421
  * @ignore
45859
46422
  * @internal
45860
46423
  */
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
- };
46424
+ /**
46425
+ * @param {object[]} [effects] - hosted ShaderEffects to warm the spliced
46426
+ * mesh variants for, normally every preloaded `{type: "shader"}` asset
46427
+ */
46428
+ async prewarm(effects = []) {
46429
+ const yieldToFrame = prewarmYield;
45870
46430
  const alive = () => {
45871
- return this.gl !== null && this.gl?.isContextLost?.() !== true;
46431
+ return this._prewarmAlive();
45872
46432
  };
45873
46433
  const sources = this._shaderSources();
45874
46434
  const instanced = this._instancedShaderSources();
@@ -45906,6 +46466,54 @@ var MeshBatcher = class extends MaterialBatcher {
45906
46466
  return;
45907
46467
  }
45908
46468
  }
46469
+ await this.prewarmEffects(effects);
46470
+ }
46471
+ /**
46472
+ * Build the spliced mesh variant of each effect, in both fog states.
46473
+ *
46474
+ * A ShaderEffect hosted on a mesh does NOT draw with the program its
46475
+ * asset compiled to: the body is spliced into the mesh shader, which is a
46476
+ * different program again, and it was built on the frame the effect first
46477
+ * drew. Preloading the shader looked like it covered this and did not, so
46478
+ * a game that declared every effect in its manifest still stuttered the
46479
+ * first time each one appeared. Warming them here is what makes the
46480
+ * preload mean what it looks like it means.
46481
+ *
46482
+ * Separate from {@link MeshBatcher#prewarm} because the two halves have
46483
+ * different lifetimes: the batcher's own variants are built once, while
46484
+ * the effects grow with every preload.
46485
+ * @param {object[]} [effects] - hosted ShaderEffects, normally the
46486
+ * preloaded `{type: "shader"}` assets
46487
+ * @returns {Promise<void>} settles once every variant is linked
46488
+ * @ignore
46489
+ * @internal
46490
+ */
46491
+ async prewarmEffects(effects = []) {
46492
+ for (const fogDefine of ["", "#define FOG\n"]) {
46493
+ for (const effect of effects) {
46494
+ if (effect?._meshBody === void 0) {
46495
+ continue;
46496
+ }
46497
+ this._effectShader(effect, fogDefine);
46498
+ await prewarmYield();
46499
+ if (!this._prewarmAlive()) {
46500
+ return;
46501
+ }
46502
+ }
46503
+ }
46504
+ }
46505
+ /**
46506
+ * Whether a warm-up may keep building into this batcher. A yield is a
46507
+ * window in which the application can be destroyed or the context lost,
46508
+ * and building into a batcher that has been torn down leaks a `GLShader`
46509
+ * per remaining variant, each subscribed to the global context-loss
46510
+ * events with nothing left to destroy it.
46511
+ * @returns {boolean} true while the context is live
46512
+ * @ignore
46513
+ * @internal
46514
+ */
46515
+ _prewarmAlive() {
46516
+ return this.gl !== null && this.gl?.isContextLost?.() !== true;
45909
46517
  }
45910
46518
  /**
45911
46519
  * Whether the bound program is one this batcher would pick for a plain
@@ -47418,6 +48026,14 @@ var LitQuadBatcher = class extends QuadBatcher {
47418
48026
  );
47419
48027
  }
47420
48028
  this.currentShader._setUVYDir?.(1);
48029
+ const scale2 = this.currentShader._vertexScale;
48030
+ if (scale2 !== void 0 && (scale2.x !== 1 || scale2.y !== 1)) {
48031
+ const q = scaleQuad(x, y, w, h, scale2.x, scale2.y);
48032
+ x = q[0];
48033
+ y = q[1];
48034
+ w = q[2];
48035
+ h = q[3];
48036
+ }
47421
48037
  }
47422
48038
  let normalTextureId = -1;
47423
48039
  let shininess = 0;
@@ -48123,6 +48739,7 @@ function initBlendEnums(gl) {
48123
48739
  "one-minus-dst-alpha": gl.ONE_MINUS_DST_ALPHA
48124
48740
  };
48125
48741
  }
48742
+ var MESH_EFFECT_LINK_BUDGET = 4;
48126
48743
  var WebGLRenderer = class _WebGLRenderer extends Renderer {
48127
48744
  /**
48128
48745
  * @param {ApplicationSettings} [options] - optional parameters for the renderer
@@ -48214,6 +48831,7 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48214
48831
  (e) => {
48215
48832
  e.preventDefault();
48216
48833
  this.isContextValid = false;
48834
+ markContextLost(this.gl);
48217
48835
  this.vertexBuffer = null;
48218
48836
  emit(ONCONTEXT_LOST, this);
48219
48837
  },
@@ -48252,6 +48870,11 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48252
48870
  if (renderer === this) {
48253
48871
  this.currentProgram = void 0;
48254
48872
  this._prewarmed = void 0;
48873
+ this._prewarmedEffects = /* @__PURE__ */ new WeakSet();
48874
+ if (this.settings.prewarm === true) {
48875
+ this.prewarm().catch(() => {
48876
+ });
48877
+ }
48255
48878
  }
48256
48879
  };
48257
48880
  this.onCanvasResize = (width, height) => {
@@ -48407,18 +49030,213 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48407
49030
  * @see {@link Renderer#prewarm}
48408
49031
  */
48409
49032
  prewarm() {
49033
+ this._prewarmedEffects ??= /* @__PURE__ */ new WeakSet();
49034
+ const effects = Object.values(shaderList).filter((effect) => {
49035
+ return effect != null && !this._prewarmedEffects.has(effect);
49036
+ });
49037
+ for (const effect of effects) {
49038
+ this._prewarmedEffects.add(effect);
49039
+ }
49040
+ const batchers = [...this.batchers.values()];
48410
49041
  if (this._prewarmed === void 0) {
48411
49042
  this._prewarmed = Promise.all(
48412
- [...this.batchers.values()].map((batcher) => {
48413
- return batcher.prewarm();
49043
+ batchers.map((batcher) => {
49044
+ return batcher.prewarm(effects);
48414
49045
  })
48415
49046
  ).then(() => {
48416
49047
  this.setBatcher("quad");
48417
49048
  });
49049
+ } else if (effects.length > 0) {
49050
+ this._prewarmed = this._prewarmed.then(() => {
49051
+ return Promise.all(
49052
+ batchers.map((batcher) => {
49053
+ return batcher.prewarmEffects?.(effects);
49054
+ })
49055
+ ).then(() => {
49056
+ this.setBatcher("quad");
49057
+ });
49058
+ });
48418
49059
  }
48419
49060
  return this._prewarmed;
48420
49061
  }
49062
+ /**
49063
+ * Warm what one renderable's post effects will need on this backend.
49064
+ *
49065
+ * An effect's own program is linked when the effect is constructed, so
49066
+ * what is left is what depends on where it is drawn: a mesh does not
49067
+ * draw an effect with that program but with the effect spliced into the
49068
+ * mesh shader, built on first draw; and a chain composites through
49069
+ * render targets, built on the frame the chain first runs or grows a
49070
+ * second effect.
49071
+ * @param {Renderable} renderable - the renderable to warm for
49072
+ * @override
49073
+ * @ignore
49074
+ * @internal
49075
+ */
49076
+ _warmPostEffects(renderable) {
49077
+ const gl = this.gl;
49078
+ if (gl === null || gl.isContextLost?.() === true) {
49079
+ return;
49080
+ }
49081
+ const effects = renderable.postEffects.filter((fx) => {
49082
+ return fx != null;
49083
+ });
49084
+ if (effects.length === 0) {
49085
+ return;
49086
+ }
49087
+ const isCamera = renderable._postEffectManaged === true;
49088
+ if (this._effectPassDepth === 0 && (isCamera || effects.length > 1 || renderable.postEffectNeedsCapture === true)) {
49089
+ const { width, height } = this.getCanvas();
49090
+ this.flush();
49091
+ this._renderTargetPool.reserve(
49092
+ isCamera,
49093
+ this._postEffectCapacity(renderable, effects),
49094
+ width,
49095
+ height
49096
+ );
49097
+ this.invalidateTextureUnit(0);
49098
+ }
49099
+ if (!this._hostsMeshEffect(renderable)) {
49100
+ return;
49101
+ }
49102
+ const batcher = this.batchers.get(
49103
+ renderable.lit === true ? "litMesh" : "mesh"
49104
+ );
49105
+ if (batcher === void 0) {
49106
+ return;
49107
+ }
49108
+ this._meshEffectWarmQueue ??= [];
49109
+ for (const fog of this._warmFog ?? [this._fog3d != null]) {
49110
+ const fogDefine = fog ? "#define FOG\n" : "";
49111
+ for (const fx of effects) {
49112
+ this._meshEffectWarmQueue.push(batcher, fx, fogDefine);
49113
+ }
49114
+ }
49115
+ if (this._meshEffectWarmScheduled !== true) {
49116
+ this._meshEffectWarmScheduled = true;
49117
+ this._meshEffectWarmDone = new Promise((resolve) => {
49118
+ this._meshEffectWarmSettle = resolve;
49119
+ });
49120
+ queueMicrotask(() => {
49121
+ this._linkQueuedMeshEffects();
49122
+ });
49123
+ }
49124
+ }
49125
+ /**
49126
+ * Run `warm` and hand back what is still to be built once it returns:
49127
+ * the spliced mesh programs it queued, which are linked over the next
49128
+ * tasks rather than all at once.
49129
+ * @param {Function} warm - issues the warm-ups
49130
+ * @returns {Promise<void>[]} the queued links, as one promise, or nothing
49131
+ * @override
49132
+ * @ignore
49133
+ * @internal
49134
+ */
49135
+ _collectWarms(warm) {
49136
+ warm();
49137
+ return this._meshEffectWarmScheduled === true ? [this._meshEffectWarmDone] : [];
49138
+ }
49139
+ /**
49140
+ * Link the spliced mesh programs queued by `_warmPostEffects`, a few
49141
+ * milliseconds' worth per task, so a pool's worth spreads over the next
49142
+ * frames instead of landing in one. A draw that comes first simply links
49143
+ * its own program, as it would have without the warm-up.
49144
+ * @ignore
49145
+ * @internal
49146
+ */
49147
+ _linkQueuedMeshEffects() {
49148
+ const queue = this._meshEffectWarmQueue;
49149
+ const gl = this.gl;
49150
+ if (gl === null || gl.isContextLost?.() === true) {
49151
+ queue.length = 0;
49152
+ this._settleMeshEffectWarm();
49153
+ return;
49154
+ }
49155
+ const start = globalThis.performance.now();
49156
+ const previous = this.currentProgram;
49157
+ try {
49158
+ while (queue.length > 0 && globalThis.performance.now() - start < MESH_EFFECT_LINK_BUDGET) {
49159
+ const batcher = queue.shift();
49160
+ const fx = queue.shift();
49161
+ const fogDefine = queue.shift();
49162
+ if (fx.destroyed !== true && batcher.canHostEffect?.(fx) === true) {
49163
+ batcher._effectShader(fx, fogDefine);
49164
+ }
49165
+ }
49166
+ } finally {
49167
+ this._restoreProgram(previous);
49168
+ }
49169
+ if (queue.length === 0) {
49170
+ this._settleMeshEffectWarm();
49171
+ } else if (globalThis.document?.hidden === true) {
49172
+ this._linkQueuedMeshEffects();
49173
+ } else {
49174
+ setTimeout(() => {
49175
+ this._linkQueuedMeshEffects();
49176
+ }, 0);
49177
+ }
49178
+ }
49179
+ /**
49180
+ * The link queue is empty: let the next warm-up schedule a new one, and
49181
+ * release whatever is waiting on this one.
49182
+ * @ignore
49183
+ * @internal
49184
+ */
49185
+ _settleMeshEffectWarm() {
49186
+ this._meshEffectWarmScheduled = false;
49187
+ this._meshEffectWarmSettle?.();
49188
+ this._meshEffectWarmSettle = void 0;
49189
+ }
49190
+ /**
49191
+ * Build the advanced blend shader and its offscreen target before the
49192
+ * first draw in one of those modes, which otherwise compiles the shader
49193
+ * and creates the target in the middle of that frame.
49194
+ * @override
49195
+ * @ignore
49196
+ * @internal
49197
+ */
49198
+ _warmAdvancedBlend() {
49199
+ const gl = this.gl;
49200
+ if (gl === null || gl.isContextLost?.() === true) {
49201
+ return;
49202
+ }
49203
+ if (typeof this._advancedBlendEffect === "undefined") {
49204
+ const previous = this.currentProgram;
49205
+ try {
49206
+ this._advancedBlendEffect = new BlendEffect(this);
49207
+ } finally {
49208
+ this._restoreProgram(previous);
49209
+ }
49210
+ }
49211
+ if (typeof this._advancedBlendTarget === "undefined" && this._effectPassDepth === 0 && this._advancedBlendOpen !== true) {
49212
+ const { width, height } = this.getCanvas();
49213
+ this.flush();
49214
+ this._advancedBlendTarget = new WebGLRenderTarget(gl, width, height, {
49215
+ samples: 0,
49216
+ format: gl.RGBA8
49217
+ });
49218
+ this.invalidateTextureUnit(0);
49219
+ }
49220
+ }
49221
+ /**
49222
+ * Put back the program a warm-up found bound. Linking leaves the new
49223
+ * program bound, and the batchers trust what they last bound, so a
49224
+ * warm-up between two draws would otherwise hand the next one a
49225
+ * stranger's program.
49226
+ * @param {WebGLProgram|undefined} previous - what was bound before
49227
+ * @ignore
49228
+ * @internal
49229
+ */
49230
+ _restoreProgram(previous) {
49231
+ if (previous != null && this.currentProgram !== previous) {
49232
+ this.gl.useProgram(previous);
49233
+ this.currentProgram = previous;
49234
+ }
49235
+ }
48421
49236
  destroy() {
49237
+ if (this._meshEffectWarmQueue !== void 0) {
49238
+ this._meshEffectWarmQueue.length = 0;
49239
+ }
48422
49240
  this._toneMapEffect?.destroy?.();
48423
49241
  this._toneMapEffect = void 0;
48424
49242
  off(GAME_RESET, this.onGameReset);
@@ -48956,7 +49774,12 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
48956
49774
  }
48957
49775
  savedProjection.copy(this.projectionMatrix);
48958
49776
  this._effectPassDepth++;
48959
- const rt = this._renderTargetPool.begin(isCamera, effects.length, w, h);
49777
+ const rt = this._renderTargetPool.begin(
49778
+ isCamera,
49779
+ this._postEffectCapacity(renderable, effects),
49780
+ w,
49781
+ h
49782
+ );
48960
49783
  this.invalidateTextureUnit(0);
48961
49784
  rt.bind();
48962
49785
  this.setViewport(0, 0, w, h);
@@ -49447,8 +50270,16 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
49447
50270
  * @see WebGLRenderer#createPattern
49448
50271
  */
49449
50272
  drawPattern(pattern, x, y, width, height) {
50273
+ if (this.customShader?._screenTextureUniforms?.length > 0) {
50274
+ this.toFrameTexture();
50275
+ }
49450
50276
  const uvs = pattern.getUVs("0,0," + width + "," + height);
49451
50277
  this.setBatcher("quad");
50278
+ const shader = this.customShader;
50279
+ if (shader != null) {
50280
+ this.currentBatcher.useShader(shader);
50281
+ shader._prepareTextures?.(this.currentBatcher);
50282
+ }
49452
50283
  this.currentBatcher.addQuad(
49453
50284
  pattern,
49454
50285
  x,
@@ -56056,7 +56887,7 @@ var Trigger = class extends Renderable {
56056
56887
  };
56057
56888
 
56058
56889
  // src/version.ts
56059
- var version = "20.9.0";
56890
+ var version = "20.10.1";
56060
56891
 
56061
56892
  // src/system/bootstrap.ts
56062
56893
  var initialized = false;
@@ -58499,6 +59330,8 @@ function gpuBlendState(mode, premultipliedAlpha) {
58499
59330
  return typeof component === "undefined" ? void 0 : { color: component, alpha: component };
58500
59331
  }
58501
59332
  var MAX_QUAD_TEXTURES = 8;
59333
+ var MAX_WARM_IN_FLIGHT = 16;
59334
+ var MISS_KEYS_KEPT = 64;
58502
59335
  var DEPTH_STENCIL_FORMAT = "depth24plus-stencil8";
58503
59336
  var STENCIL_STATES = {
58504
59337
  none: {
@@ -58575,6 +59408,16 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58575
59408
  this.sampleCount = 1;
58576
59409
  this.pipelines = /* @__PURE__ */ new Map();
58577
59410
  this.vertexLayouts = /* @__PURE__ */ new Map();
59411
+ this.inFrame = false;
59412
+ this.inFrameMisses = 0;
59413
+ this.inFrameMissKeys = [];
59414
+ this.pending = /* @__PURE__ */ new Map();
59415
+ this.collecting = void 0;
59416
+ this.warmQueue = [];
59417
+ this.warmInFlight = 0;
59418
+ this.touchQueue = /* @__PURE__ */ new Map();
59419
+ this.scratchTargets = /* @__PURE__ */ new Map();
59420
+ this.generation = 0;
58578
59421
  this.frameLayout = device.createBindGroupLayout({
58579
59422
  label: "melonJS frame globals layout",
58580
59423
  entries: [
@@ -58800,24 +59643,33 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58800
59643
  );
58801
59644
  }
58802
59645
  /**
58803
- * Fetch (building on first use) the pipeline for the given state tuple.
58804
- * @param {string} shaderKey - "quad" | "primitive" | "clear"
59646
+ * The cache key for a pipeline state tuple.
59647
+ *
59648
+ * The ONE place a key is composed. A draw looks its pipeline up through
59649
+ * {@link WebGPUPipelineCache#get} and a warm-up builds one ahead of time
59650
+ * through {@link WebGPUPipelineCache#warm}, and both come here, so the two
59651
+ * cannot drift into building the same pipeline under two names: a warm-up
59652
+ * whose key differs from the draw's by one character compiles a pipeline
59653
+ * nothing will ever ask for, and the draw still compiles its own.
59654
+ *
59655
+ * The colour format and sample count default to the cache's CURRENT ones,
59656
+ * which the renderer rewrites per render target. A warm-up runs outside
59657
+ * any pass, where those describe whatever was bound last, so it passes
59658
+ * the target it is warming for explicitly.
59659
+ * @param {string} shaderKey - "quad" | "primitive" | "clear" | a registered family
58805
59660
  * @param {string} topology - portable topology name ("triangle-list", …)
58806
59661
  * @param {string} blendMode - blend mode (normalized internally)
58807
59662
  * @param {boolean} premultipliedAlpha - source premultiplication flag
58808
59663
  * @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
59664
+ * @param {object} [meshState] - the mesh pass state, see {@link WebGPUPipelineCache#get}
59665
+ * @param {string} [format] - colour target format, the current one by default
59666
+ * @param {number} [sampleCount] - pass sample count, the current one by default
59667
+ * @returns {string} the key
58816
59668
  */
58817
- get(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode = "none", meshState) {
59669
+ keyFor(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode = "none", meshState, format = this.format, sampleCount = this.sampleCount) {
58818
59670
  const blend = normalizeBlendMode(blendMode);
58819
- const pma = premultipliedAlpha !== false;
58820
- let key = `${shaderKey}|${topology}|${blend}|${pma ? 1 : 0}|${stencilMode}|${this.format}|${this.sampleCount}`;
59671
+ const pma = blend === "none" || premultipliedAlpha !== false;
59672
+ let key = `${shaderKey}|${topology}|${blend}|${pma ? 1 : 0}|${stencilMode}|${format}|${sampleCount}`;
58821
59673
  if (meshState) {
58822
59674
  key += `|mesh:${meshState.cullMode}:${meshState.frontFace}`;
58823
59675
  if (meshState.depthWrite === false) {
@@ -58830,32 +59682,281 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58830
59682
  key += "|fog";
58831
59683
  }
58832
59684
  }
58833
- const fogFlag = meshState?.fog === true ? 1 : 0;
59685
+ return key;
59686
+ }
59687
+ /**
59688
+ * Fetch (building on first use) the pipeline for the given state tuple.
59689
+ * @param {string} shaderKey - "quad" | "primitive" | "clear"
59690
+ * @param {string} topology - portable topology name ("triangle-list", …)
59691
+ * @param {string} blendMode - blend mode (normalized internally)
59692
+ * @param {boolean} premultipliedAlpha - source premultiplication flag
59693
+ * @param {string} [stencilMode="none"] - "none" | "write" | "test" | "tag" | "mark"
59694
+ * @param {{cullMode: string, frontFace: string, depthWrite?: boolean, depthTest?: boolean, fog?: boolean}} [meshState] - mesh pass
59695
+ * state: its presence switches the depth half of the attachment on —
59696
+ * depth writes enabled, "less-equal" testing (the GL mesh mode's
59697
+ * LEQUAL, keeping coplanar geometry stable) — and sets the per-mesh
59698
+ * face-culling axes. Omitted by the whole 2D tier, whose keys and
59699
+ * descriptors stay byte-identical to a build without a mesh path.
59700
+ * @returns {GPURenderPipeline} the pipeline
59701
+ */
59702
+ get(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode = "none", meshState) {
59703
+ const key = this.keyFor(
59704
+ shaderKey,
59705
+ topology,
59706
+ blendMode,
59707
+ premultipliedAlpha,
59708
+ stencilMode,
59709
+ meshState
59710
+ );
58834
59711
  let pipeline = this.pipelines.get(key);
58835
59712
  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
59713
  pipeline = this.device.createRenderPipeline(
58849
- this._descriptor(key, shaderKey, blend, pma, stencil, primitive, {
58850
- vertexLayout,
59714
+ this._descriptor(
59715
+ key,
59716
+ shaderKey,
59717
+ topology,
59718
+ blendMode,
59719
+ premultipliedAlpha,
59720
+ stencilMode,
58851
59721
  meshState,
58852
- fogFlag
58853
- })
59722
+ this.format,
59723
+ this.sampleCount
59724
+ )
58854
59725
  );
58855
59726
  this.pipelines.set(key, pipeline);
59727
+ if (this.inFrame === true) {
59728
+ this.inFrameMisses++;
59729
+ this.inFrameMissKeys.push(key);
59730
+ if (this.inFrameMissKeys.length > MISS_KEYS_KEPT) {
59731
+ this.inFrameMissKeys.shift();
59732
+ }
59733
+ }
58856
59734
  }
58857
59735
  return pipeline;
58858
59736
  }
59737
+ /**
59738
+ * Build a pipeline ahead of the draw that will need it, without blocking.
59739
+ *
59740
+ * Takes the same tuple as {@link WebGPUPipelineCache#get}, plus the colour
59741
+ * format and sample count of the target the draw will land on, since a
59742
+ * warm-up runs outside any pass. Compiles through
59743
+ * `createRenderPipelineAsync` where the device has it, so the work
59744
+ * happens off the frame, and files the result under the same key `get`
59745
+ * will look up. Asking twice for one pipeline compiles it once.
59746
+ *
59747
+ * Never rejects. A pipeline that fails to build is left for the draw to
59748
+ * build and report, exactly as it would have without the warm-up.
59749
+ * @param {string} shaderKey - the family, already registered
59750
+ * @param {string} topology - portable topology name
59751
+ * @param {string} blendMode - blend mode
59752
+ * @param {boolean} premultipliedAlpha - source premultiplication flag
59753
+ * @param {string} stencilMode - stencil mode
59754
+ * @param {object|undefined} meshState - mesh pass state, read NOW: a later
59755
+ * change to the object does not reach the pipeline being built
59756
+ * @param {string} format - colour target format the draw will use
59757
+ * @param {number} sampleCount - sample count the draw will use
59758
+ * @returns {Promise<void>} settles once the pipeline is in the cache, or
59759
+ * has failed to get there
59760
+ */
59761
+ warm(shaderKey, topology, blendMode, premultipliedAlpha, stencilMode, meshState, format, sampleCount) {
59762
+ const key = this.keyFor(
59763
+ shaderKey,
59764
+ topology,
59765
+ blendMode,
59766
+ premultipliedAlpha,
59767
+ stencilMode,
59768
+ meshState,
59769
+ format,
59770
+ sampleCount
59771
+ );
59772
+ if (this.pipelines.has(key)) {
59773
+ return Promise.resolve();
59774
+ }
59775
+ const inFlight = this.pending.get(key);
59776
+ if (inFlight !== void 0) {
59777
+ this.collecting?.push(inFlight);
59778
+ return inFlight;
59779
+ }
59780
+ const descriptor = this._descriptor(
59781
+ key,
59782
+ shaderKey,
59783
+ topology,
59784
+ blendMode,
59785
+ premultipliedAlpha,
59786
+ stencilMode,
59787
+ meshState,
59788
+ format,
59789
+ sampleCount
59790
+ );
59791
+ const generation = this.generation;
59792
+ const promise = new Promise((resolve) => {
59793
+ this.warmQueue.push({
59794
+ run: () => {
59795
+ if (this.pipelines.has(key)) {
59796
+ return void 0;
59797
+ }
59798
+ return typeof this.device.createRenderPipelineAsync === "function" ? this.device.createRenderPipelineAsync(descriptor) : this.device.createRenderPipeline(descriptor);
59799
+ },
59800
+ resolve: (pipeline) => {
59801
+ this.pending.delete(key);
59802
+ if (pipeline !== void 0 && generation === this.generation && !this.pipelines.has(key)) {
59803
+ this.pipelines.set(key, pipeline);
59804
+ this._touch(pipeline, format, sampleCount);
59805
+ }
59806
+ resolve();
59807
+ }
59808
+ });
59809
+ });
59810
+ this.pending.set(key, promise);
59811
+ this.collecting?.push(promise);
59812
+ this._pumpWarmQueue();
59813
+ return promise;
59814
+ }
59815
+ /**
59816
+ * Run `ask` and hand back the warm-ups it asked for that are not built
59817
+ * yet: the ones it started, and the ones already compiling that it asked
59818
+ * for again. A pipeline already in the cache is not waited on, so a scene
59819
+ * entered a second time hands back nothing.
59820
+ * @param {Function} ask - asks for warm-ups, through {@link WebGPUPipelineCache#warm}
59821
+ * @returns {Promise<void>[]} one promise per pipeline still on its way
59822
+ */
59823
+ collect(ask) {
59824
+ const outer = this.collecting;
59825
+ const asked = [];
59826
+ this.collecting = asked;
59827
+ try {
59828
+ ask();
59829
+ } finally {
59830
+ this.collecting = outer;
59831
+ }
59832
+ return asked;
59833
+ }
59834
+ /**
59835
+ * Start queued warm-ups until {@link MAX_WARM_IN_FLIGHT} are compiling.
59836
+ * @ignore
59837
+ * @internal
59838
+ */
59839
+ _pumpWarmQueue() {
59840
+ while (this.warmInFlight < MAX_WARM_IN_FLIGHT && this.warmQueue.length > 0) {
59841
+ const job = this.warmQueue.shift();
59842
+ this.warmInFlight++;
59843
+ Promise.resolve().then(job.run).then(job.resolve, () => {
59844
+ job.resolve(void 0);
59845
+ }).finally(() => {
59846
+ this.warmInFlight--;
59847
+ this._pumpWarmQueue();
59848
+ });
59849
+ }
59850
+ }
59851
+ /**
59852
+ * Bind a freshly warmed pipeline once, in a pass of its own, so it is
59853
+ * compiled now rather than at the first draw that uses it.
59854
+ *
59855
+ * Some implementations accept `createRenderPipelineAsync` and still defer
59856
+ * the native compile to the first `setPipeline`, which puts it right back
59857
+ * in the frame the warm-up was meant to keep it out of. Setting it on a
59858
+ * throwaway 1×1 pass makes that first use happen here, while nothing is
59859
+ * on screen waiting for it. Where the compile already happened this is
59860
+ * one cheap bind.
59861
+ * @param {GPURenderPipeline} pipeline - the pipeline to bind
59862
+ * @param {string} format - its colour format
59863
+ * @param {number} sampleCount - its sample count
59864
+ * @ignore
59865
+ * @internal
59866
+ */
59867
+ _touch(pipeline, format, sampleCount) {
59868
+ const target = `${format}|${sampleCount}`;
59869
+ let batch = this.touchQueue.get(target);
59870
+ if (batch === void 0) {
59871
+ batch = [];
59872
+ this.touchQueue.set(target, batch);
59873
+ }
59874
+ batch.push(pipeline);
59875
+ if (this.touchQueue.size === 1 && batch.length === 1) {
59876
+ queueMicrotask(() => {
59877
+ this._flushTouches();
59878
+ });
59879
+ }
59880
+ }
59881
+ /**
59882
+ * Record and submit the passes queued by {@link WebGPUPipelineCache#_touch}.
59883
+ * @ignore
59884
+ * @internal
59885
+ */
59886
+ _flushTouches() {
59887
+ const device = this.device;
59888
+ const batches = [...this.touchQueue];
59889
+ this.touchQueue.clear();
59890
+ if (typeof device.createCommandEncoder !== "function") {
59891
+ return;
59892
+ }
59893
+ try {
59894
+ const encoder = device.createCommandEncoder({
59895
+ label: "melonJS pipeline warm-up"
59896
+ });
59897
+ for (const [target, pipelines] of batches) {
59898
+ const [format, samples] = target.split("|");
59899
+ const scratch = this._scratchTarget(format, Number(samples));
59900
+ const pass = encoder.beginRenderPass({
59901
+ label: "melonJS pipeline warm-up",
59902
+ colorAttachments: [
59903
+ { view: scratch.colorView, loadOp: "clear", storeOp: "discard" }
59904
+ ],
59905
+ depthStencilAttachment: {
59906
+ view: scratch.depthView,
59907
+ depthClearValue: 1,
59908
+ depthLoadOp: "clear",
59909
+ depthStoreOp: "discard",
59910
+ stencilLoadOp: "clear",
59911
+ stencilStoreOp: "discard"
59912
+ }
59913
+ });
59914
+ for (const pipeline of pipelines) {
59915
+ pass.setPipeline(pipeline);
59916
+ }
59917
+ pass.end();
59918
+ }
59919
+ device.queue.submit([encoder.finish()]);
59920
+ } catch {
59921
+ }
59922
+ }
59923
+ /**
59924
+ * The 1×1 colour + depth-stencil pair a warm-up pass binds for one
59925
+ * target shape, built once per shape.
59926
+ * @param {string} format - colour format
59927
+ * @param {number} sampleCount - sample count
59928
+ * @returns {{colorView: GPUTextureView, depthView: GPUTextureView}} the views
59929
+ * @ignore
59930
+ * @internal
59931
+ */
59932
+ _scratchTarget(format, sampleCount) {
59933
+ const key = `${format}|${sampleCount}`;
59934
+ let scratch = this.scratchTargets.get(key);
59935
+ if (scratch === void 0) {
59936
+ const color = this.device.createTexture({
59937
+ label: "melonJS warm-up target",
59938
+ size: [1, 1],
59939
+ format,
59940
+ sampleCount,
59941
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
59942
+ });
59943
+ const depth = this.device.createTexture({
59944
+ label: "melonJS warm-up depth",
59945
+ size: [1, 1],
59946
+ format: DEPTH_STENCIL_FORMAT,
59947
+ sampleCount,
59948
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
59949
+ });
59950
+ scratch = {
59951
+ color,
59952
+ depth,
59953
+ colorView: color.createView(),
59954
+ depthView: depth.createView()
59955
+ };
59956
+ this.scratchTargets.set(key, scratch);
59957
+ }
59958
+ return scratch;
59959
+ }
58859
59960
  /**
58860
59961
  * Assemble a pipeline descriptor.
58861
59962
  *
@@ -58866,17 +59967,33 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58866
59967
  * silently builds a second one under a key nothing will hit again.
58867
59968
  * @param {string} key - the cache key, used as the label
58868
59969
  * @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}`
59970
+ * @param {string} topology - portable topology name
59971
+ * @param {string} blendMode - blend mode (normalized here)
59972
+ * @param {boolean} premultipliedAlpha - source premultiplication flag
59973
+ * @param {string} stencilMode - stencil mode
59974
+ * @param {object|undefined} meshState - mesh pass state
59975
+ * @param {string} format - colour target format
59976
+ * @param {number} sampleCount - pass sample count
58874
59977
  * @returns {GPURenderPipelineDescriptor} the descriptor
58875
59978
  * @ignore
58876
59979
  * @internal
58877
59980
  */
58878
- _descriptor(key, shaderKey, blend, pma, stencil, primitive, opts) {
58879
- const { vertexLayout, meshState, fogFlag } = opts;
59981
+ _descriptor(key, shaderKey, topology, blendMode, premultipliedAlpha, stencilMode, meshState, format, sampleCount) {
59982
+ const blend = normalizeBlendMode(blendMode);
59983
+ const pma = premultipliedAlpha !== false;
59984
+ const stencil = STENCIL_STATES[stencilMode] ?? STENCIL_STATES.none;
59985
+ const vertexLayout = this.vertexLayouts.get(
59986
+ this.vertexLayoutAliases.get(shaderKey) ?? shaderKey
59987
+ );
59988
+ const primitive = {
59989
+ topology,
59990
+ stripIndexFormat: topology.endsWith("-strip") ? "uint32" : void 0
59991
+ };
59992
+ if (meshState) {
59993
+ primitive.cullMode = meshState.cullMode;
59994
+ primitive.frontFace = meshState.frontFace;
59995
+ }
59996
+ const fogFlag = meshState?.fog === true ? 1 : 0;
58880
59997
  return {
58881
59998
  label: `melonJS ${key}`,
58882
59999
  layout: this.pipelineLayouts[shaderKey],
@@ -58892,7 +60009,7 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58892
60009
  ...meshState ? { constants: { enable_fog: fogFlag } } : {},
58893
60010
  targets: [
58894
60011
  {
58895
- format: this.format,
60012
+ format,
58896
60013
  blend: shaderKey === "clear" ? void 0 : gpuBlendState(blend, pma),
58897
60014
  writeMask: stencil.colorWriteMask
58898
60015
  }
@@ -58915,7 +60032,7 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58915
60032
  stencilReadMask: stencil.readMask,
58916
60033
  stencilWriteMask: stencil.writeMask
58917
60034
  },
58918
- multisample: { count: this.sampleCount }
60035
+ multisample: { count: sampleCount }
58919
60036
  };
58920
60037
  }
58921
60038
  /**
@@ -58924,6 +60041,17 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
58924
60041
  */
58925
60042
  clear() {
58926
60043
  this.pipelines.clear();
60044
+ this.generation++;
60045
+ for (const job of this.warmQueue.splice(0)) {
60046
+ job.resolve(void 0);
60047
+ }
60048
+ this.pending.clear();
60049
+ this.touchQueue.clear();
60050
+ for (const scratch of this.scratchTargets.values()) {
60051
+ scratch.color.destroy?.();
60052
+ scratch.depth.destroy?.();
60053
+ }
60054
+ this.scratchTargets.clear();
58927
60055
  }
58928
60056
  };
58929
60057
 
@@ -59049,7 +60177,13 @@ var WebGPUQuadBatcher = class extends WebGPUBatcher {
59049
60177
  Math.min(u0, u1),
59050
60178
  Math.min(v0, v1)
59051
60179
  );
59052
- this.pushQuadVertices(x, y, w, h, u0, v0, u1, v1, tint, 0);
60180
+ const scale2 = effect._vertexScale;
60181
+ if (scale2 !== void 0 && (scale2.x !== 1 || scale2.y !== 1)) {
60182
+ const q = scaleQuad(x, y, w, h, scale2.x, scale2.y);
60183
+ this.pushQuadVertices(q[0], q[1], q[2], q[3], u0, v0, u1, v1, tint, 0);
60184
+ } else {
60185
+ this.pushQuadVertices(x, y, w, h, u0, v0, u1, v1, tint, 0);
60186
+ }
59053
60187
  this.flush();
59054
60188
  return;
59055
60189
  }
@@ -60708,7 +61842,11 @@ function uploadCompressedTexture(device, texture, image, metrics) {
60708
61842
  bytesPerRow: blocksPerRow * metrics.bytes,
60709
61843
  rowsPerImage: rows
60710
61844
  },
60711
- [mip.width, mip.height]
61845
+ // whole blocks: a level smaller than one block (the 2×2 and 1×1
61846
+ // tail of a 4×4-block chain) is stored padded to a full block,
61847
+ // and a copy of its logical size fails validation, leaving the
61848
+ // level empty
61849
+ [blocksPerRow * metrics.blockW, rows * metrics.blockH]
60712
61850
  );
60713
61851
  }
60714
61852
  }
@@ -60978,6 +62116,71 @@ var WebGPUTextureStore = class extends TextureStore {
60978
62116
  this.lastRecord = record;
60979
62117
  return this.bindGroupFor(record, texture, wrap, options.mipmaps === true);
60980
62118
  }
62119
+ /**
62120
+ * Build the mip-chain blitter, once. Split out of
62121
+ * {@link WebGPUTextureStore#generateMipmaps} so the warm-up can build it
62122
+ * before the first mipmapped texture is uploaded, which is otherwise in
62123
+ * the middle of the frame that first draws a mesh.
62124
+ * @ignore
62125
+ * @internal
62126
+ */
62127
+ ensureMipPipeline() {
62128
+ if (typeof this.mipPipeline !== "undefined") {
62129
+ return;
62130
+ }
62131
+ const device = this.device;
62132
+ const module = device.createShaderModule({
62133
+ label: "melonJS mip blit",
62134
+ code: mipblit_default
62135
+ });
62136
+ this.mipLayout = device.createBindGroupLayout({
62137
+ label: "melonJS mip blit layout",
62138
+ entries: [
62139
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
62140
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
62141
+ ]
62142
+ });
62143
+ this.mipPipeline = device.createRenderPipeline({
62144
+ label: "melonJS mip blit",
62145
+ layout: device.createPipelineLayout({
62146
+ bindGroupLayouts: [this.mipLayout]
62147
+ }),
62148
+ vertex: { module, entryPoint: "vertex_main" },
62149
+ fragment: {
62150
+ module,
62151
+ entryPoint: "fragment_main",
62152
+ targets: [{ format: "rgba8unorm" }]
62153
+ },
62154
+ primitive: { topology: "triangle-list" }
62155
+ });
62156
+ this.mipSampler = device.createSampler({
62157
+ label: "melonJS mip blit sampler",
62158
+ magFilter: "linear",
62159
+ minFilter: "linear"
62160
+ });
62161
+ }
62162
+ /**
62163
+ * Build the mip-chain blitter and run it once, on a throwaway 2×2
62164
+ * texture, so its native compile has happened before the first real
62165
+ * texture needs it. Creating a pipeline is not enough on every
62166
+ * implementation: some defer the compile to first use, so this uses it.
62167
+ * @ignore
62168
+ * @internal
62169
+ */
62170
+ warmMipmaps() {
62171
+ if (typeof this.mipPipeline !== "undefined") {
62172
+ return;
62173
+ }
62174
+ const texture = this.device.createTexture({
62175
+ label: "melonJS mip warm-up",
62176
+ size: [2, 2],
62177
+ format: "rgba8unorm",
62178
+ mipLevelCount: 2,
62179
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
62180
+ });
62181
+ this.generateMipmaps({ texture, mipLevelCount: 2 });
62182
+ texture.destroy();
62183
+ }
60981
62184
  /**
60982
62185
  * Build the full mip chain of a freshly-uploaded record: one blit pass
60983
62186
  * per level, each rendering the previous level with linear filtering.
@@ -60990,37 +62193,7 @@ var WebGPUTextureStore = class extends TextureStore {
60990
62193
  */
60991
62194
  generateMipmaps(record) {
60992
62195
  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
- }
62196
+ this.ensureMipPipeline();
61024
62197
  const encoder = device.createCommandEncoder({ label: "melonJS mipgen" });
61025
62198
  for (let level2 = 1; level2 < record.mipLevelCount; level2++) {
61026
62199
  const bindGroup = device.createBindGroup({
@@ -61541,6 +62714,7 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
61541
62714
  this.uniformRing.reset();
61542
62715
  this.effectUniformArena.reset();
61543
62716
  this.frameId++;
62717
+ this.pipelineCache.inFrame = true;
61544
62718
  this.currentRenderTarget = null;
61545
62719
  this.pendingColorClear = false;
61546
62720
  this.effectPassDepth = 0;
@@ -61935,6 +63109,448 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
61935
63109
  );
61936
63110
  return frame;
61937
63111
  }
63112
+ /**
63113
+ * The post-effect render-target pool, created on first use.
63114
+ * @returns {RenderTargetPool} the pool
63115
+ * @ignore
63116
+ * @internal
63117
+ */
63118
+ _postEffectPool() {
63119
+ this._renderTargetPool ??= new RenderTargetPool(
63120
+ (w, h, isCapture, index) => {
63121
+ return new WebGPURenderTarget(this, w, h, {
63122
+ sampleCount: isCapture === true ? this.canvasSampleCount : 1,
63123
+ format: index < 2 ? this._colorFormat : this.preferredFormat
63124
+ });
63125
+ }
63126
+ );
63127
+ return this._renderTargetPool;
63128
+ }
63129
+ /**
63130
+ * The pipeline-cache family of an effect's WGSL realization, building its
63131
+ * module and layouts first if this device has not seen it yet.
63132
+ * @param {ShaderEffect} effect - an effect with a valid WGSL realization
63133
+ * @returns {string} the family key
63134
+ * @ignore
63135
+ * @internal
63136
+ */
63137
+ _effectFamily(effect) {
63138
+ const realization = effect.wgslRealization;
63139
+ if (realization.gpu === null || realization.gpu.epoch !== this.pipelineCache.epoch) {
63140
+ buildEffectGPU(this, effect, realization);
63141
+ }
63142
+ return realization.gpu.key;
63143
+ }
63144
+ /**
63145
+ * Run `warm` and hand back the pipelines it asked for that are still
63146
+ * compiling, which on this backend is every one not already cached.
63147
+ * @param {Function} warm - issues the warm-ups
63148
+ * @returns {Promise<void>[]} one promise per pipeline on its way
63149
+ * @override
63150
+ * @ignore
63151
+ * @internal
63152
+ */
63153
+ _collectWarms(warm) {
63154
+ if (this.pipelineCache == null) {
63155
+ return super._collectWarms(warm);
63156
+ }
63157
+ return this.pipelineCache.collect(warm);
63158
+ }
63159
+ /**
63160
+ * Build, off the frame, every pipeline a renderable's post effects can
63161
+ * draw with, and the render targets its chain will composite through.
63162
+ *
63163
+ * Which pipeline an effect draws with depends on where it sits: an
63164
+ * effect alone on a sprite draws the sprite itself (blended with the
63165
+ * sprite's own mode), one alone on a mesh is spliced into the mesh
63166
+ * shader, and one in a chain is a full-screen blit into either the next
63167
+ * offscreen target or the one below. Its colour format depends on that
63168
+ * target too, and under `hdr` the camera's targets are half-float while
63169
+ * the canvas is not. So this warms every position the effect can reach
63170
+ * with the effects ATTACHED, enabled or not, since turning one on moves
63171
+ * the others: enabling a camera's second effect sends its first one
63172
+ * from the canvas into the ping-pong target, a different pipeline.
63173
+ *
63174
+ * Each key is composed by the same function the draw uses, from the
63175
+ * renderable's own state (blend mode, culling, transparency, fog), so
63176
+ * what is built here is what the draw will ask for.
63177
+ * @param {Renderable} renderable - the renderable to warm for
63178
+ * @override
63179
+ * @ignore
63180
+ * @internal
63181
+ */
63182
+ _warmPostEffects(renderable) {
63183
+ if (!this._canWarm()) {
63184
+ return;
63185
+ }
63186
+ const cache3 = this.pipelineCache;
63187
+ const effects = renderable.postEffects.filter((fx) => {
63188
+ return fx?.wgslRealization?.valid === true;
63189
+ });
63190
+ const count = effects.length;
63191
+ if (count === 0) {
63192
+ return;
63193
+ }
63194
+ const isCamera = renderable._postEffectManaged === true;
63195
+ const needsCapture = renderable.postEffectNeedsCapture === true;
63196
+ const pooled = isCamera || count > 1 || needsCapture;
63197
+ const canvas = [this.canvasFormat, this.canvasSampleCount];
63198
+ const scene = this._sceneTargets();
63199
+ const blit = (family, blendMode2, [format, sampleCount]) => {
63200
+ void cache3.warm(
63201
+ family,
63202
+ "triangle-list",
63203
+ blendMode2,
63204
+ true,
63205
+ "none",
63206
+ void 0,
63207
+ format,
63208
+ sampleCount
63209
+ );
63210
+ };
63211
+ if (pooled) {
63212
+ const pool2 = this._postEffectPool();
63213
+ const { width, height } = this.getCanvas();
63214
+ pool2.reserve(
63215
+ isCamera,
63216
+ this._postEffectCapacity(renderable, effects),
63217
+ width,
63218
+ height
63219
+ );
63220
+ }
63221
+ if (isCamera) {
63222
+ for (const fx of effects) {
63223
+ const family = this._effectFamily(fx);
63224
+ blit(family, "none", [this._colorFormat, 1]);
63225
+ blit(family, "none", canvas);
63226
+ }
63227
+ this._warmToneMap();
63228
+ return;
63229
+ }
63230
+ const blendMode = renderable.blendMode ?? "normal";
63231
+ const meshHost = this._hostsMeshEffect(renderable);
63232
+ for (let i = 0; i < count; i++) {
63233
+ const fx = effects[i];
63234
+ const family = this._effectFamily(fx);
63235
+ if (!needsCapture) {
63236
+ if (meshHost) {
63237
+ this._warmHostedEffect(renderable, fx, scene);
63238
+ } else {
63239
+ for (const target of scene) {
63240
+ blit(family, blendMode, target);
63241
+ }
63242
+ }
63243
+ }
63244
+ if (pooled) {
63245
+ for (const target of scene) {
63246
+ blit(family, blendMode, target);
63247
+ }
63248
+ if (i < count - 1) {
63249
+ blit(family, "none", [this.preferredFormat, 1]);
63250
+ }
63251
+ }
63252
+ }
63253
+ if (pooled && !meshHost) {
63254
+ blit("quad", blendMode, [this.preferredFormat, this.canvasSampleCount]);
63255
+ }
63256
+ }
63257
+ /**
63258
+ * The `[format, sampleCount]` targets an ordinary renderable can be drawn
63259
+ * into: the canvas, or the camera's capture target when the camera runs
63260
+ * an effect chain of its own. The two differ only in format, and only
63261
+ * under `hdr`, so this is usually one target.
63262
+ * @returns {Array<Array<string|number>>} the targets
63263
+ * @ignore
63264
+ * @internal
63265
+ */
63266
+ _sceneTargets() {
63267
+ const canvas = [this.canvasFormat, this.canvasSampleCount];
63268
+ return this._colorFormat === this.canvasFormat ? [canvas] : [canvas, [this._colorFormat, this.canvasSampleCount]];
63269
+ }
63270
+ /**
63271
+ * Whether the warm-ups can run: a live device and a pipeline cache.
63272
+ * @returns {boolean} true when ready
63273
+ * @ignore
63274
+ * @internal
63275
+ */
63276
+ _canWarm() {
63277
+ return this.pipelineCache != null && typeof this.device !== "undefined" && this.isContextValid !== false;
63278
+ }
63279
+ /**
63280
+ * The mesh batcher a mesh draws through.
63281
+ * @param {object} mesh - the mesh
63282
+ * @returns {object|undefined} the batcher
63283
+ * @ignore
63284
+ * @internal
63285
+ */
63286
+ _meshBatcherFor(mesh) {
63287
+ return this.batchers.get(
63288
+ mesh.lit === true && this.batchers.has("litMesh") ? "litMesh" : "mesh"
63289
+ );
63290
+ }
63291
+ /**
63292
+ * Warm the pipelines a mesh will draw with when it hosts an effect: the
63293
+ * effect spliced into the mesh shader.
63294
+ * @param {object} mesh - the hosting mesh
63295
+ * @param {ShaderEffect} effect - the effect it hosts
63296
+ * @param {Array<Array<string|number>>} scene - the targets it can land on
63297
+ * @ignore
63298
+ * @internal
63299
+ */
63300
+ _warmHostedEffect(mesh, effect, scene) {
63301
+ const realization = effect.wgslRealization;
63302
+ if (meshHostingBlocker(realization.builtins, realization.textures, "wgsl") !== null) {
63303
+ return;
63304
+ }
63305
+ const batcher = this._meshBatcherFor(mesh);
63306
+ if (batcher === void 0 || realization.meshModule(batcher.shaderSource?.() ?? "") === null) {
63307
+ return;
63308
+ }
63309
+ const previous = batcher.hostedEffect;
63310
+ let family;
63311
+ batcher.hostedEffect = effect;
63312
+ try {
63313
+ family = batcher.activeShaderKey();
63314
+ } finally {
63315
+ batcher.hostedEffect = previous;
63316
+ }
63317
+ this._warmMeshDraw(mesh, family, scene);
63318
+ }
63319
+ /**
63320
+ * Warm the pipelines one mesh draw resolves to, keyed on the mesh's own
63321
+ * culling, depth test and transparency exactly as `drawMesh` keys them:
63322
+ * the opaque pass, the transparent pass, or both when `transparent` is
63323
+ * left unset (blended whenever the mesh is faded).
63324
+ * @param {object} mesh - the mesh
63325
+ * @param {string} family - the pipeline family it draws with
63326
+ * @param {Array<Array<string|number>>} scene - the targets it can land on
63327
+ * @ignore
63328
+ * @internal
63329
+ */
63330
+ _warmMeshDraw(mesh, family, scene) {
63331
+ for (const fog of this._warmFog ?? [this._fog3d != null]) {
63332
+ this._warmMeshDrawFog(mesh, family, scene, fog);
63333
+ }
63334
+ }
63335
+ /**
63336
+ * {@link WebGPURenderer#_warmMeshDraw} for one fog state.
63337
+ * @param {object} mesh - the mesh
63338
+ * @param {string} family - the pipeline family it draws with
63339
+ * @param {Array<Array<string|number>>} scene - the targets it can land on
63340
+ * @param {boolean} fog - whether it draws fogged
63341
+ * @ignore
63342
+ * @internal
63343
+ */
63344
+ _warmMeshDrawFog(mesh, family, scene, fog) {
63345
+ const culling = mesh.cullBackFaces === true;
63346
+ const state4 = {
63347
+ cullMode: culling ? "back" : "none",
63348
+ // the retained path's winding rule, see `drawMesh`
63349
+ frontFace: culling && this.supportsRetainedMesh === true && mesh.rightHanded !== true ? "cw" : "ccw",
63350
+ depthWrite: void 0,
63351
+ depthTest: mesh.depthTest === false ? false : void 0,
63352
+ // `undefined` rather than `false`, as the draw sets it: the key only
63353
+ // grows a fog suffix for `true`
63354
+ fog: fog ? true : void 0
63355
+ };
63356
+ const blendedOnly = mesh.transparent === true || mesh._blendedDraw === true;
63357
+ const blended = mesh.transparent !== false || mesh._blendedDraw === true;
63358
+ for (const [format, sampleCount] of scene) {
63359
+ if (!blendedOnly) {
63360
+ void this.pipelineCache.warm(
63361
+ family,
63362
+ "triangle-list",
63363
+ "none",
63364
+ true,
63365
+ "none",
63366
+ state4,
63367
+ format,
63368
+ sampleCount
63369
+ );
63370
+ }
63371
+ if (blended) {
63372
+ void this.pipelineCache.warm(
63373
+ family,
63374
+ "triangle-list",
63375
+ mesh._blendedDraw === true ? "normal" : mesh.blendMode,
63376
+ true,
63377
+ "none",
63378
+ { ...state4, depthWrite: false },
63379
+ format,
63380
+ sampleCount
63381
+ );
63382
+ }
63383
+ }
63384
+ }
63385
+ /**
63386
+ * Warm the renderable's own draw: a mesh through its mesh family, and
63387
+ * everything else that draws a texture through the quad family, each with
63388
+ * the renderable's blend mode. Containers draw nothing of their own, and
63389
+ * a camera is a chain, warmed with its effects.
63390
+ * @param {Renderable} renderable - the renderable to warm for
63391
+ * @override
63392
+ * @ignore
63393
+ * @internal
63394
+ */
63395
+ _warmDraw(renderable) {
63396
+ if (!this._canWarm() || renderable._postEffectManaged === true || Array.isArray(renderable.children)) {
63397
+ return;
63398
+ }
63399
+ const scene = this._sceneTargets();
63400
+ if (this._hostsMeshEffect(renderable)) {
63401
+ const batcher = this._meshBatcherFor(renderable);
63402
+ if (batcher?.shaderKey !== void 0) {
63403
+ this._warmMeshDraw(renderable, batcher.shaderKey, scene);
63404
+ }
63405
+ return;
63406
+ }
63407
+ if (renderable.instanceLayout !== void 0) {
63408
+ this._warmInstancedDraw(renderable, scene);
63409
+ return;
63410
+ }
63411
+ const blendMode = renderable.blendMode ?? "normal";
63412
+ for (const [format, sampleCount] of scene) {
63413
+ void this.pipelineCache.warm(
63414
+ "quad",
63415
+ "triangle-list",
63416
+ blendMode,
63417
+ true,
63418
+ "none",
63419
+ void 0,
63420
+ format,
63421
+ sampleCount
63422
+ );
63423
+ }
63424
+ }
63425
+ /**
63426
+ * Warm what an `InstancedMesh` draws with: its instanced family, and the
63427
+ * instanced ground-shadow family when it casts one.
63428
+ *
63429
+ * Both families are registered per instance record layout, on first use,
63430
+ * which the warm-up used to leave to the first draw. Nothing about them
63431
+ * needs the draw, though: the layout is the mesh's own, so registering
63432
+ * them here builds the same module and vertex layout the draw would, and
63433
+ * the keys follow the same rules as an ordinary mesh's. A scatter of
63434
+ * props is one of these, and it is exactly the kind of thing a stage
63435
+ * builds when it starts and draws on its next frame.
63436
+ * @param {object} mesh - the instanced mesh
63437
+ * @param {Array<Array<string|number>>} scene - the targets it can land on
63438
+ * @ignore
63439
+ * @internal
63440
+ */
63441
+ _warmInstancedDraw(mesh, scene) {
63442
+ if (this.supportsInstancing !== true) {
63443
+ return;
63444
+ }
63445
+ const batcher = this._meshBatcherFor(mesh);
63446
+ if (typeof batcher?.instancedFamilyFor !== "function") {
63447
+ return;
63448
+ }
63449
+ this._warmMeshDraw(
63450
+ mesh,
63451
+ batcher.instancedFamilyFor(mesh.instanceLayout),
63452
+ scene
63453
+ );
63454
+ if (mesh._castsGroundShadow?.(this) !== true || typeof batcher.instancedShadowFamily !== "function") {
63455
+ return;
63456
+ }
63457
+ const family = batcher.instancedShadowFamily(mesh.instanceLayout);
63458
+ for (const fog of this._warmFog ?? [this._fog3d != null]) {
63459
+ const state4 = {
63460
+ cullMode: "none",
63461
+ frontFace: "ccw",
63462
+ depthWrite: false,
63463
+ depthTest: mesh.depthTest === false ? false : void 0,
63464
+ fog: fog ? true : void 0
63465
+ };
63466
+ for (const [format, sampleCount] of scene) {
63467
+ void this.pipelineCache.warm(
63468
+ family,
63469
+ "triangle-list",
63470
+ "normal",
63471
+ true,
63472
+ "none",
63473
+ state4,
63474
+ format,
63475
+ sampleCount
63476
+ );
63477
+ }
63478
+ }
63479
+ }
63480
+ /**
63481
+ * Build the advanced blend shader and its offscreen target before the
63482
+ * first draw in one of those modes, and warm the two pipelines a bracket
63483
+ * uses: the draw itself into the 8-bit offscreen, and the composite back
63484
+ * onto the scene.
63485
+ * @override
63486
+ * @ignore
63487
+ * @internal
63488
+ */
63489
+ _warmAdvancedBlend() {
63490
+ if (!this._canWarm()) {
63491
+ return;
63492
+ }
63493
+ if (typeof this._advancedBlendEffect === "undefined") {
63494
+ this._advancedBlendEffect = new BlendEffect(this);
63495
+ }
63496
+ const effect = this._advancedBlendEffect;
63497
+ if (effect.enabled !== true || effect.wgslRealization?.valid !== true) {
63498
+ return;
63499
+ }
63500
+ const { width, height } = this.getCanvas();
63501
+ if (typeof this._advancedBlendTarget === "undefined") {
63502
+ this._advancedBlendTarget = new WebGPURenderTarget(this, width, height, {
63503
+ sampleCount: 1,
63504
+ format: this.preferredFormat
63505
+ });
63506
+ }
63507
+ void this.pipelineCache.warm(
63508
+ "quad",
63509
+ "triangle-list",
63510
+ "normal",
63511
+ true,
63512
+ "none",
63513
+ void 0,
63514
+ this.preferredFormat,
63515
+ 1
63516
+ );
63517
+ const family = this._effectFamily(effect);
63518
+ for (const [format, sampleCount] of this._sceneTargets()) {
63519
+ void this.pipelineCache.warm(
63520
+ family,
63521
+ "triangle-list",
63522
+ "normal",
63523
+ true,
63524
+ "none",
63525
+ void 0,
63526
+ format,
63527
+ sampleCount
63528
+ );
63529
+ }
63530
+ }
63531
+ /**
63532
+ * Warm the tone curve's pipeline: always the last step of a camera chain,
63533
+ * so a full-screen blit onto the canvas with blending off.
63534
+ * @override
63535
+ * @ignore
63536
+ * @internal
63537
+ */
63538
+ _warmToneMap() {
63539
+ const toneMap = this._toneMapEffect;
63540
+ if (this.settings?.prewarm !== true || toneMap?.wgslRealization?.valid !== true || !this._canWarm()) {
63541
+ return;
63542
+ }
63543
+ void this.pipelineCache.warm(
63544
+ this._effectFamily(toneMap),
63545
+ "triangle-list",
63546
+ "none",
63547
+ true,
63548
+ "none",
63549
+ void 0,
63550
+ this.canvasFormat,
63551
+ this.canvasSampleCount
63552
+ );
63553
+ }
61938
63554
  /**
61939
63555
  * Begin a post-effect pass for the given renderable — the WebGPU
61940
63556
  * realization of the WebGL FBO path: the renderable's whole content
@@ -61967,17 +63583,9 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
61967
63583
  }
61968
63584
  savedProjection.copy(this.projectionMatrix);
61969
63585
  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(
63586
+ const rt = this._postEffectPool().begin(
61979
63587
  isCamera,
61980
- effects.length,
63588
+ this._postEffectCapacity(renderable, effects),
61981
63589
  canvas.width,
61982
63590
  canvas.height
61983
63591
  );
@@ -62222,23 +63830,24 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62222
63830
  * WebGPU splits what WebGL fuses into one program. The **modules** for
62223
63831
  * the engine's own families — quad, blit, primitive, clear, and the mesh
62224
63832
  * 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
63833
+ * are constructed. A **pipeline** is where the real compile happens, and
63834
+ * it is the expensive part: tens of milliseconds each, paid in the frame
63835
+ * that first draws with it. But it is keyed by draw state (blend, cull,
63836
+ * depth write) and by the format of the target being drawn into, as well
63837
+ * as by shader, so the set a scene will ask for cannot be known from
63838
+ * here. Those are warmed when an effect is attached to a renderable
63839
+ * ({@link Renderable#addPostEffect}), which is the first moment the
63840
+ * renderable that decides them is known.
63841
+ *
63842
+ * What this does warm is what needs no renderable. A `ShaderEffect`'s
63843
+ * module: `prepareEffectBinding` builds it, and only ever from inside the
63844
+ * draw path. Every effect the loader compiled from a `{type: "shader"}`
63845
+ * asset is already in `shaderList`, so those can be brought forward —
63846
+ * which is what makes declaring a level's effects alongside that level's
63847
+ * assets worth doing. And the mip-chain blitter every mipmapped texture
63848
+ * goes through on upload, when textures are filtered (`textureFilter`
63849
+ * resolving to `"linear"`; nearest filtering never builds a chain).
63850
+ * @returns {Promise<void>} settles once those are built
62242
63851
  * @see {@link Renderer#prewarm}
62243
63852
  */
62244
63853
  prewarm() {
@@ -62252,6 +63861,28 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62252
63861
  buildEffectGPU(this, effect, realization);
62253
63862
  }
62254
63863
  }
63864
+ if (this.getDefaultTextureFilter() === "linear") {
63865
+ this.textureStore.warmMipmaps();
63866
+ }
63867
+ const warms = [];
63868
+ if (this._canWarm()) {
63869
+ for (const [format, sampleCount] of this._sceneTargets()) {
63870
+ warms.push(
63871
+ this.pipelineCache.warm(
63872
+ "clear",
63873
+ "triangle-list",
63874
+ "none",
63875
+ true,
63876
+ "none",
63877
+ void 0,
63878
+ format,
63879
+ sampleCount
63880
+ )
63881
+ );
63882
+ }
63883
+ }
63884
+ return Promise.all(warms).then(() => {
63885
+ });
62255
63886
  });
62256
63887
  }
62257
63888
  /**
@@ -62273,6 +63904,9 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62273
63904
  this.frameTextureView = null;
62274
63905
  this.frameTexture = null;
62275
63906
  }
63907
+ if (this.pipelineCache != null) {
63908
+ this.pipelineCache.inFrame = false;
63909
+ }
62276
63910
  this.destroyRetiredTextures();
62277
63911
  }
62278
63912
  /**
@@ -62292,6 +63926,9 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
62292
63926
  }
62293
63927
  this.frameTextureView = null;
62294
63928
  this.frameTexture = null;
63929
+ if (this.pipelineCache != null) {
63930
+ this.pipelineCache.inFrame = false;
63931
+ }
62295
63932
  this.currentRenderTarget = null;
62296
63933
  this.pendingColorClear = false;
62297
63934
  this.pendingDepthClear = false;
@@ -64050,6 +65687,10 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
64050
65687
  batcher.init(this);
64051
65688
  }
64052
65689
  emit(ONCONTEXT_RESTORED, this);
65690
+ if (this.settings.prewarm === true) {
65691
+ this.prewarm().catch(() => {
65692
+ });
65693
+ }
64053
65694
  }
64054
65695
  /**
64055
65696
  * Returns the WebGPU canvas context
@@ -69963,8 +71604,110 @@ var ShineEffect = class extends ShaderEffect {
69963
71604
  }
69964
71605
  };
69965
71606
 
69966
- // src/video/effects/tintPulse.js
71607
+ // src/video/effects/squish.js
69967
71608
  var wgslFragment15 = `
71609
+ struct SquishUniforms {
71610
+ uSquishFrame : vec4f,
71611
+ uSquishXFactor : f32,
71612
+ uSquishBulge : f32,
71613
+ };
71614
+ @group(3) @binding(0) var<uniform> fx : SquishUniforms;
71615
+
71616
+ fn apply(color : vec4f, uv : vec2f) -> vec4f {
71617
+ // -1..1 across the quad: the sprite's frame, stretched by the vertex scale
71618
+ var p = (uv - fx.uSquishFrame.xy) / fx.uSquishFrame.zw * 2.0 - 1.0;
71619
+ // undo the stretch: the sprite keeps its width, in the middle of the
71620
+ // wider quad, with room on both sides
71621
+ p.x = p.x * fx.uSquishXFactor;
71622
+ // each row widens by its share of the bulge: all of it across the
71623
+ // middle, none at the top and bottom
71624
+ p.x = p.x / max(1.0 + fx.uSquishBulge * sqrt(max(1.0 - p.y * p.y, 0.0)), 0.0001);
71625
+ let local = (p + 1.0) * 0.5;
71626
+ // sampled before the test, since textureSample needs uniform control flow
71627
+ let texel = textureSample(uTexture, uSampler, fx.uSquishFrame.xy + clamp(local, vec2f(0.0), vec2f(1.0)) * fx.uSquishFrame.zw) * vColor;
71628
+ let inside = all(local >= vec2f(0.0)) && all(local <= vec2f(1.0));
71629
+ return select(vec4f(0.0), texel, inside);
71630
+ }
71631
+ `;
71632
+ var SquishEffect = class _SquishEffect extends ShaderEffect {
71633
+ /**
71634
+ * @param {Renderer} renderer - the current renderer instance
71635
+ * @param {object} [options] - effect options
71636
+ * @param {number} [options.bulge=0] - how far the middle bulges out (positive) or pinches in (negative), from -1 to 1
71637
+ */
71638
+ constructor(renderer, options = {}) {
71639
+ super(renderer, {
71640
+ glsl: `
71641
+ uniform vec4 uSquishFrame;
71642
+ uniform float uSquishXFactor;
71643
+ uniform float uSquishBulge;
71644
+ vec4 apply(vec4 color, vec2 uv) {
71645
+ // -1..1 across the quad: the sprite's frame, stretched by the vertex scale
71646
+ vec2 p = (uv - uSquishFrame.xy) / uSquishFrame.zw * 2.0 - 1.0;
71647
+ // undo the stretch: the sprite keeps its width, in the middle
71648
+ // of the wider quad, with room on both sides
71649
+ p.x *= uSquishXFactor;
71650
+ // each row widens by its share of the bulge: all of it across
71651
+ // the middle, none at the top and bottom
71652
+ p.x /= max(1.0 + uSquishBulge * sqrt(max(1.0 - p.y * p.y, 0.0)), 0.0001);
71653
+ vec2 local = (p + 1.0) * 0.5;
71654
+ // transparent past the sprite: the room around it, or the
71655
+ // indent of a pinch
71656
+ if (local.x < 0.0 || local.x > 1.0 || local.y < 0.0 || local.y > 1.0) {
71657
+ return vec4(0.0);
71658
+ }
71659
+ return texture2D(uSampler, uSquishFrame.xy + local * uSquishFrame.zw) * vColor;
71660
+ }
71661
+ `,
71662
+ wgsl: wgslFragment15
71663
+ });
71664
+ this._bulge = 0;
71665
+ this._frame = new Float32Array([0, 0, 1, 1]);
71666
+ this.setUniform("uSquishFrame", this._frame);
71667
+ this.setBulge(options.bulge ?? 0);
71668
+ }
71669
+ /**
71670
+ * set how far the middle of the sprite bulges out, or pinches in
71671
+ * @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
71672
+ */
71673
+ setBulge(bulge) {
71674
+ const value = Math.min(Math.max(bulge, -1), 1);
71675
+ this._bulge = value;
71676
+ const xFactor = 1 + Math.max(value, 0);
71677
+ this._vertexScale.x = xFactor;
71678
+ this.setUniform("uSquishXFactor", xFactor);
71679
+ this.setUniform("uSquishBulge", value);
71680
+ }
71681
+ /**
71682
+ * The batchers hand every effect the frame it draws through here (it is
71683
+ * what `noise_uv` runs across), and the bulge is measured against the
71684
+ * same frame: where the sprite sits in its atlas, in UV units. A render
71685
+ * target blit hands over the whole target.
71686
+ * @ignore
71687
+ * @internal
71688
+ */
71689
+ _setNoiseUVRect(sourceWidth, sourceHeight, objectWidth, objectHeight, u0, v0) {
71690
+ const frame = this._frame;
71691
+ frame[0] = u0;
71692
+ frame[1] = v0;
71693
+ frame[2] = Math.max(Math.abs(objectWidth), 1) / Math.max(sourceWidth, 1);
71694
+ frame[3] = Math.max(Math.abs(objectHeight), 1) / Math.max(sourceHeight, 1);
71695
+ this.setUniform("uSquishFrame", frame);
71696
+ }
71697
+ /**
71698
+ * Create an independent copy of this effect, with the same bulge.
71699
+ * @returns {SquishEffect} a new effect
71700
+ */
71701
+ clone() {
71702
+ if (this.destroyed) {
71703
+ throw new Error("SquishEffect.clone: effect has been destroyed");
71704
+ }
71705
+ return new _SquishEffect(this._renderer, { bulge: this._bulge });
71706
+ }
71707
+ };
71708
+
71709
+ // src/video/effects/tintPulse.js
71710
+ var wgslFragment16 = `
69968
71711
  struct TintPulseUniforms {
69969
71712
  uPulseColor : vec3f,
69970
71713
  uPulseSpeed : f32,
@@ -69998,7 +71741,7 @@ var TintPulseEffect = class extends ShaderEffect {
69998
71741
  return vec4(mix(color.rgb, uPulseColor * color.a, pulse), color.a);
69999
71742
  }
70000
71743
  `,
70001
- wgsl: wgslFragment15
71744
+ wgsl: wgslFragment16
70002
71745
  });
70003
71746
  const color = options.color ?? [1, 0, 0];
70004
71747
  this.setUniform("uPulseColor", new Float32Array(color));
@@ -70025,7 +71768,7 @@ var fragment = `
70025
71768
  return vec4(color.rgb * v, color.a);
70026
71769
  }
70027
71770
  `;
70028
- var wgslFragment16 = `
71771
+ var wgslFragment17 = `
70029
71772
  struct VignetteUniforms {
70030
71773
  uStrength : f32,
70031
71774
  uSize : f32,
@@ -70046,7 +71789,7 @@ var VignetteEffect = class extends ShaderEffect {
70046
71789
  * @param {number} [options.size=25.0] - vignette spread multiplier (higher = smaller dark area)
70047
71790
  */
70048
71791
  constructor(renderer, options = {}) {
70049
- super(renderer, { glsl: fragment, wgsl: wgslFragment16 });
71792
+ super(renderer, { glsl: fragment, wgsl: wgslFragment17 });
70050
71793
  this.strength = options.strength ?? 0.15;
70051
71794
  this.size = options.size ?? 25;
70052
71795
  this.setUniform("uStrength", this.strength);
@@ -70071,7 +71814,7 @@ var VignetteEffect = class extends ShaderEffect {
70071
71814
  };
70072
71815
 
70073
71816
  // src/video/effects/wave.js
70074
- var wgslFragment17 = `
71817
+ var wgslFragment18 = `
70075
71818
  struct WaveUniforms {
70076
71819
  uAmplitude : f32,
70077
71820
  uFrequency : f32,
@@ -70107,7 +71850,7 @@ var WaveEffect = class extends ShaderEffect {
70107
71850
  return texture2D(uSampler, distorted) * vColor;
70108
71851
  }
70109
71852
  `,
70110
- wgsl: wgslFragment17
71853
+ wgsl: wgslFragment18
70111
71854
  });
70112
71855
  this.setUniform("uAmplitude", options.amplitude ?? 0.01);
70113
71856
  this.setUniform("uFrequency", options.frequency ?? 10);
@@ -71704,6 +73447,7 @@ export {
71704
73447
  Sphere,
71705
73448
  Sprite,
71706
73449
  Sprite3d,
73450
+ SquishEffect,
71707
73451
  Stage,
71708
73452
  TMXHexagonalRenderer,
71709
73453
  TMXIsometricRenderer,