@moku-labs/game 0.15.6 → 0.15.7

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.
@@ -598,6 +598,16 @@ const DROP_SHARE = .1;
598
598
  const DWELL_BASE_MS = 5e3;
599
599
  /** The dwell never grows past this, ms. */
600
600
  const DWELL_MAX_MS = 12e4;
601
+ /** The render scales the adaptive rule steps through, highest first: shares of the canvas resolution. */
602
+ const SCALE_STEPS = Object.freeze([
603
+ 1,
604
+ .85,
605
+ .75,
606
+ .6,
607
+ .5
608
+ ]);
609
+ /** No scale to step through: the renderer allows none. */
610
+ const NO_SCALES = Object.freeze([]);
601
611
  /** The thermal statuses in order of heat: Android's `THERMAL_STATUS_*` numbers are their indexes. */
602
612
  const THERMAL_STATUSES = [
603
613
  "none",
@@ -632,6 +642,22 @@ function ladderOf(displayHz, maxFps, floor) {
632
642
  return Object.freeze(steps);
633
643
  }
634
644
  /**
645
+ * The render scales the rule may choose down to a floor, highest first, frozen; none when 1 alone reaches it. A floor
646
+ * between two steps stops at the step above it (the renderer's config only takes a step). Allocates: called when the
647
+ * renderer allows a floor, never per frame.
648
+ *
649
+ * @param floor - The lowest scale the renderer allows.
650
+ * @returns The scales.
651
+ * @example
652
+ * ```ts
653
+ * scaleStepsOf(0.75); // [1, 0.85, 0.75]
654
+ * ```
655
+ */
656
+ function scaleStepsOf(floor) {
657
+ const steps = SCALE_STEPS.filter((step) => step >= floor);
658
+ return steps.length > 1 ? Object.freeze(steps) : NO_SCALES;
659
+ }
660
+ /**
635
661
  * The index in `DISPLAY_RATES` a tick interval votes for: the rate whose period is within `DISPLAY_TOLERANCE` of
636
662
  * it, or -1. An indexed loop: no allocation.
637
663
  *
@@ -789,17 +815,20 @@ function thermalTarget(ladder, status, fps) {
789
815
  //#region src/plugins/time/rate.ts
790
816
  /**
791
817
  * @file time — the adaptive frame rate (spec 21 G3–G6): the governor that drops to the next divisor of the display
792
- * rate when drawn frames come late and returns one step after steady room, the display rate learned from the ticks
793
- * in every app (and with the governor from the native shell), the thermal input, and `setRate`. Every number of the
794
- * per-frame path lives in the governor and the display state, allocated once; a frame allocates nothing here. Only a
795
- * change allocates: its payload, the snapshot, the request.
818
+ * rate when drawn frames come late and returns one step after steady room, on a GPU-bound screen the render scale
819
+ * stepped before the rate, the back-off of failed returns, the display rate learned from the ticks in every app (and
820
+ * with the governor from the native shell), the thermal input, and `setRate`. Every number of the per-frame path lives
821
+ * in the governor and the display state, allocated once; a frame allocates nothing here. Only a change allocates: its
822
+ * payload, the snapshot, the request.
796
823
  */
797
- /** The ladder before the display rate is known, and always without `adaptive`. */
824
+ /** The ladder before the display rate is known, and always without `adaptive`; the scales the renderer allows none of. */
798
825
  const NO_STEPS = Object.freeze([]);
799
826
  /** Bit of a sample that came late. */
800
827
  const MISSED = 1;
801
828
  /** Bit of a sample whose frame was heavy for the higher rate. */
802
829
  const HEAVY = 2;
830
+ /** Bit of a late sample whose frame kept the main thread longer than its frame. */
831
+ const BUSY = 4;
803
832
  /** The thermal rank from which the rule never steps up: `"moderate"` and every hotter status. */
804
833
  const HEAT_LIMIT_RANK = 2;
805
834
  /** Milliseconds in a second: turns a rate into the period of one frame. */
@@ -852,6 +881,7 @@ function createGovernor(floor) {
852
881
  return {
853
882
  floor,
854
883
  ladder: NO_STEPS,
884
+ scales: NO_STEPS,
855
885
  thermal: null,
856
886
  lastDrawn: false,
857
887
  lastIdle: false,
@@ -865,12 +895,14 @@ function createGovernor(floor) {
865
895
  dropTail: 0,
866
896
  dropDrawn: 0,
867
897
  dropMissed: 0,
898
+ dropBusy: 0,
868
899
  returnTail: 0,
869
900
  returnDrawn: 0,
870
901
  returnMissed: 0,
871
902
  returnHeavy: 0,
872
903
  switchedAt: Number.NEGATIVE_INFINITY,
873
904
  lastReturnAt: Number.NEGATIVE_INFINITY,
905
+ climbed: false,
874
906
  dwellMs: DWELL_BASE_MS,
875
907
  requested: 0,
876
908
  requestSeq: 0
@@ -906,7 +938,8 @@ function snapshotOf(rate) {
906
938
  ladder: governor?.ladder ?? NO_STEPS,
907
939
  thermal: governor?.thermal ?? null,
908
940
  asked,
909
- heldBy: asked !== null && hz !== null && asked > hz ? "panel" : null
941
+ heldBy: asked !== null && hz !== null && asked > hz ? "panel" : null,
942
+ scale: rate.scale
910
943
  });
911
944
  }
912
945
  /**
@@ -923,6 +956,7 @@ function createRateState(config) {
923
956
  const mode = floor === void 0 ? "fixed" : "auto";
924
957
  const parts = {
925
958
  fps: config.maxFps,
959
+ scale: 1,
926
960
  mode,
927
961
  reason: "start",
928
962
  display: createDisplayState(),
@@ -953,6 +987,7 @@ function emptyWindows(g) {
953
987
  g.dropTail = g.written;
954
988
  g.dropDrawn = 0;
955
989
  g.dropMissed = 0;
990
+ g.dropBusy = 0;
956
991
  g.returnTail = g.written;
957
992
  g.returnDrawn = 0;
958
993
  g.returnMissed = 0;
@@ -967,6 +1002,7 @@ function dropOldest(g) {
967
1002
  const flags = g.sampleFlags[g.dropTail % 512] ?? 0;
968
1003
  g.dropDrawn -= 1;
969
1004
  if ((flags & MISSED) !== 0) g.dropMissed -= 1;
1005
+ if ((flags & BUSY) !== 0) g.dropBusy -= 1;
970
1006
  g.dropTail += 1;
971
1007
  }
972
1008
  /**
@@ -1022,8 +1058,19 @@ function evidenceOf(g, reason) {
1022
1058
  };
1023
1059
  }
1024
1060
  /**
1025
- * The dwell's bookkeeping of a switch: a drop soon after a return doubles it (a failed return), a drop after a long
1026
- * hold resets it, a return notes when it happened, `setRate` starts it over; the display and the heat leave it.
1061
+ * Starts the back-off over: the base dwell, no return to judge.
1062
+ *
1063
+ * @param g - The governor.
1064
+ */
1065
+ function resetDwell(g) {
1066
+ g.dwellMs = DWELL_BASE_MS;
1067
+ g.lastReturnAt = Number.NEGATIVE_INFINITY;
1068
+ g.climbed = false;
1069
+ }
1070
+ /**
1071
+ * The dwell's bookkeeping of a switch: the first drop after a return judges it (held less than a minute it failed and
1072
+ * the dwell doubles, held longer the dwell starts over), the steps down after it leave the dwell, a return notes when
1073
+ * it happened, `setRate` starts it over; the display and the heat leave it.
1027
1074
  *
1028
1075
  * @param g - The governor.
1029
1076
  * @param reason - The reason of the switch.
@@ -1031,19 +1078,16 @@ function evidenceOf(g, reason) {
1031
1078
  */
1032
1079
  function keepDwell(g, reason, at) {
1033
1080
  switch (reason) {
1034
- case "missed": {
1035
- const held = at - g.lastReturnAt;
1036
- if (held < 1e4) g.dwellMs = Math.min(2 * g.dwellMs, DWELL_MAX_MS);
1037
- else if (held >= 6e4) g.dwellMs = DWELL_BASE_MS;
1081
+ case "missed":
1082
+ if (g.climbed) g.dwellMs = at - g.lastReturnAt < 6e4 ? Math.min(2 * g.dwellMs, DWELL_MAX_MS) : DWELL_BASE_MS;
1083
+ g.climbed = false;
1038
1084
  break;
1039
- }
1040
1085
  case "headroom":
1041
1086
  g.lastReturnAt = at;
1087
+ g.climbed = true;
1042
1088
  break;
1043
1089
  case "forced":
1044
- case "auto":
1045
- g.dwellMs = DWELL_BASE_MS;
1046
- g.lastReturnAt = Number.NEGATIVE_INFINITY;
1090
+ case "auto": resetDwell(g);
1047
1091
  }
1048
1092
  }
1049
1093
  /**
@@ -1082,22 +1126,25 @@ function requestRate(ctx) {
1082
1126
  followNativeRate(ctx, (answer) => readAnswer(ctx, g, seq, answer));
1083
1127
  }
1084
1128
  /**
1085
- * The one place the rate or its mode changes (the governor, the display, the heat and `setRate` call it). Nothing
1086
- * when both stay. Otherwise the dwell is kept, both windows are emptied and the next interval is skipped, the
1087
- * snapshot is rebuilt, `time:rate` is emitted with the evidence, the change is logged and the shell is asked for the
1088
- * new rate. The new cap applies from the next tick: the frame that is due still runs.
1129
+ * The one place the rate, the render scale or the mode changes (the governor, the display, the heat and `setRate`
1130
+ * call it). Nothing when all stay. Otherwise the dwell is kept, both windows are emptied and the next interval is
1131
+ * skipped, the snapshot is rebuilt, `time:rate` is emitted with the evidence, the change is logged and the shell is
1132
+ * asked for the new rate. The new cap applies from the next tick: the frame that is due still runs, at the new scale.
1089
1133
  *
1090
1134
  * @param ctx - Domain context of the time plugin.
1091
1135
  * @param to - The new rate.
1136
+ * @param scale - The new render scale.
1092
1137
  * @param mode - The new mode.
1093
1138
  * @param reason - Why.
1094
1139
  * @param at - When, on the page clock.
1095
1140
  */
1096
- function switchRate(ctx, to, mode, reason, at) {
1141
+ function switchRate(ctx, to, scale, mode, reason, at) {
1097
1142
  const { rate } = ctx.state;
1098
- if (to === rate.fps && mode === rate.mode) return;
1143
+ if (to === rate.fps && scale === rate.scale && mode === rate.mode) return;
1099
1144
  const g = rate.governor;
1100
1145
  const from = rate.fps;
1146
+ const fromScale = rate.scale;
1147
+ const scaleOnly = to === from && mode === rate.mode;
1101
1148
  const evidence = evidenceOf(g, reason);
1102
1149
  if (g !== void 0) {
1103
1150
  keepDwell(g, reason, at);
@@ -1106,6 +1153,7 @@ function switchRate(ctx, to, mode, reason, at) {
1106
1153
  emptyWindows(g);
1107
1154
  }
1108
1155
  rate.fps = to;
1156
+ rate.scale = scale;
1109
1157
  rate.mode = mode;
1110
1158
  rate.reason = reason;
1111
1159
  rate.snapshot = snapshotOf(rate);
@@ -1117,8 +1165,15 @@ function switchRate(ctx, to, mode, reason, at) {
1117
1165
  displayHz: rate.display.hz,
1118
1166
  thermal: g?.thermal ?? null,
1119
1167
  window: evidence,
1120
- dwellMs: g?.dwellMs ?? 0
1168
+ dwellMs: g?.dwellMs ?? 0,
1169
+ scale
1170
+ });
1171
+ if (scale !== fromScale) ctx.log.info("time: render scale", {
1172
+ from: fromScale,
1173
+ to: scale,
1174
+ reason
1121
1175
  });
1176
+ if (scaleOnly) return;
1122
1177
  ctx.log.info("time: frame rate", {
1123
1178
  from,
1124
1179
  to,
@@ -1137,7 +1192,7 @@ function applyThermal(ctx, g) {
1137
1192
  const { rate } = ctx.state;
1138
1193
  if (rate.mode !== "auto" || g.ladder.length === 0) return;
1139
1194
  const to = thermalTarget(g.ladder, g.thermal, rate.fps);
1140
- if (to < rate.fps) switchRate(ctx, to, "auto", "thermal", nowOf());
1195
+ if (to < rate.fps) switchRate(ctx, to, rate.scale, "auto", "thermal", nowOf());
1141
1196
  }
1142
1197
  /**
1143
1198
  * Stores a new thermal status of the shell and applies it. A status the shell sent wrong, and the same status
@@ -1182,7 +1237,7 @@ function learnDisplay(ctx, hz, at) {
1182
1237
  if (g !== void 0) g.ladder = ladderOf(hz, ctx.config.maxFps, g.floor);
1183
1238
  rate.snapshot = snapshotOf(rate);
1184
1239
  if (g === void 0 || rate.mode !== "auto") return;
1185
- if (!g.ladder.includes(rate.fps)) switchRate(ctx, g.ladder.find((step) => step <= rate.fps) ?? g.ladder.at(-1) ?? rate.fps, "auto", "display", at);
1240
+ if (!g.ladder.includes(rate.fps)) switchRate(ctx, g.ladder.find((step) => step <= rate.fps) ?? g.ladder.at(-1) ?? rate.fps, rate.scale, "auto", "display", at);
1186
1241
  if (first) applyThermal(ctx, g);
1187
1242
  }
1188
1243
  /**
@@ -1279,31 +1334,54 @@ function shouldDrop(g) {
1279
1334
  return g.dropMissed >= Math.max(8, Math.ceil(DROP_SHARE * g.dropDrawn));
1280
1335
  }
1281
1336
  /**
1282
- * Tells whether the return window has room to step up: no heat, the dwell passed, 90 drawn frames in three seconds
1283
- * with at most one late and a tenth heavy.
1337
+ * Tells whether the late frames of the drop window are GPU-bound: fewer than half of them kept the main thread longer
1338
+ * than their frame.
1339
+ *
1340
+ * @param g - The governor.
1341
+ * @returns True when the scale, not the rate, should give.
1342
+ */
1343
+ function gpuBound(g) {
1344
+ return 2 * g.dropBusy < g.dropMissed;
1345
+ }
1346
+ /**
1347
+ * Tells whether the return window has room to step up: no heat, the dwell passed (twice the dwell for a step of the
1348
+ * scale), 90 drawn frames in three seconds with at most one late, and for a step of the rate a tenth heavy at most.
1284
1349
  *
1285
1350
  * @param g - The governor.
1286
1351
  * @param at - The page time of the sample.
1287
- * @returns True when the rate may return one step.
1352
+ * @param scaleStep - The step up is one of the render scale: it costs the GPU, not the main thread.
1353
+ * @returns True when the rule may return one step.
1288
1354
  */
1289
- function canReturn(g, at) {
1290
- return thermalRank(g.thermal) < HEAT_LIMIT_RANK && at - g.switchedAt >= g.dwellMs && g.returnDrawn >= 90 && g.returnMissed <= 1 && g.returnHeavy <= Math.floor(.1 * g.returnDrawn);
1355
+ function canReturn(g, at, scaleStep) {
1356
+ return thermalRank(g.thermal) < HEAT_LIMIT_RANK && at - g.switchedAt >= (scaleStep ? 2 * g.dwellMs : g.dwellMs) && g.returnDrawn >= 90 && g.returnMissed <= 1 && (scaleStep || g.returnHeavy <= Math.floor(.1 * g.returnDrawn));
1291
1357
  }
1292
1358
  /**
1293
- * Decides on the windows after a sample: down one step when late intervals reach `max(8, 10 %)` of the second's,
1294
- * up one step after the dwell with 90 steady drawn frames in three seconds, at most one late and a tenth heavy, and
1295
- * no heat.
1359
+ * Decides on the windows after a sample. Down one step when late intervals reach `max(8, 10 %)` of the second's: the
1360
+ * render scale when the window is GPU-bound and a lower scale is allowed, the rate otherwise. Up one step in the
1361
+ * reverse of the way down, with 90 steady drawn frames in three seconds, at most one late and no heat: the rate first,
1362
+ * at the scale it dropped at, after the dwell and with a tenth heavy at most, then the scale after twice the dwell. A
1363
+ * probe of the higher rate so never starts at full scale, and its failure changes no resolution.
1296
1364
  *
1297
1365
  * @param ctx - Domain context of the time plugin.
1298
1366
  * @param g - The governor.
1299
1367
  * @param at - The page time of the sample.
1300
1368
  */
1301
1369
  function decide(ctx, g, at) {
1302
- const { fps } = ctx.state.rate;
1303
- const lower = stepBelow(g.ladder, fps);
1370
+ const { fps, scale } = ctx.state.rate;
1371
+ if (shouldDrop(g)) {
1372
+ const smaller = gpuBound(g) ? stepBelow(g.scales, scale) : void 0;
1373
+ const lower = stepBelow(g.ladder, fps);
1374
+ if (smaller !== void 0) switchRate(ctx, fps, smaller, "auto", "missed", at);
1375
+ else if (lower !== void 0) switchRate(ctx, lower, scale, "auto", "missed", at);
1376
+ return;
1377
+ }
1304
1378
  const higher = stepAbove(g.ladder, fps);
1305
- if (lower !== void 0 && shouldDrop(g)) switchRate(ctx, lower, "auto", "missed", at);
1306
- else if (higher !== void 0 && canReturn(g, at)) switchRate(ctx, higher, "auto", "headroom", at);
1379
+ if (higher !== void 0 && canReturn(g, at, false)) {
1380
+ switchRate(ctx, higher, scale, "auto", "headroom", at);
1381
+ return;
1382
+ }
1383
+ const larger = stepAbove(g.scales, scale);
1384
+ if (larger !== void 0 && canReturn(g, at, true)) switchRate(ctx, fps, larger, "auto", "headroom", at);
1307
1385
  }
1308
1386
  /**
1309
1387
  * Judges the interval from the last run frame's start to this one: the first after a change is skipped, one that is
@@ -1325,10 +1403,11 @@ function judgeFrame$1(ctx, g, intervalMs, at) {
1325
1403
  const higher = stepAbove(g.ladder, fps);
1326
1404
  const missed = intervalMs > missLineMs(fps, ctx.state.rate.display.hz ?? fps);
1327
1405
  const heavy = higher !== void 0 && g.lastWorkMs > .5 * SECOND_MS$1 / higher;
1406
+ const busy = missed && g.lastWorkMs > SECOND_MS$1 / fps;
1328
1407
  const slot = g.written % 512;
1329
1408
  evict$2(g, at);
1330
1409
  g.sampleAt[slot] = at;
1331
- g.sampleFlags[slot] = (missed ? MISSED : 0) | (heavy ? HEAVY : 0);
1410
+ g.sampleFlags[slot] = (missed ? MISSED : 0) | (heavy ? HEAVY : 0) | (busy ? BUSY : 0);
1332
1411
  g.written += 1;
1333
1412
  g.dropDrawn += 1;
1334
1413
  g.returnDrawn += 1;
@@ -1337,6 +1416,7 @@ function judgeFrame$1(ctx, g, intervalMs, at) {
1337
1416
  g.returnMissed += 1;
1338
1417
  }
1339
1418
  if (heavy) g.returnHeavy += 1;
1419
+ if (busy) g.dropBusy += 1;
1340
1420
  decide(ctx, g, at);
1341
1421
  }
1342
1422
  /**
@@ -1358,6 +1438,39 @@ function closeFrame(ctx, g, workMs) {
1358
1438
  g.lastWorkMs = Math.max(0, workMs - agentMs);
1359
1439
  }
1360
1440
  /**
1441
+ * Starts the governor over for a new scene: the back-off of failed returns goes back to the base dwell, both windows
1442
+ * are emptied and the next interval is skipped, so the new screen is judged on its own frames. The rate and the scale
1443
+ * stay. Nothing without a governor.
1444
+ *
1445
+ * @param rate - The rate state.
1446
+ */
1447
+ function sceneRate(rate) {
1448
+ const g = rate.governor;
1449
+ if (g === void 0) return;
1450
+ resetDwell(g);
1451
+ g.skipNext = true;
1452
+ emptyWindows(g);
1453
+ }
1454
+ /**
1455
+ * Sets the render scales the rule may step through, down to `floor`. A scale under the new floor goes up to it at
1456
+ * once, quietly: the caller draws at what it decided. Nothing without a governor.
1457
+ *
1458
+ * @param rate - The rate state.
1459
+ * @param floor - The lowest scale the renderer allows; 1 allows none.
1460
+ * @returns The frame rate after the call.
1461
+ */
1462
+ function allowScale(rate, floor) {
1463
+ const g = rate.governor;
1464
+ if (g === void 0) return rate.snapshot;
1465
+ g.scales = scaleStepsOf(floor);
1466
+ const lowest = g.scales.at(-1) ?? 1;
1467
+ if (rate.scale < lowest) {
1468
+ rate.scale = lowest;
1469
+ rate.snapshot = snapshotOf(rate);
1470
+ }
1471
+ return rate.snapshot;
1472
+ }
1473
+ /**
1361
1474
  * Restarts the tick history of the display after a resume and, with a governor, its windows, and skips the next
1362
1475
  * interval: a return from the background is a new situation. The display rate already known stays.
1363
1476
  *
@@ -1403,10 +1516,10 @@ function resumeAuto(ctx) {
1403
1516
  const { maxFps } = ctx.config;
1404
1517
  const g = ctx.state.rate.governor;
1405
1518
  if (g === void 0) {
1406
- switchRate(ctx, maxFps, "fixed", "auto", nowOf());
1519
+ switchRate(ctx, maxFps, 1, "fixed", "auto", nowOf());
1407
1520
  return;
1408
1521
  }
1409
- switchRate(ctx, autoRate(g, maxFps), "auto", "auto", nowOf());
1522
+ switchRate(ctx, autoRate(g, maxFps), 1, "auto", "auto", nowOf());
1410
1523
  }
1411
1524
  /**
1412
1525
  * Forces the rate (the exact cap, never above `maxFps`) or gives it back to the config with `"auto"`.
@@ -1420,7 +1533,7 @@ function setRateChoice(ctx, choice) {
1420
1533
  const value = choice;
1421
1534
  if (!isChoice(value)) throw new Error(`[game] time.setRate takes "auto", 30, 60 or 120.\n Pass one of them, not ${String(value)}.`);
1422
1535
  const { rate } = ctx.state;
1423
- if (value !== "auto") switchRate(ctx, Math.min(value, ctx.config.maxFps), "forced", "forced", nowOf());
1536
+ if (value !== "auto") switchRate(ctx, Math.min(value, ctx.config.maxFps), 1, "forced", "forced", nowOf());
1424
1537
  else if (rate.mode === "forced") resumeAuto(ctx);
1425
1538
  return rate.snapshot;
1426
1539
  }
@@ -1694,7 +1807,7 @@ function countFrameListeners(ctx) {
1694
1807
  }
1695
1808
  /**
1696
1809
  * Creates the time API: frame callbacks per phase, the `Time` resource, scale, pause and
1697
- * resume, `wake` for the idle cap, the measurement of an agent build, the frame rate, and `step`
1810
+ * resume, `wake` for the idle cap, the measurement of an agent build, the frame rate and its render scale, and `step`
1698
1811
  * for tests and tools.
1699
1812
  *
1700
1813
  * @param ctx - Domain context of the time plugin.
@@ -1744,6 +1857,8 @@ function createTimeApi(ctx) {
1744
1857
  },
1745
1858
  rate: () => ctx.state.rate.snapshot,
1746
1859
  setRate: (choice) => setRateChoice(ctx, choice),
1860
+ allowScale: (floor) => allowScale(ctx.state.rate, floor),
1861
+ sceneChanged: () => sceneRate(ctx.state.rate),
1747
1862
  frameDrawn: () => {
1748
1863
  ctx.state.rate.drawnNow = true;
1749
1864
  },
@@ -1907,7 +2022,7 @@ const timePlugin = /*#__PURE__*/ createPlugin$1("time", {
1907
2022
  },
1908
2023
  events: (register) => register.map({
1909
2024
  "time:thermal": "The native shell reported a thermal reading",
1910
- "time:rate": "The loop's frame rate changed"
2025
+ "time:rate": "The loop's frame rate or render scale changed"
1911
2026
  }),
1912
2027
  createState: createTimeState,
1913
2028
  api: createTimeApi,
@@ -12845,7 +12960,541 @@ function installRebindingRenderGroupPipe(pixi) {
12845
12960
  };
12846
12961
  }
12847
12962
  //#endregion
12963
+ //#region src/plugins/renderer/host/render-pass.ts
12964
+ /** Pixi's `CLEAR.NONE`. */
12965
+ const CLEAR_NONE = 0;
12966
+ /** Pixi's `CLEAR.ALL`: colour, depth and stencil. */
12967
+ const CLEAR_ALL = 17664;
12968
+ /** Pixi's `CLEAR.COLOR`. */
12969
+ const CLEAR_COLOR = 16384;
12970
+ /**
12971
+ * The `CLEAR` bits of a pass's clear, as Pixi reads a boolean. `clearBits(true); // 17664`
12972
+ *
12973
+ * @param clear - The clear Pixi was handed.
12974
+ * @returns The bits.
12975
+ */
12976
+ function clearBits(clear) {
12977
+ if (clear === true) return CLEAR_ALL;
12978
+ if (clear === false) return CLEAR_NONE;
12979
+ return clear;
12980
+ }
12981
+ /**
12982
+ * Builds the classes of the lazy canvas pass from the module object, at call time, because the bases arrive with the
12983
+ * lazily loaded module. Each system keeps its base's `static extension` object as it is, so the registry files it
12984
+ * under the same type and name. They cost one test of two booleans per pipeline request, and nothing else per draw.
12985
+ *
12986
+ * @param pixi - The loaded Pixi module.
12987
+ * @returns The render target system and the pipeline system.
12988
+ */
12989
+ function createLazyCanvasClasses(pixi) {
12990
+ /**
12991
+ * Pixi's WebGPU adaptor, which begins a pass on the canvas at its first draw and sets the canvas's stencil and depth
12992
+ * operations by the rule of the file. One per renderer.
12993
+ */
12994
+ class LazyCanvasAdaptor extends pixi.GpuRenderTargetAdaptor {
12995
+ /** The canvas is bound and its pass has not begun: the pipeline system begins it at the first draw. */
12996
+ pending = false;
12997
+ /** A pipeline that tests or writes depth was asked for: the canvas keeps its depth from then on. */
12998
+ depthUsed = false;
12999
+ /** The renderer: Pixi's own `_renderer` is private. */
13000
+ gpu = void 0;
13001
+ /** The render target system: Pixi's own `_renderTargetSystem` is private. */
13002
+ targets = void 0;
13003
+ /** The renderer's pipeline system is the lazy one: only then does a pass wait for a draw. */
13004
+ hooked = false;
13005
+ /** The canvas whose pass is pending or whose clear is owed. */
13006
+ canvas = void 0;
13007
+ /** The `CLEAR` bits the canvas owes. */
13008
+ owed = CLEAR_NONE;
13009
+ /** The colour of the owed clear, copied, when one was given. */
13010
+ owedColor = [
13011
+ 0,
13012
+ 0,
13013
+ 0,
13014
+ 0
13015
+ ];
13016
+ /** `owedColor` holds the colour of the owed clear. */
13017
+ owedColorSet = false;
13018
+ /** The viewport of the canvas's pass to come, copied from the bind that deferred it. */
13019
+ view = new pixi.Rectangle();
13020
+ /** The canvas whose pass this adaptor began and has not ended. */
13021
+ openCanvas = void 0;
13022
+ /** The open canvas pass stores its stencil. */
13023
+ keepStencil = false;
13024
+ /** The open canvas pass stores its depth. */
13025
+ keepDepth = false;
13026
+ /** The pass being begun stores its stencil. */
13027
+ planStencil = false;
13028
+ /** The pass being begun stores its depth. */
13029
+ planDepth = false;
13030
+ /** The canvas pass that ended last stored its depth. */
13031
+ depthStored = false;
13032
+ /** A canvas pass began since the last `renderEnd`. */
13033
+ acquired = false;
13034
+ /** A mask was pushed on the canvas in the last frame drawn on it. */
13035
+ maskedBefore = false;
13036
+ /** A mask was pushed on the canvas in this frame. */
13037
+ maskedNow = false;
13038
+ /** The root bind of the canvas emitted no target change: the stencil system hears of it when its pass begins. */
13039
+ changeOwed = false;
13040
+ /**
13041
+ * Keeps the renderer and the render target system, then lets Pixi subscribe the adaptor.
13042
+ *
13043
+ * @param renderer - The WebGPU renderer.
13044
+ * @param renderTargetSystem - Its render target system.
13045
+ */
13046
+ init(renderer, renderTargetSystem) {
13047
+ super.init(renderer, renderTargetSystem);
13048
+ this.gpu = renderer;
13049
+ this.targets = renderTargetSystem;
13050
+ }
13051
+ /** Forgets every pass of the old device, then lets Pixi drop its restore helper. */
13052
+ contextChange() {
13053
+ super.contextChange();
13054
+ this.pending = false;
13055
+ this.canvas = void 0;
13056
+ this.owed = CLEAR_NONE;
13057
+ this.owedColorSet = false;
13058
+ this.openCanvas = void 0;
13059
+ this.depthStored = false;
13060
+ this.changeOwed = false;
13061
+ }
13062
+ /**
13063
+ * Tells whether the root bind of a render may skip its target change: the root is a canvas whose pass is deferred.
13064
+ * The adaptor then owes the stencil system the change, until the canvas pass begins or another target is bound.
13065
+ *
13066
+ * @param root - The root target of the render.
13067
+ * @returns True when the bind may skip the change.
13068
+ */
13069
+ owesChange(root) {
13070
+ this.changeOwed = this.defers(root);
13071
+ return this.changeOwed;
13072
+ }
13073
+ /**
13074
+ * Begins a pass as Pixi does on any target but the canvas inside a frame. On the canvas it reuses an open pass as
13075
+ * Pixi does, and otherwise ends the open pass and defers the canvas's, its clear owed.
13076
+ *
13077
+ * @param renderTarget - The target.
13078
+ * @param clear - What to clear.
13079
+ * @param clearColor - The colour to clear to.
13080
+ * @param viewport - The viewport.
13081
+ * @param mipLevel - The mip level.
13082
+ * @param layer - The array layer.
13083
+ */
13084
+ startRenderPass(renderTarget, clear = true, clearColor, viewport, mipLevel = 0, layer = 0) {
13085
+ if (viewport === void 0 || mipLevel !== 0 || layer !== 0 || !this.defers(renderTarget)) {
13086
+ this.pending = false;
13087
+ this.openCanvas = void 0;
13088
+ this.changeOwed = false;
13089
+ super.startRenderPass(renderTarget, clear, clearColor, viewport, mipLevel, layer);
13090
+ return;
13091
+ }
13092
+ const bits = clearBits(clear);
13093
+ if (this.canvas !== void 0 && this.canvas !== renderTarget) this.settleCanvas();
13094
+ if (bits === CLEAR_NONE && this.isOpen(renderTarget)) {
13095
+ this.beginCanvas(renderTarget, CLEAR_NONE, void 0, viewport);
13096
+ return;
13097
+ }
13098
+ this.finishRenderPass();
13099
+ this.canvas = renderTarget;
13100
+ this.pending = true;
13101
+ this.owe(bits, clearColor);
13102
+ this.view.copyFrom(viewport);
13103
+ }
13104
+ /** Ends the open pass, as Pixi does. */
13105
+ finishRenderPass() {
13106
+ super.finishRenderPass();
13107
+ this.openCanvas = void 0;
13108
+ }
13109
+ /**
13110
+ * Copies out of a render target, as Pixi does. A copy that reads or writes the canvas first pays the clear it owes,
13111
+ * or a first pass it never had; a canvas that had its pass in this render and owes nothing begins no empty one.
13112
+ *
13113
+ * @param source - The target copied from.
13114
+ * @param destination - The texture copied into.
13115
+ * @param from - The origin in the source.
13116
+ * @param size - The size of the copy.
13117
+ * @param to - The origin in the destination.
13118
+ * @returns The destination.
13119
+ */
13120
+ copyToTexture(source, destination, from, size, to) {
13121
+ const canvas = this.canvas;
13122
+ if (canvas !== void 0 && (source === canvas || destination.source === canvas.colorTexture) && (this.owed !== CLEAR_NONE || !this.acquired)) this.payOwed(canvas);
13123
+ return super.copyToTexture(source, destination, from, size, to);
13124
+ }
13125
+ /**
13126
+ * Copies a render target's depth, as Pixi does; a copy of the canvas's depth makes the canvas keep its depth from
13127
+ * then on, and pays the clear it owes first.
13128
+ *
13129
+ * @param source - The target copied from.
13130
+ * @param destination - The depth texture copied into.
13131
+ * @param from - The origin in the source.
13132
+ * @param size - The size of the copy.
13133
+ * @param to - The origin in the destination.
13134
+ */
13135
+ copyDepthTexture(source, destination, from, size, to) {
13136
+ if (this.isCanvas(source)) {
13137
+ this.depthUsed = true;
13138
+ if (source === this.canvas) this.payOwed(source);
13139
+ }
13140
+ super.copyDepthTexture(source, destination, from, size, to);
13141
+ }
13142
+ /** Begins the pending canvas pass, with the clear it owes: a draw is about to be recorded into it. */
13143
+ beginPending() {
13144
+ const target = this.canvas;
13145
+ if (!this.pending || target === void 0) return;
13146
+ const bits = this.owed;
13147
+ const color = this.owedColorSet ? this.owedColor : void 0;
13148
+ this.pending = false;
13149
+ this.canvas = void 0;
13150
+ this.owed = CLEAR_NONE;
13151
+ this.owedColorSet = false;
13152
+ this.beginCanvas(target, bits, color, this.view);
13153
+ }
13154
+ /**
13155
+ * A pipeline that tests or writes depth was asked for: the canvas keeps its depth from now on, and an open canvas
13156
+ * pass that discards it begins again, with the depth cleared, as nothing wrote it yet, and stored.
13157
+ */
13158
+ useDepth() {
13159
+ this.depthUsed = true;
13160
+ const canvas = this.openCanvas;
13161
+ if (canvas?.depthStencilAttachment !== void 0 && !this.keepDepth && this.passOpen()) this.restart(canvas);
13162
+ }
13163
+ /**
13164
+ * A stencil mask is about to be pushed on the bound target: on the canvas, the stencil is kept from now on.
13165
+ *
13166
+ * @param target - The bound target.
13167
+ */
13168
+ maskPushes(target) {
13169
+ if (this.isCanvas(target)) this.maskedNow = true;
13170
+ }
13171
+ /**
13172
+ * A stencil mask was pushed on the bound target: an open canvas pass that discards its stencil begins again, the
13173
+ * stencil cleared, as no mask was open, and stored, before the mask is drawn. A pending canvas pass whose target
13174
+ * change is owed begins now, so the stencil system knows its target before it sets the mask's mode.
13175
+ *
13176
+ * @param target - The bound target.
13177
+ */
13178
+ maskPushed(target) {
13179
+ if (this.changeOwed && this.pending && this.canvas === target) this.beginPending();
13180
+ else if (this.openCanvas === target && !this.keepStencil && this.passOpen()) this.restart(target);
13181
+ }
13182
+ /**
13183
+ * The end of a render call: pays the clear the canvas owes, gives a canvas that saw no pass one, so the frame
13184
+ * clears and presents it as Pixi's does, and moves the mask memory on after a frame drawn on the canvas.
13185
+ *
13186
+ * @param root - The root target of the render call.
13187
+ */
13188
+ settle(root) {
13189
+ this.settleCanvas();
13190
+ this.acquired = false;
13191
+ if (this.isCanvas(root)) {
13192
+ this.maskedBefore = this.maskedNow;
13193
+ this.maskedNow = false;
13194
+ }
13195
+ }
13196
+ /**
13197
+ * Tells whether a pass on a target waits for its first draw: the target is a canvas without multisampling, and the
13198
+ * renderer's pipeline system is the lazy one that begins it.
13199
+ *
13200
+ * @param target - The target.
13201
+ * @returns True when the pass is deferred.
13202
+ */
13203
+ defers(target) {
13204
+ if (!this.hooked) this.hooked = this.gpu?.pipeline instanceof LazyCanvasPipeline;
13205
+ return this.hooked && this.isCanvas(target);
13206
+ }
13207
+ /**
13208
+ * Tells whether a target is a canvas without multisampling: Pixi gives a canvas's GPU target its context.
13209
+ *
13210
+ * @param target - The target.
13211
+ * @returns True for such a canvas.
13212
+ */
13213
+ isCanvas(target) {
13214
+ const targets = this.targets;
13215
+ if (targets === void 0) return false;
13216
+ const gpuTarget = targets.getGpuRenderTarget(target);
13217
+ return gpuTarget.contexts.length > 0 && !gpuTarget.msaa;
13218
+ }
13219
+ /**
13220
+ * Tells whether a pass of the encoder is open.
13221
+ *
13222
+ * @returns True while one is.
13223
+ */
13224
+ passOpen() {
13225
+ const encoder = this.gpu?.encoder;
13226
+ return encoder !== void 0 && encoder.renderPassEncoder !== null;
13227
+ }
13228
+ /**
13229
+ * Tells whether the pass this adaptor began on a canvas is still open.
13230
+ *
13231
+ * @param target - The canvas.
13232
+ * @returns True when its pass is open.
13233
+ */
13234
+ isOpen(target) {
13235
+ return this.openCanvas === target && this.passOpen();
13236
+ }
13237
+ /**
13238
+ * Adds a clear to the one the canvas owes; a colour clear given a colour keeps a copy of it.
13239
+ *
13240
+ * @param bits - The `CLEAR` bits.
13241
+ * @param color - The colour, when one was given.
13242
+ */
13243
+ owe(bits, color) {
13244
+ const colour = bits & CLEAR_COLOR;
13245
+ this.owed |= bits;
13246
+ if (colour === 0) return;
13247
+ this.owedColorSet = Array.isArray(color);
13248
+ if (!Array.isArray(color)) return;
13249
+ this.owedColor[0] = color[0];
13250
+ this.owedColor[1] = color[1];
13251
+ this.owedColor[2] = color[2];
13252
+ this.owedColor[3] = color[3];
13253
+ }
13254
+ /**
13255
+ * Begins a pass on the canvas through Pixi, with the stencil and depth operations of the rule set on its
13256
+ * depth-stencil attachment first, where Pixi's descriptor reads them; a clear Pixi was asked for still wins. Pixi
13257
+ * reuses an open pass when nothing is cleared and nothing changed; a pass that really began is recorded.
13258
+ *
13259
+ * @param target - The canvas.
13260
+ * @param bits - The `CLEAR` bits.
13261
+ * @param color - The clear colour, when one was given.
13262
+ * @param viewport - The viewport.
13263
+ */
13264
+ beginCanvas(target, bits, color, viewport) {
13265
+ const gpu = this.gpu;
13266
+ const targets = this.targets;
13267
+ if (gpu === void 0 || targets === void 0) return;
13268
+ this.planDepthStencil(target, targets);
13269
+ const before = gpu.encoder.renderPassEncoder;
13270
+ if (viewport !== this.view) this.view.copyFrom(viewport);
13271
+ super.startRenderPass(target, bits, color, viewport, 0, 0);
13272
+ this.openCanvas = target;
13273
+ if (gpu.encoder.renderPassEncoder !== before) this.began(target, targets);
13274
+ }
13275
+ /**
13276
+ * Sets the stencil and depth operations of the rule on the canvas's depth-stencil attachment, made first when
13277
+ * Pixi would make it, and keeps what the pass stores.
13278
+ *
13279
+ * @param target - The canvas.
13280
+ * @param targets - The render target system.
13281
+ */
13282
+ planDepthStencil(target, targets) {
13283
+ if ((target.stencil || target.depth) && target.depthStencilAttachment === void 0) target.ensureDepthStencilTexture();
13284
+ const attachment = target.depthStencilAttachment;
13285
+ const masked = targets.getGpuRenderTarget(target).maskStackIndex !== 0;
13286
+ this.planStencil = masked || this.maskedBefore || this.maskedNow;
13287
+ this.planDepth = this.depthUsed && attachment !== void 0;
13288
+ if (attachment === void 0) return;
13289
+ attachment.stencilLoadOp = masked ? "load" : "clear";
13290
+ attachment.stencilStoreOp = this.planStencil ? "store" : "discard";
13291
+ attachment.depthLoadOp = this.depthStored ? "load" : "clear";
13292
+ attachment.depthStoreOp = this.planDepth ? "store" : "discard";
13293
+ }
13294
+ /**
13295
+ * Records a canvas pass that really began (Pixi did not reuse the open one), and tells the stencil system of the
13296
+ * canvas when the root bind owes it the change, now that there is a pass to set its reference on.
13297
+ *
13298
+ * @param target - The canvas.
13299
+ * @param targets - The render target system.
13300
+ */
13301
+ began(target, targets) {
13302
+ this.acquired = true;
13303
+ this.keepStencil = this.planStencil;
13304
+ this.keepDepth = this.planDepth;
13305
+ this.depthStored = this.planDepth;
13306
+ if (this.changeOwed) {
13307
+ this.changeOwed = false;
13308
+ targets.onRenderTargetChange.emit(target);
13309
+ }
13310
+ }
13311
+ /**
13312
+ * Begins and ends one pass on a canvas now, with the clear it owes, and leaves the canvas bound with nothing owed.
13313
+ *
13314
+ * @param target - The canvas.
13315
+ */
13316
+ payOwed(target) {
13317
+ const bits = this.owed;
13318
+ const color = this.owedColorSet ? this.owedColor : void 0;
13319
+ this.owed = CLEAR_NONE;
13320
+ this.owedColorSet = false;
13321
+ this.beginCanvas(target, bits, color, this.view);
13322
+ this.finishRenderPass();
13323
+ }
13324
+ /**
13325
+ * Ends the canvas's wait: pays the clear it owes, or a first pass it never had, and forgets it.
13326
+ */
13327
+ settleCanvas() {
13328
+ const target = this.canvas;
13329
+ if (target === void 0) return;
13330
+ if (this.owed !== CLEAR_NONE || !this.acquired) this.payOwed(target);
13331
+ this.pending = false;
13332
+ this.canvas = void 0;
13333
+ }
13334
+ /**
13335
+ * Begins the open canvas pass again with nothing cleared, by the rule as it stands now, and sets its stencil
13336
+ * reference again: a new pass starts from 0, and Pixi sets it only when the mode or the target changes.
13337
+ *
13338
+ * @param target - The canvas.
13339
+ */
13340
+ restart(target) {
13341
+ const gpu = this.gpu;
13342
+ const targets = this.targets;
13343
+ if (gpu === void 0 || targets === void 0) return;
13344
+ this.finishRenderPass();
13345
+ this.beginCanvas(target, CLEAR_NONE, void 0, this.view);
13346
+ gpu.encoder.setStencilReference(targets.getGpuRenderTarget(target).stencilReference);
13347
+ }
13348
+ }
13349
+ /** Pixi's WebGPU render target system over the lazy adaptor, which settles the canvas at the end of a render. */
13350
+ class LazyCanvasTargets extends pixi.GpuRenderTargetSystem {
13351
+ static extension = pixi.GpuRenderTargetSystem.extension;
13352
+ /** The adaptor, typed as the lazy one. */
13353
+ lazy;
13354
+ /**
13355
+ * Makes Pixi's system, then puts the lazy adaptor in the place of Pixi's.
13356
+ *
13357
+ * @param renderer - The WebGPU renderer.
13358
+ */
13359
+ constructor(renderer) {
13360
+ super(renderer);
13361
+ const lazy = new LazyCanvasAdaptor();
13362
+ lazy.init(renderer, this);
13363
+ this.adaptor = lazy;
13364
+ this.lazy = lazy;
13365
+ }
13366
+ /**
13367
+ * Starts a render as Pixi does; a root canvas whose pass is deferred is marked bound first, so Pixi's bind of it
13368
+ * emits no target change: the adaptor emits it when the pass begins.
13369
+ *
13370
+ * @param options - The root binding.
13371
+ */
13372
+ renderStart(options) {
13373
+ const root = this.getRenderTarget(options.target);
13374
+ if (this.renderTarget !== root && Boolean(root.flipY) === Boolean(options.flipY) && this.lazy.owesChange(root)) this.renderTarget = root;
13375
+ super.renderStart(options);
13376
+ }
13377
+ /** The end of a render call, before the encoder submits it: the canvas settles. */
13378
+ renderEnd() {
13379
+ this.lazy.settle(this.rootRenderTarget);
13380
+ }
13381
+ /** Makes sure the bound target has a stencil for a mask about to be pushed, and keeps it by the rule. */
13382
+ ensureDepthStencil() {
13383
+ const target = this.renderTarget;
13384
+ this.lazy.maskPushes(target);
13385
+ super.ensureDepthStencil();
13386
+ this.lazy.maskPushed(target);
13387
+ }
13388
+ }
13389
+ /** Pixi's WebGPU pipeline system, which begins a deferred canvas pass before a draw or a stencil reference needs it. */
13390
+ class LazyCanvasPipeline extends pixi.PipelineSystem {
13391
+ static extension = pixi.PipelineSystem.extension;
13392
+ /** The renderer: Pixi's own `_renderer` is private. */
13393
+ host;
13394
+ /** The lazy adaptor of the renderer, once found. */
13395
+ lazy = void 0;
13396
+ /**
13397
+ * Keeps the renderer it is made for.
13398
+ *
13399
+ * @param renderer - The WebGPU renderer.
13400
+ */
13401
+ constructor(renderer) {
13402
+ super(renderer);
13403
+ this.host = renderer;
13404
+ }
13405
+ /**
13406
+ * Notes a state that tests or writes depth, begins a pending canvas pass, then answers Pixi's pipeline. Its five
13407
+ * arguments are named, not spread: a rest array would be made at every draw.
13408
+ *
13409
+ * @param geometry - The geometry.
13410
+ * @param program - The program.
13411
+ * @param state - The state.
13412
+ * @param topology - The topology.
13413
+ * @param overrides - The shader overrides.
13414
+ * @returns The pipeline.
13415
+ */
13416
+ getPipeline(geometry, program, state, topology, overrides) {
13417
+ const lazy = this.lazy ?? this.link();
13418
+ if (lazy !== void 0) {
13419
+ if (!lazy.depthUsed && (state.depthTest || state.depthMask)) lazy.useDepth();
13420
+ if (lazy.pending) lazy.beginPending();
13421
+ }
13422
+ return super.getPipeline(geometry, program, state, topology, overrides);
13423
+ }
13424
+ /**
13425
+ * Begins a pending canvas pass, then answers Pixi's buffer names: the first call of a batch or a graphics draw.
13426
+ *
13427
+ * @param geometry - The geometry.
13428
+ * @param program - The program.
13429
+ * @returns The names of the buffers to bind.
13430
+ */
13431
+ getBufferNamesToBind(geometry, program) {
13432
+ this.beginPending();
13433
+ return super.getBufferNamesToBind(geometry, program);
13434
+ }
13435
+ /**
13436
+ * Begins a pending canvas pass, then sets the stencil mode: the stencil system sets its reference on the pass next.
13437
+ *
13438
+ * @param stencilMode - The stencil mode.
13439
+ */
13440
+ setStencilMode(stencilMode) {
13441
+ this.beginPending();
13442
+ super.setStencilMode(stencilMode);
13443
+ }
13444
+ /**
13445
+ * Begins a pending canvas pass, then answers Pixi's key: a render bundle is recorded or replayed next.
13446
+ *
13447
+ * @returns The key.
13448
+ */
13449
+ get bundleStateKey() {
13450
+ this.beginPending();
13451
+ return super.bundleStateKey;
13452
+ }
13453
+ /** Begins the pending canvas pass of the renderer, when there is one. */
13454
+ beginPending() {
13455
+ const lazy = this.lazy ?? this.link();
13456
+ if (lazy?.pending === true) lazy.beginPending();
13457
+ }
13458
+ /**
13459
+ * Finds the renderer's lazy adaptor, once its render target system exists.
13460
+ *
13461
+ * @returns The adaptor, or `undefined` while there is none.
13462
+ */
13463
+ link() {
13464
+ const adaptor = this.host.renderTarget.adaptor;
13465
+ if (adaptor instanceof LazyCanvasAdaptor) this.lazy = adaptor;
13466
+ return this.lazy;
13467
+ }
13468
+ }
13469
+ return {
13470
+ targets: LazyCanvasTargets,
13471
+ pipeline: LazyCanvasPipeline
13472
+ };
13473
+ }
13474
+ /**
13475
+ * Swaps Pixi's WebGPU render target and pipeline systems for the lazy canvas ones in the module's extension registry,
13476
+ * and registers Pixi's stencil system again after them: the registry appends a class it is given, and the stencil
13477
+ * system subscribes to the render target system when it is made, so it must come later. Every application made
13478
+ * afterwards defers its canvas pass. The registry is global to the module object, so the remover matters: it puts
13479
+ * Pixi's own classes back, the stencil system again last.
13480
+ *
13481
+ * @param pixi - The loaded Pixi module.
13482
+ * @returns The remover.
13483
+ * @throws {TypeError} When the module has none of the classes to subclass.
13484
+ */
13485
+ function installLazyCanvasPass(pixi) {
13486
+ const lazy = createLazyCanvasClasses(pixi);
13487
+ pixi.extensions.remove(pixi.GpuRenderTargetSystem, pixi.GpuStencilSystem, pixi.PipelineSystem);
13488
+ pixi.extensions.add(lazy.targets, pixi.GpuStencilSystem, lazy.pipeline);
13489
+ return () => {
13490
+ pixi.extensions.remove(lazy.targets, pixi.GpuStencilSystem, lazy.pipeline);
13491
+ pixi.extensions.add(pixi.GpuRenderTargetSystem, pixi.GpuStencilSystem, pixi.PipelineSystem);
13492
+ };
13493
+ }
13494
+ //#endregion
12848
13495
  //#region src/plugins/renderer/host/resolution.ts
13496
+ /** The short side, in CSS pixels, at or under which a coarse pointer is a phone: the rule of `effects.phone`. */
13497
+ const PHONE_SIDE$1 = 820;
12849
13498
  /**
12850
13499
  * Device pixels per CSS pixel of the window; 1 without a window. Moved here from `host/init.ts`.
12851
13500
  *
@@ -12855,20 +13504,67 @@ function devicePixels() {
12855
13504
  return typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
12856
13505
  }
12857
13506
  /**
12858
- * The resolution to draw at: the override, else the device pixels under the cap.
13507
+ * The resolution to draw at: the override, else the device pixels under the cap at the render scale.
12859
13508
  *
12860
13509
  * @param state - The resolution state of the host.
12861
13510
  * @param state.dpr - Device pixels per CSS pixel.
12862
13511
  * @param state.cap - The cap of `maxResolution`.
12863
13512
  * @param state.override - The switch of `monitor.setResolution`, when set.
13513
+ * @param state.scale - The render scale of the adaptive frame rate.
12864
13514
  * @returns The target resolution.
12865
13515
  * @example
12866
13516
  * ```ts
12867
- * targetResolution({ dpr: 2.81, cap: 2, override: undefined }); // 2
13517
+ * targetResolution({ dpr: 2.81, cap: 2, override: undefined, scale: 0.75 }); // 1.5
12868
13518
  * ```
12869
13519
  */
12870
13520
  function targetResolution(state) {
12871
- return state.override ?? Math.min(state.dpr, state.cap);
13521
+ return state.override ?? Math.min(state.dpr, state.cap) * state.scale;
13522
+ }
13523
+ /**
13524
+ * Tells a phone: a coarse pointer and a short side of at most 820 CSS px, the rule `effects` reads for `phone: "auto"`.
13525
+ * False without a window.
13526
+ *
13527
+ * @returns True on a phone.
13528
+ */
13529
+ function isPhone() {
13530
+ if (typeof globalThis.matchMedia !== "function") return false;
13531
+ return globalThis.matchMedia("(pointer: coarse)").matches && Math.min(globalThis.innerWidth, globalThis.innerHeight) <= PHONE_SIDE$1;
13532
+ }
13533
+ /**
13534
+ * The lowest render scale the adaptive frame rate may choose on this device: the floor of `adaptiveScale` where its
13535
+ * `devices` take this one, otherwise 1, none.
13536
+ *
13537
+ * @param config - The resolved plugin config.
13538
+ * @returns The floor; 1 allows no scale.
13539
+ */
13540
+ function scaleFloorOf(config) {
13541
+ const { adaptiveScale } = config;
13542
+ if (adaptiveScale === false) return 1;
13543
+ return adaptiveScale.devices === "all" || isPhone() ? adaptiveScale.floor : 1;
13544
+ }
13545
+ /**
13546
+ * Follows the render scale `time:rate` carries: remembers it and asks for the target with reason `"rate"`. The
13547
+ * override still wins.
13548
+ *
13549
+ * @param state - The host branch of the plugin state.
13550
+ * @param scale - The render scale the adaptive frame rate chose.
13551
+ * @returns True when a change is now pending.
13552
+ */
13553
+ function followScale(state, scale) {
13554
+ if (scale === state.resolution.scale) return false;
13555
+ state.resolution.scale = scale;
13556
+ return askResolution(state, targetResolution(state.resolution), "rate");
13557
+ }
13558
+ /**
13559
+ * Lets the adaptive frame rate step the render scale down to the floor of this device, or to none while the override
13560
+ * holds, and takes the scale it answers: a scale it withdrew is 1 again.
13561
+ *
13562
+ * @param ctx - Domain context of the renderer plugin.
13563
+ */
13564
+ function allowRuleScale(ctx) {
13565
+ const { resolution } = ctx.state.host;
13566
+ const floor = resolution.override === void 0 ? resolution.floor : 1;
13567
+ resolution.scale = ctx.deps.time.allowScale(floor).scale;
12872
13568
  }
12873
13569
  /**
12874
13570
  * The one mechanism: remembers a resolution for the next frame. Nothing is applied here. While no
@@ -13461,7 +14157,8 @@ function createPoolState() {
13461
14157
  /**
13462
14158
  * Creates the initial host state: no Pixi, no application, nothing drawn, nothing listening, no
13463
14159
  * counting, no bitmap text pipe and no render-group pipe installed, the resolution of a page without a window (device
13464
- * pixels 1, cap 2, no override, nothing pending), no own GPU, no kept picture and no pool trim yet.
14160
+ * pixels 1, cap 2, no override, nothing pending, the whole scale, no scale allowed yet), no own GPU, no kept picture
14161
+ * and no pool trim yet.
13465
14162
  *
13466
14163
  * @returns The host branch of the plugin state.
13467
14164
  */
@@ -13483,11 +14180,14 @@ function createHostState() {
13483
14180
  uninstallPasses: void 0,
13484
14181
  uninstallSdfText: void 0,
13485
14182
  uninstallRenderGroup: void 0,
14183
+ uninstallRenderPass: void 0,
13486
14184
  resolution: {
13487
14185
  dpr: 1,
13488
14186
  cap: 2,
13489
14187
  override: void 0,
13490
- pending: void 0
14188
+ pending: void 0,
14189
+ scale: 1,
14190
+ floor: 1
13491
14191
  },
13492
14192
  onResolutionChange: [],
13493
14193
  ownGpu: void 0,
@@ -13604,11 +14304,42 @@ function installRenderGroup(ctx, pixi) {
13604
14304
  }
13605
14305
  }
13606
14306
  /**
14307
+ * Swaps Pixi's WebGPU render target and pipeline systems for the renderer's, which begin the canvas's render pass at
14308
+ * its first draw and keep the canvas's stencil and depth only where something reads them (B3). Like the batched text it
14309
+ * runs before `new Application()`, once per host state, in every build. A Pixi without the pieces draws on with its own
14310
+ * systems, with a warning.
14311
+ *
14312
+ * @param ctx - Domain context of the renderer plugin.
14313
+ * @param pixi - The module the application is built from.
14314
+ */
14315
+ function installRenderPass(ctx, pixi) {
14316
+ const state = ctx.state.host;
14317
+ if (state.uninstallRenderPass !== void 0) return;
14318
+ try {
14319
+ state.uninstallRenderPass = installLazyCanvasPass(pixi);
14320
+ } catch (error) {
14321
+ ctx.log.warn("renderer: the canvas pass begins at render start", { error });
14322
+ }
14323
+ }
14324
+ /**
14325
+ * Tells whether a feature brings a filter that reads what was drawn under its view (`defineFilter({ backdrop: true })`).
14326
+ * Pixi's WebGL renderer copies that from its back buffer, and skips such a filter, with a warning, without one; the back
14327
+ * buffer costs a full-screen copy per drawn frame, so it is asked for only then. WebGPU copies from the canvas and
14328
+ * ignores the option.
14329
+ *
14330
+ * @param features - Every feature of the game, its sub-features included.
14331
+ * @returns True when one of their filters is a backdrop.
14332
+ */
14333
+ function wantsBackBuffer(features) {
14334
+ for (const feature of features) for (const entry of feature.description.filters ?? []) if ("backdrop" in entry.filter && entry.filter.backdrop === true) return true;
14335
+ return false;
14336
+ }
14337
+ /**
13607
14338
  * Loads Pixi once, creates the application and appends its canvas, at the target resolution of
13608
14339
  * now: a restore after a lost device comes back at it, override included. Pixi picks WebGL by
13609
14340
  * itself when WebGPU is missing, so only a device with neither ends up in the catch of the
13610
14341
  * caller. An agent build marks the Pixi load and the init on the page's timeline and hands Pixi
13611
- * the engine's own device.
14342
+ * the engine's own device. A game with a backdrop filter gets Pixi's WebGL back buffer.
13612
14343
  *
13613
14344
  * @param ctx - Domain context of the renderer plugin.
13614
14345
  * @param mount - Where the canvas goes.
@@ -13625,6 +14356,7 @@ async function createApplication(ctx, mount, deps) {
13625
14356
  installTextBatch(ctx, pixi);
13626
14357
  installCounting$1(ctx, pixi, deps);
13627
14358
  installRenderGroup(ctx, pixi);
14359
+ installRenderPass(ctx, pixi);
13628
14360
  state.resolution.dpr = devicePixels();
13629
14361
  state.resolution.cap = ctx.config.maxResolution;
13630
14362
  state.resolution.pending = void 0;
@@ -13636,7 +14368,8 @@ async function createApplication(ctx, mount, deps) {
13636
14368
  resolution: targetResolution(state.resolution),
13637
14369
  autoDensity: true,
13638
14370
  autoStart: false,
13639
- sharedTicker: false
14371
+ sharedTicker: false,
14372
+ useBackBuffer: wantsBackBuffer(ctx.deps.flow.features.all())
13640
14373
  };
13641
14374
  await (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? initTimed(ctx, app, options) : app.init(options));
13642
14375
  state.app = app;
@@ -13668,8 +14401,8 @@ function destroyApplication(state) {
13668
14401
  /**
13669
14402
  * Undoes everything `init` created. Works on the state alone, because `onStop` has no context.
13670
14403
  * `ready` drops first, so a device that goes away during the teardown is ignored. The draw
13671
- * counter, the batched text and the render-group pipe swap Pixi's own classes back, so the next application on
13672
- * the page starts from them. The pool trim starts over and its listeners go; Pixi's pool itself is left.
14404
+ * counter, the batched text, the render-group pipe and the lazy canvas pass swap Pixi's own classes back, so the next
14405
+ * application on the page starts from them. The pool trim starts over and its listeners go; Pixi's pool itself is left.
13673
14406
  *
13674
14407
  * @param state - The host branch of the plugin state.
13675
14408
  */
@@ -13687,6 +14420,8 @@ function stopHost(state) {
13687
14420
  state.uninstallSdfText = void 0;
13688
14421
  state.uninstallRenderGroup?.();
13689
14422
  state.uninstallRenderGroup = void 0;
14423
+ state.uninstallRenderPass?.();
14424
+ state.uninstallRenderPass = void 0;
13690
14425
  state.unsupported?.remove();
13691
14426
  state.unsupported = void 0;
13692
14427
  state.onReady.length = 0;
@@ -14199,6 +14934,7 @@ function createHostApi(ctx, deps) {
14199
14934
  setResolution: (value, reason) => askResolution(state, value, reason),
14200
14935
  overrideResolution: (value) => {
14201
14936
  state.resolution.override = value;
14937
+ allowRuleScale(ctx);
14202
14938
  refreshResolution(ctx, "debug");
14203
14939
  return targetResolution(state.resolution);
14204
14940
  },
@@ -14256,6 +14992,8 @@ function createHostApi(ctx, deps) {
14256
14992
  watchDevice(ctx, restart);
14257
14993
  watchVisibility(ctx);
14258
14994
  watchPixelRatio(ctx);
14995
+ state.resolution.floor = scaleFloorOf(ctx.config);
14996
+ allowRuleScale(ctx);
14259
14997
  state.ready = true;
14260
14998
  for (const fn of state.onReady) fn();
14261
14999
  }
@@ -29683,9 +30421,9 @@ function createRendererApi(ctx) {
29683
30421
  //#region src/plugins/renderer/handlers.ts
29684
30422
  /**
29685
30423
  * @file renderer plugin — the hooks: a pause hands the captures still waiting for a drawn frame
29686
- * their picture, since no frame comes while the clock is paused, and trims Pixi's texture pool. In an agent build the
29687
- * performance monitor also hears the pause stack, the flow path, the thermal readings and the
29688
- * changes of the frame rate.
30424
+ * their picture, since no frame comes while the clock is paused, and trims Pixi's texture pool; a change of the frame
30425
+ * rate brings the render scale the adaptive rule chose. In an agent build the performance monitor also hears the pause
30426
+ * stack, the flow path, the thermal readings, the changes of the frame rate and of the scale.
29689
30427
  */
29690
30428
  /**
29691
30429
  * Creates the hooks of the renderer. `time` is resolved when a hook fires, not while this factory
@@ -29743,13 +30481,23 @@ function createHandlers$5(ctx) {
29743
30481
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) monitorOf().thermal(reading);
29744
30482
  },
29745
30483
  /**
29746
- * Writes a change of the frame rate as a `rate` event line, the payload as its detail. Agent
29747
- * builds only.
30484
+ * Draws at the render scale the change carries from the next frame, in every build. An agent build writes the
30485
+ * change as a `rate` event line, the payload as its detail, and a move of the scale as a `scale` line.
29748
30486
  *
29749
30487
  * @param change - The change `time:rate` carried.
29750
30488
  */
29751
30489
  "time:rate": (change) => {
29752
- if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) monitorOf().event("rate", change);
30490
+ const from = ctx.state.host.resolution.scale;
30491
+ followScale(ctx.state.host, change.scale);
30492
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) {
30493
+ monitorOf().event("rate", change);
30494
+ if (change.scale !== from) monitorOf().event("scale", {
30495
+ from,
30496
+ to: change.scale,
30497
+ reason: change.reason,
30498
+ fps: change.to
30499
+ });
30500
+ }
29753
30501
  }
29754
30502
  };
29755
30503
  }
@@ -29974,12 +30722,28 @@ function checkPoolTrim(poolTrim) {
29974
30722
  if (!isCount(aboveMb)) throw new Error(`[game] renderer.poolTrim.aboveMb is ${String(aboveMb)}.\n Give the MiB the targets must grow by, 0 or more.`);
29975
30723
  }
29976
30724
  /**
30725
+ * Checks the adaptive render scale of the config: `false`, or a floor that is a step of the scale and the devices it
30726
+ * applies to.
30727
+ *
30728
+ * @param adaptiveScale - What the config holds.
30729
+ * @throws {Error} When it is neither `false` nor an object, the floor is not 1, 0.85, 0.75, 0.6 or 0.5, or the
30730
+ * devices are neither `"phone"` nor `"all"`.
30731
+ */
30732
+ function checkAdaptiveScale(adaptiveScale) {
30733
+ if (adaptiveScale === false) return;
30734
+ if (typeof adaptiveScale !== "object" || adaptiveScale === null) throw new Error(`[game] renderer.adaptiveScale is ${String(adaptiveScale)}.\n Give { floor, devices } or false.`);
30735
+ const floor = "floor" in adaptiveScale ? adaptiveScale.floor : void 0;
30736
+ const devices = "devices" in adaptiveScale ? adaptiveScale.devices : void 0;
30737
+ if (typeof floor !== "number" || !SCALE_STEPS.includes(floor)) throw new Error(`[game] renderer.adaptiveScale.floor is ${String(floor)}.\n Give a step of the scale: 1, 0.85, 0.75, 0.6 or 0.5.`);
30738
+ if (devices !== "phone" && devices !== "all") throw new Error(`[game] renderer.adaptiveScale.devices is ${String(devices)}.\n Give "phone" or "all".`);
30739
+ }
30740
+ /**
29977
30741
  * Checks the numbers of the config a game can get wrong, at `createApp`, in every build: the
29978
- * resolution cap, the pool trim, the performance monitor, the budget of the kept pictures and the
29979
- * probe draws of the warm-up.
30742
+ * resolution cap, the pool trim, the adaptive render scale, the performance monitor, the budget of the kept pictures
30743
+ * and the probe draws of the warm-up.
29980
30744
  *
29981
30745
  * @param config - The resolved plugin config.
29982
- * @throws {Error} When `maxResolution` is not a number above 0, `poolTrim` fails its check,
30746
+ * @throws {Error} When `maxResolution` is not a number above 0, `poolTrim` or `adaptiveScale` fails its check,
29983
30747
  * `monitor.budgetMs` is neither `"rate"` nor a number above 0, `monitor.frames` is not a whole
29984
30748
  * number from 64 to 16384, `keptMb` is not a finite number of at least 0, or `warm` is not a
29985
30749
  * whole number from 0 to 16.
@@ -29989,6 +30753,7 @@ function checkConfig(config) {
29989
30753
  const { budgetMs, frames } = config.monitor;
29990
30754
  if (!Number.isFinite(maxResolution) || maxResolution <= 0) throw new Error(`[game] renderer.maxResolution is ${maxResolution}.\n Give a number above 0.`);
29991
30755
  checkPoolTrim(config.poolTrim);
30756
+ checkAdaptiveScale(config.adaptiveScale);
29992
30757
  if (budgetMs !== "rate" && !(Number.isFinite(budgetMs) && budgetMs > 0)) throw new Error(`[game] renderer.monitor.budgetMs is ${String(budgetMs)}.\n Give the frame budget in milliseconds, or "rate" for one frame of the current rate.`);
29993
30758
  if (!Number.isInteger(frames) || frames < RING_FRAMES.min || frames > RING_FRAMES.max) throw new Error(`[game] renderer.monitor.frames is ${frames}.\n Give a whole number from 64 to 16384.`);
29994
30759
  if (typeof keptMb !== "number" || !Number.isFinite(keptMb) || keptMb < 0) throw new Error(`[game] renderer.keptMb is ${String(keptMb)}.\n Give the MiB kept pictures may hold, or 0 to keep none.`);
@@ -30074,7 +30839,11 @@ const rendererPlugin = /*#__PURE__*/ createPlugin$1("renderer", {
30074
30839
  },
30075
30840
  onDemand: false,
30076
30841
  keptMb: 64,
30077
- warm: 1
30842
+ warm: 1,
30843
+ adaptiveScale: {
30844
+ floor: .75,
30845
+ devices: "phone"
30846
+ }
30078
30847
  },
30079
30848
  events: (register) => register.map({ "renderer:device-lost": "The GPU device or context was lost; the game is paused" }),
30080
30849
  createState: createRendererState,
@@ -35880,7 +36649,7 @@ function leavingOf(previous, scene) {
35880
36649
  * Builds the scene in one synchronous block: the layers, then the projections only the old scene
35881
36650
  * lists out, then the list of the new scene in (a projection both list stays and takes its new
35882
36651
  * place), the new scene to the world's systems, then the list the scene names to `assets`, then
35883
- * the frame loop out of its idle cap, and only then the event.
36652
+ * the frame loop out of its idle cap and the adaptive frame rate told of the new screen, and only then the event.
35884
36653
  *
35885
36654
  * @param ctx - Domain context of the plugin.
35886
36655
  * @param scene - The scene whose bundles are there.
@@ -35899,6 +36668,7 @@ function apply(ctx, scene, named) {
35899
36668
  state.named = named;
35900
36669
  ctx.deps.assets.sceneBundles(named);
35901
36670
  ctx.deps.time.wake();
36671
+ ctx.deps.time.sceneChanged();
35902
36672
  ctx.emit("scenes:changed", {
35903
36673
  from,
35904
36674
  to: scene.id,
@@ -37645,96 +38415,6 @@ function createEffectsApi(ctx) {
37645
38415
  };
37646
38416
  }
37647
38417
  //#endregion
37648
- //#region src/plugins/effects/filters/glsl.ts
37649
- /**
37650
- * The head of every filter's GLSL. `#version 300 es` comes first, so Pixi compiles both stages
37651
- * as GLSL ES 3.0 and the dev check compiles the same text; `highp` matches the `f32` of the WGSL
37652
- * twin and the precision of the uniforms the vertex stage shares. Then the texture coordinate of
37653
- * `defaultFilterVert`, the output, the input texture and the global filter uniforms as `vec4`.
37654
- */
37655
- const GLSL_HEADER = `#version 300 es
37656
- precision highp float;
37657
-
37658
- in vec2 vTextureCoord;
37659
- out vec4 finalColor;
37660
-
37661
- uniform sampler2D uTexture;
37662
- uniform vec4 uInputSize;
37663
- uniform vec4 uInputPixel;
37664
- uniform vec4 uInputClamp;
37665
- uniform vec4 uOutputFrame;
37666
- uniform vec4 uGlobalFrame;
37667
- uniform vec4 uOutputTexture;
37668
-
37669
- `;
37670
- /** The GLSL type of each uniform type: a number is a `float`, a colour or a tuple a vector. */
37671
- const GLSL_TYPES = {
37672
- f32: "float",
37673
- "vec2<f32>": "vec2",
37674
- "vec3<f32>": "vec3",
37675
- "vec4<f32>": "vec4"
37676
- };
37677
- /**
37678
- * One `uniform` line per uniform, in declaration order, under the bare names the group keys.
37679
- * Nothing without uniforms.
37680
- *
37681
- * @param uniforms - The uniforms in declaration order.
37682
- * @returns The GLSL lines, or `""`.
37683
- * @example
37684
- * ```ts
37685
- * glslUniforms([{ name: "color", type: "vec3<f32>", form: "color", size: 3 }]); // "uniform vec3 color;\n\n"
37686
- * ```
37687
- */
37688
- function glslUniforms(uniforms) {
37689
- if (uniforms.length === 0) return "";
37690
- return `${uniforms.map((uniform) => `uniform ${GLSL_TYPES[uniform.type]} ${uniform.name};\n`).join("")}\n`;
37691
- }
37692
- /**
37693
- * The engine's half of each input in GLSL: the texture, and the function that reads it at the body's uv, its scale
37694
- * and its clamp applied. The two uniforms it reads are declared before it by `glslUniforms`. Nothing without inputs.
37695
- *
37696
- * @param inputs - The inputs in declaration order.
37697
- * @returns The GLSL lines, or `""`.
37698
- * @example
37699
- * ```ts
37700
- * glslInputs([blurInput]).startsWith("uniform sampler2D blurTexture;"); // true
37701
- * ```
37702
- */
37703
- function glslInputs(inputs) {
37704
- return inputs.map((input) => `uniform sampler2D ${input.texture};\n\nvec4 ${input.sample}(vec2 uv) {\n return textureLod(${input.texture}, clamp(uv * uInputSize.xy * ${input.scale.name}, ${input.clamp.name}.xy, ${input.clamp.name}.zw), 0.0);\n}
37705
-
37706
- `).join("");
37707
- }
37708
- /** The GLSL twin of `canvasPixel(uv)`: the pixel's canvas position in CSS px (spec 23 P8.3). */
37709
- const GLSL_CANVAS_PIXEL = `vec2 canvasPixel(vec2 uv) {
37710
- return uOutputFrame.xy + keptOrigin + uv * uInputSize.xy;
37711
- }
37712
-
37713
- `;
37714
- /**
37715
- * The whole GLSL of a filter: the header, the uniforms (then the scale and the clamp of each input, then the
37716
- * engine's `keptOrigin` of a body that calls `canvasPixel`), the inputs, the helper, the body.
37717
- *
37718
- * @param uniforms - The uniforms in declaration order.
37719
- * @param inputs - The inputs in declaration order.
37720
- * @param body - The fragment body the game wrote.
37721
- * @param origin - The `keptOrigin` uniform, for a body that calls `canvasPixel`.
37722
- * @returns The GLSL fragment source the `GlProgram` is built from.
37723
- * @example
37724
- * ```ts
37725
- * assembleGlsl([], [], body).endsWith(body); // true
37726
- * ```
37727
- */
37728
- function assembleGlsl(uniforms, inputs, body, origin) {
37729
- const declared = glslUniforms([
37730
- ...uniforms,
37731
- ...inputs.flatMap((input) => [input.scale, input.clamp]),
37732
- ...origin === void 0 ? [] : [origin]
37733
- ]);
37734
- const helper = origin === void 0 ? "" : GLSL_CANVAS_PIXEL;
37735
- return `${GLSL_HEADER}${declared}${glslInputs(inputs)}${helper}${body}`;
37736
- }
37737
- //#endregion
37738
38418
  //#region src/plugins/effects/filters/wgsl.ts
37739
38419
  /**
37740
38420
  * The head of every filter shader: the global filter uniforms and the input texture and sampler
@@ -37811,30 +38491,165 @@ const CANVAS_PIXEL = `fn canvasPixel(uv: vec2<f32>) -> vec2<f32> {
37811
38491
  return gfu.uOutputFrame.xy + fu.keptOrigin + uv * gfu.uInputSize.xy;
37812
38492
  }
37813
38493
 
38494
+ `;
38495
+ /**
38496
+ * The engine's uniform a backdrop's `sampleBack` scales its uv by: one over the size of the back texture's backing,
38497
+ * CSS px. Filled by every apply from the texture Pixi copied.
38498
+ */
38499
+ const BACK_SCALE = Object.freeze({
38500
+ name: "backScale",
38501
+ type: "vec2<f32>",
38502
+ form: "vector",
38503
+ size: 2
38504
+ });
38505
+ /** The engine's uniform a backdrop's `sampleBack` clamps to: half a texel inside the copied part of the back texture. */
38506
+ const BACK_CLAMP = Object.freeze({
38507
+ name: "backClamp",
38508
+ type: "vec4<f32>",
38509
+ form: "vector",
38510
+ size: 4
38511
+ });
38512
+ /**
38513
+ * What a backdrop's body reads what was drawn under its view through: the texture Pixi's filter system copies the
38514
+ * canvas into and binds at group 0 binding 3 (`Filter.blendRequired`), sampled at the body's uv with the engine's
38515
+ * scale and clamp, as an input is.
38516
+ */
38517
+ const SAMPLE_BACK = `@group(0) @binding(3) var uBackTexture: texture_2d<f32>;
38518
+
38519
+ fn sampleBack(uv: vec2<f32>) -> vec4<f32> {
38520
+ return textureSampleLevel(uBackTexture, uSampler, clamp(uv * gfu.uInputSize.xy * fu.backScale, fu.backClamp.xy, fu.backClamp.zw), 0.0);
38521
+ }
38522
+
37814
38523
  `;
37815
38524
  /**
37816
38525
  * The whole source of a filter: the header, the uniform struct (the uniforms, then the scale and the clamp of each
37817
- * input, then the engine's `keptOrigin` of a body that calls `canvasPixel`), the input bindings, the helper, the
37818
- * body. A body without the helper assembles byte-identically to one without the cache.
38526
+ * input, then a backdrop's back scale and clamp, then the engine's `keptOrigin` of a body that calls `canvasPixel`),
38527
+ * the input bindings, a backdrop's `sampleBack`, the `canvasPixel` helper, the body. A body without either helper
38528
+ * assembles byte-identically to one without the cache and without the backdrop.
37819
38529
  *
37820
38530
  * @param uniforms - The uniforms in declaration order.
37821
38531
  * @param inputs - The inputs in declaration order.
37822
38532
  * @param body - The fragment body the game wrote.
37823
38533
  * @param origin - The `keptOrigin` uniform, for a body that calls `canvasPixel`.
38534
+ * @param backdrop - True for a filter that reads what was drawn under its view.
37824
38535
  * @returns The WGSL source both stages are built from.
37825
38536
  * @example
37826
38537
  * ```ts
37827
- * assemble([], [], body).endsWith(body); // true
38538
+ * assemble([], [], body, undefined, true).includes("fn sampleBack(uv: vec2<f32>) -> vec4<f32>"); // true
37828
38539
  * ```
37829
38540
  */
37830
- function assemble(uniforms, inputs, body, origin) {
38541
+ function assemble(uniforms, inputs, body, origin, backdrop = false) {
37831
38542
  const struct = uniformStruct([
37832
38543
  ...uniforms,
37833
38544
  ...inputs.flatMap((input) => [input.scale, input.clamp]),
38545
+ ...backdrop ? [BACK_SCALE, BACK_CLAMP] : [],
37834
38546
  ...origin === void 0 ? [] : [origin]
37835
38547
  ]);
38548
+ const back = backdrop ? SAMPLE_BACK : "";
37836
38549
  const helper = origin === void 0 ? "" : CANVAS_PIXEL;
37837
- return `${HEADER}${struct}${inputBindings(inputs)}${helper}${body}`;
38550
+ return `${HEADER}${struct}${inputBindings(inputs)}${back}${helper}${body}`;
38551
+ }
38552
+ //#endregion
38553
+ //#region src/plugins/effects/filters/glsl.ts
38554
+ /**
38555
+ * The head of every filter's GLSL. `#version 300 es` comes first, so Pixi compiles both stages
38556
+ * as GLSL ES 3.0 and the dev check compiles the same text; `highp` matches the `f32` of the WGSL
38557
+ * twin and the precision of the uniforms the vertex stage shares. Then the texture coordinate of
38558
+ * `defaultFilterVert`, the output, the input texture and the global filter uniforms as `vec4`.
38559
+ */
38560
+ const GLSL_HEADER = `#version 300 es
38561
+ precision highp float;
38562
+
38563
+ in vec2 vTextureCoord;
38564
+ out vec4 finalColor;
38565
+
38566
+ uniform sampler2D uTexture;
38567
+ uniform vec4 uInputSize;
38568
+ uniform vec4 uInputPixel;
38569
+ uniform vec4 uInputClamp;
38570
+ uniform vec4 uOutputFrame;
38571
+ uniform vec4 uGlobalFrame;
38572
+ uniform vec4 uOutputTexture;
38573
+
38574
+ `;
38575
+ /** The GLSL type of each uniform type: a number is a `float`, a colour or a tuple a vector. */
38576
+ const GLSL_TYPES = {
38577
+ f32: "float",
38578
+ "vec2<f32>": "vec2",
38579
+ "vec3<f32>": "vec3",
38580
+ "vec4<f32>": "vec4"
38581
+ };
38582
+ /**
38583
+ * One `uniform` line per uniform, in declaration order, under the bare names the group keys.
38584
+ * Nothing without uniforms.
38585
+ *
38586
+ * @param uniforms - The uniforms in declaration order.
38587
+ * @returns The GLSL lines, or `""`.
38588
+ * @example
38589
+ * ```ts
38590
+ * glslUniforms([{ name: "color", type: "vec3<f32>", form: "color", size: 3 }]); // "uniform vec3 color;\n\n"
38591
+ * ```
38592
+ */
38593
+ function glslUniforms(uniforms) {
38594
+ if (uniforms.length === 0) return "";
38595
+ return `${uniforms.map((uniform) => `uniform ${GLSL_TYPES[uniform.type]} ${uniform.name};\n`).join("")}\n`;
38596
+ }
38597
+ /**
38598
+ * The engine's half of each input in GLSL: the texture, and the function that reads it at the body's uv, its scale
38599
+ * and its clamp applied. The two uniforms it reads are declared before it by `glslUniforms`. Nothing without inputs.
38600
+ *
38601
+ * @param inputs - The inputs in declaration order.
38602
+ * @returns The GLSL lines, or `""`.
38603
+ * @example
38604
+ * ```ts
38605
+ * glslInputs([blurInput]).startsWith("uniform sampler2D blurTexture;"); // true
38606
+ * ```
38607
+ */
38608
+ function glslInputs(inputs) {
38609
+ return inputs.map((input) => `uniform sampler2D ${input.texture};\n\nvec4 ${input.sample}(vec2 uv) {\n return textureLod(${input.texture}, clamp(uv * uInputSize.xy * ${input.scale.name}, ${input.clamp.name}.xy, ${input.clamp.name}.zw), 0.0);\n}
38610
+
38611
+ `).join("");
38612
+ }
38613
+ /** The GLSL twin of `canvasPixel(uv)`: the pixel's canvas position in CSS px (spec 23 P8.3). */
38614
+ const GLSL_CANVAS_PIXEL = `vec2 canvasPixel(vec2 uv) {
38615
+ return uOutputFrame.xy + keptOrigin + uv * uInputSize.xy;
38616
+ }
38617
+
38618
+ `;
38619
+ /** The GLSL twin of `sampleBack(uv)`: what was drawn under the view, at the body's uv, scaled and clamped. */
38620
+ const GLSL_SAMPLE_BACK = `uniform sampler2D uBackTexture;
38621
+
38622
+ vec4 sampleBack(vec2 uv) {
38623
+ return textureLod(uBackTexture, clamp(uv * uInputSize.xy * backScale, backClamp.xy, backClamp.zw), 0.0);
38624
+ }
38625
+
38626
+ `;
38627
+ /**
38628
+ * The whole GLSL of a filter: the header, the uniforms (then the scale and the clamp of each input, then a backdrop's
38629
+ * back scale and clamp, then the engine's `keptOrigin` of a body that calls `canvasPixel`), the inputs, a backdrop's
38630
+ * `sampleBack`, the `canvasPixel` helper, the body.
38631
+ *
38632
+ * @param uniforms - The uniforms in declaration order.
38633
+ * @param inputs - The inputs in declaration order.
38634
+ * @param body - The fragment body the game wrote.
38635
+ * @param origin - The `keptOrigin` uniform, for a body that calls `canvasPixel`.
38636
+ * @param backdrop - True for a filter that reads what was drawn under its view.
38637
+ * @returns The GLSL fragment source the `GlProgram` is built from.
38638
+ * @example
38639
+ * ```ts
38640
+ * assembleGlsl([], [], body, undefined, true).includes("uniform sampler2D uBackTexture;"); // true
38641
+ * ```
38642
+ */
38643
+ function assembleGlsl(uniforms, inputs, body, origin, backdrop = false) {
38644
+ const declared = glslUniforms([
38645
+ ...uniforms,
38646
+ ...inputs.flatMap((input) => [input.scale, input.clamp]),
38647
+ ...backdrop ? [BACK_SCALE, BACK_CLAMP] : [],
38648
+ ...origin === void 0 ? [] : [origin]
38649
+ ]);
38650
+ const back = backdrop ? GLSL_SAMPLE_BACK : "";
38651
+ const helper = origin === void 0 ? "" : GLSL_CANVAS_PIXEL;
38652
+ return `${GLSL_HEADER}${declared}${glslInputs(inputs)}${back}${helper}${body}`;
37838
38653
  }
37839
38654
  //#endregion
37840
38655
  //#region src/plugins/effects/filters/define.ts
@@ -37859,9 +38674,10 @@ const MAIN_GLSL = /\bvoid\s+main\s*\(/;
37859
38674
  /**
37860
38675
  * Names a uniform cannot take: the two fields every filter component carries, and the names the
37861
38676
  * shader headers declare, which a game uniform would clash with (on WebGL only, for most). The
37862
- * GLSL header also declares `finalColor` and `vTextureCoord`.
38677
+ * GLSL header also declares `finalColor` and `vTextureCoord`; a backdrop's, `uBackTexture`,
38678
+ * `backScale`, `backClamp` and `sampleBack`.
37863
38679
  */
37864
- const RESERVED_NAME = /^(?:enabled|order|uInputSize|uInputPixel|uInputClamp|uOutputFrame|uGlobalFrame|uOutputTexture|uTexture|uSampler|finalColor|vTextureCoord|keptOrigin|canvasPixel)$/;
38680
+ const RESERVED_NAME = /^(?:enabled|order|uInputSize|uInputPixel|uInputClamp|uOutputFrame|uGlobalFrame|uOutputTexture|uTexture|uSampler|finalColor|vTextureCoord|keptOrigin|canvasPixel|uBackTexture|backScale|backClamp|sampleBack)$/;
37865
38681
  /**
37866
38682
  * What a body reads of the screen: the root target's frame, the fragment position, the output target's size, the
37867
38683
  * origin of the output frame (its size `.zw` is the same in a picture and live).
@@ -37945,21 +38761,22 @@ function worstOf(a, b) {
37945
38761
  }
37946
38762
  /**
37947
38763
  * What the kept-picture cache may do with a filter, from what the game declared and what its bodies read only, never
37948
- * the engine's header (LEAD-FIXES-4 item 3): `cache: false` is `"screen"`; a body that reads the screen and declares
37949
- * nothing is `"screen"` too (the safe direction); a body that calls `canvasPixel` is `"placed"`, as is
37950
- * `cache: "placed"`; everything else is `"local"`.
38764
+ * the engine's header (LEAD-FIXES-4 item 3): a backdrop and `cache: false` are `"screen"`; a body that reads the
38765
+ * screen and declares nothing is `"screen"` too (the safe direction); a body that calls `canvasPixel` is `"placed"`,
38766
+ * as is `cache: "placed"`; everything else is `"local"`.
37951
38767
  *
37952
38768
  * @param wgsl - The WGSL body.
37953
38769
  * @param glsl - The GLSL body.
37954
38770
  * @param cache - What the game declared.
38771
+ * @param backdrop - Whether the filter reads what was drawn under its view.
37955
38772
  * @returns The category.
37956
38773
  * @example
37957
38774
  * ```ts
37958
- * cacheOf("let p = gfu.uOutputFrame.xy + uv * gfu.uInputSize.xy;", "", undefined); // "screen"
38775
+ * cacheOf("let p = gfu.uOutputFrame.xy + uv * gfu.uInputSize.xy;", "", undefined, false); // "screen"
37959
38776
  * ```
37960
38777
  */
37961
- function cacheOf(wgsl, glsl, cache) {
37962
- if (cache === false) return "screen";
38778
+ function cacheOf(wgsl, glsl, cache, backdrop) {
38779
+ if (cache === false || backdrop) return "screen";
37963
38780
  if (cache === void 0 && (readsScreenIn(wgsl) || readsScreenIn(glsl))) return "screen";
37964
38781
  if (cache === "placed" || callsCanvasPixel(wgsl, glsl)) return "placed";
37965
38782
  return "local";
@@ -38026,6 +38843,48 @@ function readResolution(id, resolution) {
38026
38843
  throw new Error(`[game] Filter "${id}": resolution must be from 0 to 4, 0 for the resolution of the canvas.\n Got ${String(resolution)}.`);
38027
38844
  }
38028
38845
  /**
38846
+ * Reads the declared backdrop switch: `true`, `false` or left out. A JavaScript caller may pass anything.
38847
+ *
38848
+ * @param id - The filter id.
38849
+ * @param backdrop - What the game declared.
38850
+ * @returns True for a backdrop.
38851
+ * @throws {Error} When it is given and is not a boolean.
38852
+ */
38853
+ function readBackdrop(id, backdrop) {
38854
+ if (backdrop === void 0 || typeof backdrop === "boolean") return backdrop === true;
38855
+ throw new Error(`[game] Filter "${id}": backdrop must be true or false.\n Got ${JSON.stringify(backdrop)}.`);
38856
+ }
38857
+ /**
38858
+ * Refuses what a backdrop cannot have: a padding (Pixi copies the canvas under the padded box, and a copy past the
38859
+ * canvas edge fails on the GPU) and a cache that keeps it (what is under it changes without it).
38860
+ *
38861
+ * @param id - The filter id.
38862
+ * @param padding - The padding as declared, `0` when left out.
38863
+ * @param cache - The cache as declared.
38864
+ * @throws {Error} When the padding is not 0 or the cache keeps it.
38865
+ */
38866
+ function checkBackdrop(id, padding, cache) {
38867
+ if (padding !== 0) throw new Error(`[game] Filter "${id}" is a backdrop: its padding must be 0.\n Pixi copies the canvas under the padded box, and a copy past the canvas edge fails: leave padding out.`);
38868
+ if (cache === "local" || cache === "placed") throw new Error(`[game] Filter "${id}" is a backdrop: it reads what is drawn under its view, so it is drawn live in every frame.\n Leave cache out, or set it to false.`);
38869
+ }
38870
+ /**
38871
+ * Reads where an input's first step reads: the view's own input (the default) or, for a backdrop, what was drawn under
38872
+ * the view. A JavaScript caller may pass anything.
38873
+ *
38874
+ * @param id - The filter id.
38875
+ * @param name - The input name.
38876
+ * @param from - What the game declared.
38877
+ * @param backdrop - Whether the filter is a backdrop.
38878
+ * @returns `"input"` or `"back"`.
38879
+ * @throws {Error} When it is neither, or `"back"` on a filter that is no backdrop.
38880
+ */
38881
+ function readFrom(id, name, from, backdrop) {
38882
+ if (from === void 0 || from === "input") return "input";
38883
+ if (from !== "back") throw new Error(`[game] Filter "${id}": input "${name}" starts from ${JSON.stringify(from)}.\n Write from: "input" or from: "back".`);
38884
+ if (!backdrop) throw new Error(`[game] Filter "${id}": input "${name}" starts from "back", which only a backdrop filter can read.\n Give the filter backdrop: true.`);
38885
+ return "back";
38886
+ }
38887
+ /**
38029
38888
  * Tells whether a value is a list of 2 to 4 finite numbers.
38030
38889
  *
38031
38890
  * @param value - A uniform declaration.
@@ -38206,21 +39065,23 @@ function readStages(id, name, stages) {
38206
39065
  for (const [index, stage] of stages.entries()) {
38207
39066
  if (!isDefinedFilter(stage)) throw new Error(`[game] Filter "${id}": stage ${index} of input "${name}" is not a filter made by defineFilter.\n List what defineFilter returned; a built-in kind cannot be a stage.`);
38208
39067
  if (!drawsAlone(stage.filter)) throw new Error(`[game] Filter "${id}": stage "${stage.filter.id}" of input "${name}" has inputs or a fallback of its own.\n A stage draws alone: give it neither.`);
39068
+ if (stage.filter.backdrop) throw new Error(`[game] Filter "${id}": stage "${stage.filter.id}" of input "${name}" is a backdrop.\n A stage reads the step before it: give it no backdrop.`);
38209
39069
  if (!read.includes(stage)) read.push(stage);
38210
39070
  }
38211
39071
  return Object.freeze(read);
38212
39072
  }
38213
39073
  /**
38214
- * Reads the inputs of a definition: the names the engine declares for each, its two uniforms, its
38215
- * stages and its chain.
39074
+ * Reads the inputs of a definition: the names the engine declares for each, where its first step reads, its two
39075
+ * uniforms, its stages and its chain.
38216
39076
  *
38217
39077
  * @param id - The filter id.
38218
39078
  * @param declared - What the game declared under `inputs`.
38219
39079
  * @param uniforms - The read uniforms.
39080
+ * @param backdrop - Whether the filter is a backdrop: only then may an input start from back.
38220
39081
  * @returns The inputs in declaration order, frozen; empty without `inputs`.
38221
39082
  * @throws {Error} When `inputs` is not an object, has more than four entries, or an input breaks one of its rules.
38222
39083
  */
38223
- function readInputs(id, declared, uniforms) {
39084
+ function readInputs(id, declared, uniforms, backdrop) {
38224
39085
  if (declared === void 0) return Object.freeze([]);
38225
39086
  if (typeof declared !== "object" || declared === null || Array.isArray(declared)) throw new Error(`[game] Filter "${id}": inputs must be an object of named inputs.\n Write inputs: { blur: { stages: [...], chain } }.`);
38226
39087
  const entries = Object.entries(declared);
@@ -38228,10 +39089,12 @@ function readInputs(id, declared, uniforms) {
38228
39089
  const taken = /* @__PURE__ */ new Set();
38229
39090
  return Object.freeze(entries.map(([name, input]) => {
38230
39091
  const names = checkInputNames(id, name, uniforms, taken);
39092
+ const from = readFrom(id, name, input.from, backdrop);
38231
39093
  const stages = readStages(id, name, input.stages);
38232
39094
  if (typeof input.chain !== "function") throw new TypeError(`[game] Filter "${id}": input "${name}" needs a chain.\n Write chain: (value, { resolution }) => [{ stage, scale }].`);
38233
39095
  return Object.freeze({
38234
39096
  name,
39097
+ from,
38235
39098
  texture: names.texture,
38236
39099
  sampler: names.sampler,
38237
39100
  scale: Object.freeze({
@@ -38254,21 +39117,23 @@ function readInputs(id, declared, uniforms) {
38254
39117
  }
38255
39118
  /**
38256
39119
  * Reads the fallback of a definition: another filter of ours, drawing alone, over a subset of its
38257
- * uniforms with the same types.
39120
+ * uniforms with the same types; a backdrop only for a backdrop, which it draws in place of on the same view.
38258
39121
  *
38259
39122
  * @param id - The filter id.
38260
39123
  * @param fallback - What the game declared under `fallback`.
38261
39124
  * @param uniforms - The read uniforms.
39125
+ * @param backdrop - Whether the filter is a backdrop.
38262
39126
  * @returns The fallback's definition, or `undefined` without one.
38263
- * @throws {Error} When it is no filter of ours, has the same id, has inputs or a fallback, or a uniform of it is not
38264
- * one of this filter's with the same type.
39127
+ * @throws {Error} When it is no filter of ours, has the same id, has inputs or a fallback, a uniform of it is not
39128
+ * one of this filter's with the same type, or it is a backdrop and the filter is not.
38265
39129
  */
38266
- function readFallback(id, fallback, uniforms) {
39130
+ function readFallback(id, fallback, uniforms, backdrop) {
38267
39131
  if (fallback === void 0) return void 0;
38268
39132
  if (!isDefinedFilter(fallback)) throw new Error(`[game] Filter "${id}": fallback is not a filter made by defineFilter.\n Pass what defineFilter returned.`);
38269
39133
  const definition = fallback.filter;
38270
39134
  if (definition.id === id) throw new Error(`[game] Filter "${id}": its fallback has the same id.\n Give the fallback an id of its own.`);
38271
39135
  if (!drawsAlone(definition)) throw new Error(`[game] Filter "${id}": its fallback "${definition.id}" has inputs or a fallback of its own.\n A fallback draws alone and simply: give it neither.`);
39136
+ if (definition.backdrop && !backdrop) throw new Error(`[game] Filter "${id}": its fallback "${definition.id}" is a backdrop, and "${id}" is not.\n A fallback draws in its place on the same view: give it no backdrop, or make "${id}" a backdrop too.`);
38272
39137
  for (const uniform of definition.uniforms) {
38273
39138
  const own = uniforms.find((candidate) => candidate.name === uniform.name);
38274
39139
  if (!(own?.type === uniform.type && own.form === uniform.form)) throw new Error(`[game] Filter "${id}": its fallback "${definition.id}" declares "${uniform.name}", which "${id}" ${own === void 0 ? "does not declare" : "declares as another type"}.\n A fallback draws with the value of ${id}: use a subset of its uniforms.`);
@@ -38318,18 +39183,20 @@ function cacheWithInputs(cache, inputs) {
38318
39183
  * An `area` answers, from the value, the rectangle in the entity's own units outside of which the
38319
39184
  * filter draws nothing: Pixi then renders and fills only that box, not the view's bounds. A
38320
39185
  * `resolution` draws the view and its filters at that many texels per CSS px instead of the
38321
- * canvas's.
38322
- * Register it through the `filters` key of a feature. The README shows a filter with a blur input
38323
- * and one with an `area`.
39186
+ * canvas's. A `backdrop` reads what was drawn under its view inside that box: the body calls
39187
+ * `sampleBack(uv)`, and an input may start `from: "back"`; it is drawn live in every frame.
39188
+ * Register it through the `filters` key of a feature. The README shows a filter with a blur input,
39189
+ * one with an `area` and a backdrop.
38324
39190
  *
38325
39191
  * @param id - The filter id, also the component name; unique, and not one of the built-ins.
38326
39192
  * @param spec - Both bodies, the uniforms, the passes one apply costs, the padding, the inputs, the fallback, the
38327
- * cache category, the area and the resolution.
39193
+ * cache category, the area, the resolution and the backdrop switch.
38328
39194
  * @returns The component type, with the frozen `filter` definition attached.
38329
39195
  * @throws {Error} When the id is empty, the WGSL has no `mainFragment`, the GLSL no `void main(`,
38330
39196
  * a uniform is reserved, badly named or of no known form, the passes or the padding break
38331
39197
  * their rules, an input or the fallback breaks one of its rules, `passes` is given with inputs,
38332
- * `area` is not a function, or `resolution` is not a number from 0 to 4.
39198
+ * `area` is not a function, `resolution` is not a number from 0 to 4, `backdrop` is not a
39199
+ * boolean, or a backdrop has a padding or a cache that keeps it.
38333
39200
  * @example
38334
39201
  * ```ts
38335
39202
  * // features/board/effects.ts: a gold tint a motion hook tweens on a merge, on WebGPU and WebGL.
@@ -38343,21 +39210,23 @@ function defineFilter(id, spec) {
38343
39210
  readSpec(id, spec);
38344
39211
  const read = Object.entries(spec.uniforms ?? {}).map(([name, declaration]) => readUniform(id, name, declaration));
38345
39212
  const uniforms = Object.freeze(read.map((entry) => entry.spec));
38346
- const inputs = readInputs(id, spec.inputs, uniforms);
39213
+ const backdrop = readBackdrop(id, spec.backdrop);
39214
+ const inputs = readInputs(id, spec.inputs, uniforms, backdrop);
38347
39215
  if (spec.inputs !== void 0 && spec.passes !== void 0) throw new Error(`[game] Filter "${id}": passes is counted by the engine for a filter with inputs.\n Leave passes out.`);
38348
- const fallback = readFallback(id, spec.fallback, uniforms);
39216
+ const fallback = readFallback(id, spec.fallback, uniforms, backdrop);
38349
39217
  const passes = spec.passes ?? 1;
38350
39218
  const padding = spec.padding ?? 0;
38351
39219
  checkCost(id, passes, padding, uniforms);
38352
39220
  checkCache(id, spec.cache);
39221
+ if (backdrop) checkBackdrop(id, padding, spec.cache);
38353
39222
  const area = readArea(id, spec.area);
38354
39223
  const resolution = readResolution(id, spec.resolution);
38355
39224
  const origin = callsCanvasPixel(spec.wgsl, spec.glsl) ? KEPT_ORIGIN : void 0;
38356
- const cache = cacheWithInputs(cacheOf(spec.wgsl, spec.glsl, spec.cache), inputs);
39225
+ const cache = cacheWithInputs(cacheOf(spec.wgsl, spec.glsl, spec.cache, backdrop), inputs);
38357
39226
  const filter = Object.freeze({
38358
39227
  id,
38359
- source: assemble(uniforms, inputs, spec.wgsl, origin),
38360
- glsl: assembleGlsl(uniforms, inputs, spec.glsl, origin),
39228
+ source: assemble(uniforms, inputs, spec.wgsl, origin, backdrop),
39229
+ glsl: assembleGlsl(uniforms, inputs, spec.glsl, origin, backdrop),
38361
39230
  uniforms,
38362
39231
  passes,
38363
39232
  padding,
@@ -38366,9 +39235,10 @@ function defineFilter(id, spec) {
38366
39235
  cache,
38367
39236
  origin,
38368
39237
  resolution,
39238
+ backdrop,
38369
39239
  ...area === void 0 ? {} : { area }
38370
39240
  });
38371
- if (spec.cache !== false && (readsScreenIn(spec.wgsl) || readsScreenIn(spec.glsl))) screenReaders.add(filter);
39241
+ if (!backdrop && spec.cache !== false && (readsScreenIn(spec.wgsl) || readsScreenIn(spec.glsl))) screenReaders.add(filter);
38372
39242
  const defaults = Object.fromEntries([
38373
39243
  ...read.map((entry) => [entry.spec.name, entry.value]),
38374
39244
  ["enabled", true],
@@ -38828,7 +39698,9 @@ function brokenMark() {
38828
39698
  * asks the renderer for no frame. A destroy frees the uniform buffers first: `Filter.destroy()`
38829
39699
  * leaves them to a GC that runs after a minute (P10). A filter with inputs draws each place of its
38830
39700
  * chains through Pixi's public `applyFilter` from inside its own `apply`, into pooled textures given
38831
- * back in a `finally`; a stand-in draws in place of a broken kind or instance.
39701
+ * back in a `finally`; a stand-in draws in place of a broken kind or instance. A backdrop is built
39702
+ * with Pixi's `blendRequired`: Pixi copies the canvas under the view before it draws, and the apply
39703
+ * places `sampleBack` on that copy (`activeBackTexture`) and starts the chains `from: "back"` there.
38832
39704
  */
38833
39705
  /**
38834
39706
  * The resolution of every filter but `Blur`: the render target's, so a filter draws at the
@@ -39001,11 +39873,21 @@ function createOurs(pixi, definition, value) {
39001
39873
  type: input.clamp.type
39002
39874
  };
39003
39875
  }
39876
+ if (definition.backdrop) {
39877
+ structures[BACK_SCALE.name] = {
39878
+ value: new Float32Array(BACK_SCALE.size),
39879
+ type: BACK_SCALE.type
39880
+ };
39881
+ structures[BACK_CLAMP.name] = {
39882
+ value: new Float32Array(BACK_CLAMP.size),
39883
+ type: BACK_CLAMP.type
39884
+ };
39885
+ }
39004
39886
  if (definition.origin !== void 0) structures[definition.origin.name] = {
39005
39887
  value: new Float32Array(definition.origin.size),
39006
39888
  type: definition.origin.type
39007
39889
  };
39008
- const group = definition.uniforms.length > 0 || definition.inputs.length > 0 || definition.origin !== void 0 ? new pixi.UniformGroup(structures) : void 0;
39890
+ const group = definition.uniforms.length > 0 || definition.inputs.length > 0 || definition.backdrop || definition.origin !== void 0 ? new pixi.UniformGroup(structures) : void 0;
39009
39891
  const resources = group === void 0 ? {} : { fu: group };
39010
39892
  const empty = pixi.Texture.EMPTY.source;
39011
39893
  for (const input of definition.inputs) {
@@ -39019,7 +39901,8 @@ function createOurs(pixi, definition, value) {
39019
39901
  glProgram,
39020
39902
  resources,
39021
39903
  padding: paddingOf$1(definition, value),
39022
- resolution: resolutionOf(definition.resolution)
39904
+ resolution: resolutionOf(definition.resolution),
39905
+ blendRequired: definition.backdrop
39023
39906
  }),
39024
39907
  group,
39025
39908
  definition,
@@ -39278,10 +40161,85 @@ function createFilter(ectx, pixi, kind, value, probeMap) {
39278
40161
  entity: 0
39279
40162
  });
39280
40163
  if (instance.kind === "wgsl" && instance.definition.inputs.length > 0) wireInputs(ectx, pixi, instance);
40164
+ else if (instance.kind === "wgsl" && instance.definition.backdrop) wireBack(pixi, instance);
39281
40165
  guardApply(ectx, instance, kind);
39282
40166
  return instance;
39283
40167
  }
39284
40168
  /**
40169
+ * What was drawn under the view of the running apply: the texture Pixi's filter system copied the canvas under the
40170
+ * view's box into, or its empty texture outside a backdrop's pop.
40171
+ *
40172
+ * @param pixi - The module the renderer loaded.
40173
+ * @param manager - Pixi's filter system, inside an apply.
40174
+ * @returns The back texture.
40175
+ */
40176
+ function backOf(pixi, manager) {
40177
+ return manager.activeBackTexture ?? pixi.Texture.EMPTY;
40178
+ }
40179
+ /**
40180
+ * Writes the scale and the clamp that place a sample of the body's uv on a texture: one over the size of its backing,
40181
+ * and half a texel inside the part of it its frame covers, rounded to float32. Written only where a value differs, so
40182
+ * Pixi's bind group stays cached; no array is allocated.
40183
+ *
40184
+ * @param uniforms - The values of the uniform group.
40185
+ * @param scaleName - The `vec2` scale uniform.
40186
+ * @param clampName - The `vec4` clamp uniform.
40187
+ * @param texture - The texture the body samples through them.
40188
+ */
40189
+ function writePlacement(uniforms, scaleName, clampName, texture) {
40190
+ const { source } = texture;
40191
+ const scale = uniforms[scaleName];
40192
+ const clamp = uniforms[clampName];
40193
+ const halfX = .5 / source.pixelWidth;
40194
+ const halfY = .5 / source.pixelHeight;
40195
+ if (scale instanceof Float32Array) {
40196
+ writeAt(scale, 0, 1 / source.width, true);
40197
+ writeAt(scale, 1, 1 / source.height, true);
40198
+ }
40199
+ if (clamp instanceof Float32Array) {
40200
+ writeAt(clamp, 0, halfX, true);
40201
+ writeAt(clamp, 1, halfY, true);
40202
+ writeAt(clamp, 2, texture.frame.width / source.width - halfX, true);
40203
+ writeAt(clamp, 3, texture.frame.height / source.height - halfY, true);
40204
+ }
40205
+ }
40206
+ /**
40207
+ * Places a backdrop's `sampleBack` on the back texture of this apply. Pixi binds the texture itself (group 0
40208
+ * binding 3); the engine fills the scale and the clamp, since the copy's backing is sized at the resolution of the
40209
+ * surface under the view, the input's at the view's.
40210
+ *
40211
+ * @param body - The backdrop body.
40212
+ * @param back - What was drawn under the view.
40213
+ */
40214
+ function bindBack(body, back) {
40215
+ const uniforms = body.group?.uniforms;
40216
+ if (uniforms !== void 0) writePlacement(uniforms, BACK_SCALE.name, BACK_CLAMP.name, back);
40217
+ }
40218
+ /**
40219
+ * The apply of a backdrop without inputs, as Pixi calls it: `sampleBack` placed on the copy Pixi made, then the body
40220
+ * drawn from the view's input.
40221
+ *
40222
+ * @param pixi - The module the renderer loaded.
40223
+ * @param body - The backdrop body.
40224
+ * @param manager - Pixi's filter system.
40225
+ * @param input - The view's input texture.
40226
+ * @param output - The output.
40227
+ * @param clearMode - Whether the output is cleared.
40228
+ */
40229
+ function applyBack(pixi, body, manager, input, output, clearMode) {
40230
+ bindBack(body, backOf(pixi, manager));
40231
+ manager.applyFilter(body.filter, input, output, clearMode);
40232
+ }
40233
+ /**
40234
+ * Gives a backdrop body without inputs its own `apply`, which places `sampleBack` before it draws.
40235
+ *
40236
+ * @param pixi - The module the renderer loaded.
40237
+ * @param body - The body, just built.
40238
+ */
40239
+ function wireBack(pixi, body) {
40240
+ body.filter.apply = (manager, input, output, clearMode) => applyBack(pixi, body, manager, input, output, clearMode);
40241
+ }
40242
+ /**
39285
40243
  * The share of the resolution the last place of one input draws at.
39286
40244
  *
39287
40245
  * @param inputs - The chains of the instance.
@@ -39307,8 +40265,12 @@ function stagesOf(inputs) {
39307
40265
  * @param inputs - The chains of the instance.
39308
40266
  */
39309
40267
  function giveBack(pixi, inputs) {
39310
- for (const target of inputs.taken) pixi.TexturePool.returnTexture(target);
39311
- inputs.taken.length = 0;
40268
+ const { taken } = inputs;
40269
+ for (let index = 0; index < taken.length; index += 1) {
40270
+ const target = taken[index];
40271
+ if (target !== void 0) pixi.TexturePool.returnTexture(target);
40272
+ }
40273
+ while (taken.length > 0) taken.pop();
39312
40274
  }
39313
40275
  /**
39314
40276
  * Draws the places of one input, each from the one before, the first from `from`, into a pooled
@@ -39318,7 +40280,7 @@ function giveBack(pixi, inputs) {
39318
40280
  * @param inputs - The chains of the instance.
39319
40281
  * @param index - The input.
39320
40282
  * @param manager - Pixi's filter system, inside an active filter pass.
39321
- * @param from - The view's input, or a kept picture.
40283
+ * @param from - The view's input, the copy of what was drawn under the view, or a kept picture.
39322
40284
  * @param resolution - The resolution of `from`.
39323
40285
  * @param target - Where the last place draws, instead of a pooled texture.
39324
40286
  * @returns The texture the last place drew into; `from` for an empty chain.
@@ -39360,20 +40322,7 @@ function bindInput(instance, index, last) {
39360
40322
  const { resources } = instance.filter;
39361
40323
  if (resources[input.texture] !== source) resources[input.texture] = source;
39362
40324
  if (resources[input.sampler] !== source.style) resources[input.sampler] = source.style;
39363
- const scale = uniforms[input.scale.name];
39364
- const clamp = uniforms[input.clamp.name];
39365
- const halfX = .5 / source.pixelWidth;
39366
- const halfY = .5 / source.pixelHeight;
39367
- if (scale instanceof Float32Array) {
39368
- writeAt(scale, 0, 1 / source.width, true);
39369
- writeAt(scale, 1, 1 / source.height, true);
39370
- }
39371
- if (clamp instanceof Float32Array) {
39372
- writeAt(clamp, 0, halfX, true);
39373
- writeAt(clamp, 1, halfY, true);
39374
- writeAt(clamp, 2, last.frame.width / source.width - halfX, true);
39375
- writeAt(clamp, 3, last.frame.height / source.height - halfY, true);
39376
- }
40325
+ writePlacement(uniforms, input.scale.name, input.clamp.name, last);
39377
40326
  }
39378
40327
  /**
39379
40328
  * Points every input's texture and sampler back at Pixi's empty texture: a filter never holds a
@@ -39394,11 +40343,24 @@ function unbindInputs(pixi, instance) {
39394
40343
  }
39395
40344
  }
39396
40345
  /**
40346
+ * Tells whether the chains of an instance must run again for the resolutions of this apply: the view's input's for a
40347
+ * chain that starts from it, what was drawn under the view's for one that starts from back.
40348
+ *
40349
+ * @param inputs - The chains of the instance.
40350
+ * @param resolution - The resolution of the view's input.
40351
+ * @param backResolution - The resolution of the back; the input's for a filter that reads none.
40352
+ * @returns True when one the chains start from changed.
40353
+ */
40354
+ function resolutionMoved(inputs, resolution, backResolution) {
40355
+ return inputs.fromInput && resolution !== inputs.resolution || inputs.fromBack && backResolution !== inputs.backResolution;
40356
+ }
40357
+ /**
39397
40358
  * The apply of a filter with inputs, as Pixi calls it: a resolution change runs the chains once
39398
- * at the new value, then every place draws, the last texture of each input is bound, and the main
39399
- * body draws from the view's input. Held results are bound instead and no stage draws. The pooled
39400
- * textures go back and the inputs are unbound in a `finally`, also when a place throws; a throw then
39401
- * reaches the guard, which breaks the instance.
40359
+ * at the new value, then every place draws, each chain from the view's input or, `from: "back"`,
40360
+ * from the copy Pixi made of the canvas under the view, the last texture of each input is bound, a
40361
+ * backdrop's `sampleBack` is placed, and the main body draws from the view's input. Held results are
40362
+ * bound instead and no stage draws. The pooled textures go back and the inputs are unbound in a
40363
+ * `finally`, also when a place throws; a throw then reaches the guard, which breaks the instance.
39402
40364
  *
39403
40365
  * @param ectx - Domain context of the effects plugin.
39404
40366
  * @param pixi - The module the renderer loaded.
@@ -39411,16 +40373,23 @@ function unbindInputs(pixi, instance) {
39411
40373
  */
39412
40374
  function applyInputs(ectx, pixi, instance, inputs, manager, input, output, clearMode) {
39413
40375
  const { resolution } = input.source;
39414
- if (inputs.held === void 0 && resolution !== inputs.resolution) evaluateInRender(ectx, pixi, instance, inputs, resolution);
40376
+ const { definition } = instance;
40377
+ const back = definition.backdrop ? backOf(pixi, manager) : input;
40378
+ const backResolution = inputs.fromBack ? back.source.resolution : resolution;
40379
+ if (inputs.held === void 0 && resolutionMoved(inputs, resolution, backResolution)) evaluateInRender(ectx, pixi, instance, inputs, resolution, backResolution);
39415
40380
  if (instance.broken) {
39416
40381
  drawBroken(instance, manager, input, output, clearMode);
39417
40382
  return;
39418
40383
  }
39419
40384
  try {
39420
40385
  for (let index = 0; index < inputs.places.length; index += 1) {
39421
- const last = inputs.held === void 0 ? drawChain(pixi, inputs, index, manager, input, resolution) : inputs.held[index] ?? input;
40386
+ const fromBack = definition.inputs[index]?.from === "back";
40387
+ const start = fromBack ? back : input;
40388
+ const startResolution = fromBack ? backResolution : resolution;
40389
+ const last = inputs.held === void 0 ? drawChain(pixi, inputs, index, manager, start, startResolution) : inputs.held[index] ?? input;
39422
40390
  bindInput(instance, index, last);
39423
40391
  }
40392
+ if (definition.backdrop) bindBack(instance, back);
39424
40393
  manager.applyFilter(instance.filter, input, output, clearMode);
39425
40394
  } finally {
39426
40395
  giveBack(pixi, inputs);
@@ -39442,7 +40411,7 @@ function applyInputs(ectx, pixi, instance, inputs, manager, input, output, clear
39442
40411
  */
39443
40412
  function drawInto(ectx, pixi, instance, inputs, manager, picture, targets) {
39444
40413
  const { resolution } = picture.source;
39445
- if (!instance.broken && resolution !== inputs.resolution) evaluateInRender(ectx, pixi, instance, inputs, resolution);
40414
+ if (!instance.broken && resolution !== inputs.resolution) evaluateInRender(ectx, pixi, instance, inputs, resolution, resolution);
39446
40415
  if (instance.broken) return;
39447
40416
  try {
39448
40417
  for (let index = 0; index < inputs.places.length; index += 1) {
@@ -39462,10 +40431,14 @@ function drawInto(ectx, pixi, instance, inputs, manager, picture, targets) {
39462
40431
  * @param instance - The instance, just built.
39463
40432
  */
39464
40433
  function wireInputs(ectx, pixi, instance) {
39465
- const count = instance.definition.inputs.length;
40434
+ const declared = instance.definition.inputs;
40435
+ const count = declared.length;
39466
40436
  const inputs = {
39467
40437
  stale: true,
39468
40438
  resolution: 0,
40439
+ backResolution: 0,
40440
+ fromInput: declared.some((input) => input.from === "input"),
40441
+ fromBack: declared.some((input) => input.from === "back"),
39469
40442
  version: 0,
39470
40443
  context: { resolution: 0 },
39471
40444
  places: Array.from({ length: count }, () => []),
@@ -39491,7 +40464,9 @@ function wireInputs(ectx, pixi, instance) {
39491
40464
  instance.inputs = inputs;
39492
40465
  instance.filter.apply = (manager, input, output, clearMode) => applyInputs(ectx, pixi, instance, inputs, manager, input, output, clearMode);
39493
40466
  const { resolution } = instance.definition;
39494
- evaluate(ectx, pixi, instance, inputs, resolution > 0 ? resolution : ectx.deps.renderer.host.resolution());
40467
+ const canvas = ectx.deps.renderer.host.resolution();
40468
+ const own = resolution > 0 ? resolution : canvas;
40469
+ evaluate(ectx, pixi, instance, inputs, own, inputs.fromBack ? canvas : own);
39495
40470
  }
39496
40471
  /**
39497
40472
  * Reads one property of a value a chain returned, whatever its type.
@@ -39685,9 +40660,10 @@ function placeSteps(pixi, inputs, index, steps) {
39685
40660
  return changed || rebuilt;
39686
40661
  }
39687
40662
  /**
39688
- * Runs the chain of every input for the instance's value at a resolution and places its steps.
40663
+ * Runs the chain of every input for the instance's value at a resolution and places its steps: a chain that starts
40664
+ * from the view's input at the input's resolution, one that starts from back at the back's.
39689
40665
  * A chain that throws or returns a step it may not draw breaks the instance (reason `"chain"`).
39690
- * The version moves when a place, a scale, a stage uniform or the resolution changed; the passes
40666
+ * The version moves when a place, a scale, a stage uniform or a resolution changed; the passes
39691
40667
  * become the main body plus every place.
39692
40668
  *
39693
40669
  * @param ectx - Domain context of the effects plugin.
@@ -39695,14 +40671,15 @@ function placeSteps(pixi, inputs, index, steps) {
39695
40671
  * @param instance - The instance.
39696
40672
  * @param inputs - Its chains.
39697
40673
  * @param resolution - The resolution of the texture the view is drawn into.
40674
+ * @param backResolution - The resolution of what was drawn under the view; the input's for a filter that reads none.
39698
40675
  */
39699
- function evaluate(ectx, pixi, instance, inputs, resolution) {
40676
+ function evaluate(ectx, pixi, instance, inputs, resolution, backResolution) {
39700
40677
  const kind = ectx.state.kinds.get(instance.kindId);
39701
- let changed = resolution !== inputs.resolution;
40678
+ let changed = resolution !== inputs.resolution || backResolution !== inputs.backResolution;
39702
40679
  let passes = 1;
39703
- inputs.context.resolution = resolution;
39704
40680
  for (const [index, input] of instance.definition.inputs.entries()) {
39705
40681
  let steps;
40682
+ inputs.context.resolution = input.from === "back" ? backResolution : resolution;
39706
40683
  try {
39707
40684
  steps = input.chain(instance.value, inputs.context);
39708
40685
  } catch (error) {
@@ -39718,6 +40695,7 @@ function evaluate(ectx, pixi, instance, inputs, resolution) {
39718
40695
  passes += steps.length;
39719
40696
  }
39720
40697
  inputs.resolution = resolution;
40698
+ inputs.backResolution = backResolution;
39721
40699
  inputs.stale = false;
39722
40700
  instance.passes = passes;
39723
40701
  if (changed) inputs.version += 1;
@@ -39732,9 +40710,10 @@ function evaluate(ectx, pixi, instance, inputs, resolution) {
39732
40710
  * @param instance - The instance.
39733
40711
  * @param inputs - Its chains.
39734
40712
  * @param resolution - The resolution of the texture the view is drawn into.
40713
+ * @param backResolution - The resolution of what was drawn under the view.
39735
40714
  */
39736
- function evaluateInRender(ectx, pixi, instance, inputs, resolution) {
39737
- evaluate(ectx, pixi, instance, inputs, resolution);
40715
+ function evaluateInRender(ectx, pixi, instance, inputs, resolution, backResolution) {
40716
+ evaluate(ectx, pixi, instance, inputs, resolution, backResolution);
39738
40717
  ectx.state.filterSync.all = true;
39739
40718
  }
39740
40719
  /**
@@ -39756,12 +40735,13 @@ function refreshInputs(ectx, pixi, instance, off) {
39756
40735
  inputs.retired.length = 0;
39757
40736
  }
39758
40737
  if (instance.broken || !instance.filter.enabled || off || !inputs.stale) return;
39759
- evaluate(ectx, pixi, instance, inputs, inputs.resolution);
40738
+ evaluate(ectx, pixi, instance, inputs, inputs.resolution, inputs.backResolution);
39760
40739
  }
39761
40740
  /**
39762
40741
  * The body a stand-in of a dev build draws: the fallback with the striped mark over it, or the
39763
40742
  * mark alone. The pair draws the fallback into one pooled texture of the input's frame and
39764
- * resolution, then the mark from it into the output, and gives the texture back in a `finally`.
40743
+ * resolution, then the mark from it into the output, and gives the texture back in a `finally`. A
40744
+ * backdrop fallback has its `sampleBack` placed first.
39765
40745
  *
39766
40746
  * @param pixi - The module the renderer loaded.
39767
40747
  * @param fallback - The checked fallback, or `undefined`.
@@ -39786,6 +40766,7 @@ function markedBody(pixi, fallback, value) {
39786
40766
  request.height = input.frame.height;
39787
40767
  request.resolution = input.source.resolution;
39788
40768
  const pooled = pixi.TexturePool.getOptimalTexture(request);
40769
+ if (body.definition.backdrop) bindBack(body, backOf(pixi, manager));
39789
40770
  try {
39790
40771
  manager.applyFilter(body.filter, input, pooled, true);
39791
40772
  manager.applyFilter(mark.filter, pooled, output, clearMode);
@@ -39815,7 +40796,10 @@ function createStandIn(ectx, pixi, kind, value) {
39815
40796
  const usable = status === "ok" ? fallback : void 0;
39816
40797
  let body;
39817
40798
  if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ === true) body = markedBody(pixi, usable, value);
39818
- else if (usable !== void 0) body = createOurs(pixi, usable, value);
40799
+ else if (usable !== void 0) {
40800
+ body = createOurs(pixi, usable, value);
40801
+ if (usable.backdrop) wireBack(pixi, body);
40802
+ }
39819
40803
  if (body === void 0) return void 0;
39820
40804
  body.filter.enabled = value.enabled;
39821
40805
  body.areaStale = body.definition.area !== void 0;
@@ -41001,14 +41985,27 @@ function cacheOfInstance(instance) {
41001
41985
  return instance.kind === "wgsl" ? instance.definition.cache : CORE_CACHE[instance.kind];
41002
41986
  }
41003
41987
  /**
41988
+ * Tells whether one of the inputs of a definition starts from the view's own input: its chain draws a copy of the
41989
+ * view's subtree. A chain from back draws what was drawn under the view instead.
41990
+ *
41991
+ * @param definition - The definition.
41992
+ * @returns True when one does.
41993
+ */
41994
+ function readsOwnInput(definition) {
41995
+ const { inputs } = definition;
41996
+ for (let index = 0; index < inputs.length; index += 1) if (inputs[index]?.from === "input") return true;
41997
+ return false;
41998
+ }
41999
+ /**
41004
42000
  * Whether an instance shows a blurred copy of what is under it (spec 23 P8.5): the built-in `Blur`, or one of ours
41005
- * with inputs. The predicate an overlay's held loops read (spec 22).
42001
+ * with an input drawn from its own subtree. A backdrop whose chains start from back blurs the scene under the view,
42002
+ * not the view: an overlay holds no loops for it. The predicate an overlay's held loops read (spec 22).
41006
42003
  *
41007
42004
  * @param instance - The instance.
41008
42005
  * @returns True, or `undefined` for every other filter (the slot leaves the key out).
41009
42006
  */
41010
42007
  function blursOf(instance) {
41011
- return instance.kind === "blur" || instance.kind === "wgsl" && instance.definition.inputs.length > 0 ? true : void 0;
42008
+ return instance.kind === "blur" || instance.kind === "wgsl" && readsOwnInput(instance.definition) ? true : void 0;
41012
42009
  }
41013
42010
  /**
41014
42011
  * Tells whether a debug switch turned a kind off (`effects.diagnostics.disable`). The guard is
@@ -59151,6 +60148,7 @@ function cubicBezier(x1, y1, x2, y2) {
59151
60148
  * | renderer | preference | "webgpu", WebGL is Pixi's fallback |
59152
60149
  * | renderer | aspect | { min: 4/3, max: 21/9 }; shallow merge: a game that sets `aspect` gives both fields |
59153
60150
  * | renderer | poolLimit | 256 |
60151
+ * | renderer | adaptiveScale | { floor: 0.75, devices: "phone" }, the render scale of the adaptive frame rate on a GPU-bound phone; false keeps the resolution; shallow merge: a game that sets it gives both fields |
59154
60152
  * | renderer | unsupportedMessage | "This device cannot run the game." |
59155
60153
  * | renderer | loadPixi | () => import("pixi.js") |
59156
60154
  * | renderer | debug | { nineSlice: false, verifyFrames: false, kept: true, overdraw: false }; shallow merge: a game that sets `debug` replaces the whole object, and a switch it leaves out is off, except `kept`, which stays on unless set to false; `verifyFrames` and `overdraw` work in dev and debug builds only |