@moku-labs/game 0.14.13 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/app/page.d.mts +1 -1
  2. package/dist/app/page.mjs +2 -2
  3. package/dist/app/system.d.mts +3 -3
  4. package/dist/app.d.mts +3 -3
  5. package/dist/app.mjs +1 -1
  6. package/dist/assets.d.mts +1 -1
  7. package/dist/cli.mjs +75 -36
  8. package/dist/{commands-DAETmK86.d.mts → commands-BynjWVVy.d.mts} +7 -6
  9. package/dist/control.d.mts +30 -18
  10. package/dist/control.mjs +2 -2
  11. package/dist/{define-BASd9ZUh.d.mts → define-Ckj_TMWu.d.mts} +2 -2
  12. package/dist/{dev-DbHgx-R-.mjs → dev-BNndV-hU.mjs} +4 -4
  13. package/dist/{headless-cFveciuY.d.mts → headless-B0mH9qYg.d.mts} +1 -1
  14. package/dist/{index-DedbED3T.d.mts → index-CCiTo1_3.d.mts} +16 -4
  15. package/dist/index.d.mts +5 -5
  16. package/dist/index.mjs +2 -2
  17. package/dist/inspect.d.mts +6 -6
  18. package/dist/inspect.mjs +1 -1
  19. package/dist/jsx-dev-runtime.d.mts +1 -1
  20. package/dist/jsx-runtime.d.mts +1 -1
  21. package/dist/project.d.mts +4 -4
  22. package/dist/project.mjs +1 -1
  23. package/dist/{run-BbipFRc6.mjs → run-C07NXds7.mjs} +35 -25
  24. package/dist/{runtime-DoSlRx8X.d.mts → runtime-BV3xv6gx.d.mts} +1 -1
  25. package/dist/{sources-Cdvb0rOS.mjs → sources-OvHvyeJS.mjs} +1 -1
  26. package/dist/{src-BEbJ_sSx.mjs → src-CwVzErzN.mjs} +1921 -652
  27. package/dist/testing.d.mts +4 -4
  28. package/dist/testing.mjs +1 -1
  29. package/dist/{types-GHE4J5Uf.d.mts → types-Bw-qLjFZ.d.mts} +2 -2
  30. package/dist/{types-C73hbmxi.d.mts → types-CpdkyhaC.d.mts} +4 -4
  31. package/dist/{types-DngFQJro.d.mts → types-D44EL4gT.d.mts} +2 -2
  32. package/dist/{types-BGcda5Le.d.mts → types-E2ykwIUO.d.mts} +556 -42
  33. package/dist/{types-7GXcXxxv.d.mts → types-G3ZAAsbd.d.mts} +20 -1
  34. package/dist/{visual-_XhoGeIC.d.mts → visual-DoHwXeQi.d.mts} +6 -6
  35. package/dist/visual.d.mts +1 -1
  36. package/dist/visual.mjs +2 -2
  37. package/dist/{watch-BEdrp80i.mjs → watch-BSgptowj.mjs} +16 -6
  38. package/llms.txt +6 -4
  39. package/package.json +1 -1
@@ -3,7 +3,7 @@ import { r as systemSource, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { a as finishDrafts, i as enableDraftPatches, n as deepFreeze$1, o as openDrafts, r as dropDrafts } from "./drafts-BawpSaVp.mjs";
4
4
  import { d as graphHash, f as isRestCheckpoint, h as pushEntry, i as servePaused, l as findNode, m as compact, n as captureFrame, o as collectFlows, p as slotNames, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as framePath } from "./capture-D7XvoHdH.mjs";
5
5
  import { A as Order, C as Sprite, D as Exiting, F as system, I as systemsFor, L as tag, M as component, O as Layer, P as resource, S as Shape, T as componentsFor, _ as HudPart, b as OverlayLevel, c as Countdown, d as builtInStyles, g as Display, h as textFor, i as samePose, j as Tree, k as Now, l as Text, m as readStyle, n as drawnRect, o as visualOf$1, r as fitScaleOf, s as visualRectOf, t as CONTENT$1, v as NineSlice, w as Transform, x as Parent, y as Overlay } from "./visual-2m4wJL_z.mjs";
6
- import { a as isDev } from "./dev-DbHgx-R-.mjs";
6
+ import { a as isDev } from "./dev-BNndV-hU.mjs";
7
7
  import { i as isComponentDefinition, n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
8
8
  import { envPlugin, logPlugin } from "@moku-labs/common/browser";
9
9
  import { createCoreConfig } from "@moku-labs/core";
@@ -124,7 +124,9 @@ function createClockApi(ctx) {
124
124
  poke: () => {
125
125
  deliverElapsed(ctx.state);
126
126
  },
127
- dueAt: () => ctx.state.dueAt
127
+ dueAt: () => ctx.state.dueAt,
128
+ pending: () => ctx.state.dueAt === void 0 ? 0 : 1,
129
+ listeners: () => ctx.state.listeners.length
128
130
  };
129
131
  }
130
132
  //#endregion
@@ -684,6 +686,25 @@ function capSlackMs(displayHz, frameMs) {
684
686
  return whole >= 1 && Math.abs(periods - whole) <= .05 ? 500 / displayHz : 1;
685
687
  }
686
688
  /**
689
+ * The rate the loop really runs at under a cap: the cap skips every tick that comes more than `capSlackMs` early, so
690
+ * a frame runs on the first vsync at or after one frame of the cap minus the slack, never faster than the display. The
691
+ * cap itself while the display is unknown. Called when the snapshot is rebuilt, never per frame.
692
+ *
693
+ * @param fps - The cap: the loop's rate, the idle cap left out.
694
+ * @param displayHz - The display's refresh rate, or `null` while unknown.
695
+ * @returns Frames per second the loop runs at.
696
+ * @example
697
+ * ```ts
698
+ * loopRateOf(60, 144); // 48: every third tick of a 144 Hz panel
699
+ * ```
700
+ */
701
+ function loopRateOf(fps, displayHz) {
702
+ if (displayHz === null) return fps;
703
+ const frameMs = 1e3 / fps;
704
+ const ticks = Math.ceil((frameMs - capSlackMs(displayHz, frameMs)) * displayHz / 1e3);
705
+ return displayHz / Math.max(1, ticks);
706
+ }
707
+ /**
687
708
  * Normalizes a thermal status of the shell: a status name, or Android's whole number 0–6 mapped in order.
688
709
  *
689
710
  * @param value - The raw status.
@@ -856,20 +877,36 @@ function createGovernor(floor) {
856
877
  };
857
878
  }
858
879
  /**
859
- * The frozen object `rate()` answers, built from the rate now.
880
+ * The rate the loop is told to hold: the cap in mode `"fixed"` (`maxFps`) and `"forced"` (the `setRate` value, never
881
+ * above `maxFps`); `null` in mode `"auto"`, where the adaptive rule decides. Outside `"auto"` the cap is always one of
882
+ * the fixed choices, so the guard only narrows its type.
883
+ *
884
+ * @param rate - The rate and its mode.
885
+ * @returns The asked rate, or `null`.
886
+ */
887
+ function askedOf(rate) {
888
+ return rate.mode !== "auto" && isChoice(rate.fps) ? rate.fps : null;
889
+ }
890
+ /**
891
+ * The frozen object `rate()` answers, built from the rate now: `fps` is what the loop really runs at under the cap on
892
+ * the known display, and `heldBy` is `"panel"` when the asked rate is above the display's.
860
893
  *
861
894
  * @param rate - The rate, its mode, its reason, the display and the governor.
862
895
  * @returns A fresh frozen snapshot.
863
896
  */
864
897
  function snapshotOf(rate) {
865
898
  const { governor } = rate;
899
+ const { hz } = rate.display;
900
+ const asked = askedOf(rate);
866
901
  return Object.freeze({
867
- fps: rate.fps,
902
+ fps: loopRateOf(rate.fps, hz),
868
903
  mode: rate.mode,
869
904
  reason: rate.reason,
870
- displayHz: rate.display.hz,
905
+ displayHz: hz,
871
906
  ladder: governor?.ladder ?? NO_STEPS,
872
- thermal: governor?.thermal ?? null
907
+ thermal: governor?.thermal ?? null,
908
+ asked,
909
+ heldBy: asked !== null && hz !== null && asked > hz ? "panel" : null
873
910
  });
874
911
  }
875
912
  /**
@@ -1597,7 +1634,7 @@ function noteCap(ctx, frameMs) {
1597
1634
  * skipped, is a sample of the display rate. The first frame, and the first frame after a
1598
1635
  * `resume`, counts as one capped frame instead of the gap to a stale timestamp. A frame that
1599
1636
  * arrives more than the cap's slack before the current cap allows it is skipped — the cap is
1600
- * `idleFps` while the screen is idle and the loop's rate (`rate().fps`) otherwise, the slack half
1637
+ * `idleFps` while the screen is idle and the loop's rate otherwise (`rate().asked`, or the adaptive rule's), the slack half
1601
1638
  * a display period (`capSlackMs`) — and a long gap is clamped at `maxDeltaMs` so a backgrounded
1602
1639
  * tab does not produce a giant step. The idle timer moves by the raw gap, at most `idleAfterMs`,
1603
1640
  * so a throttled page goes idle in `idleAfterMs`. The interval that ends at a run frame is judged
@@ -1645,6 +1682,17 @@ function addFrameCallback(ctx, phase, callback) {
1645
1682
  };
1646
1683
  }
1647
1684
  /**
1685
+ * Counts the registered frame callbacks of the six phases, named one by one as `capturePhases` does: no loop, no
1686
+ * iterator, no allocation.
1687
+ *
1688
+ * @param ctx - Domain context of the time plugin.
1689
+ * @returns The count.
1690
+ */
1691
+ function countFrameListeners(ctx) {
1692
+ const { input, animate, layout, sync, signals, render } = ctx.state.callbacks;
1693
+ return input.length + animate.length + layout.length + sync.length + signals.length + render.length;
1694
+ }
1695
+ /**
1648
1696
  * Creates the time API: frame callbacks per phase, the `Time` resource, scale, pause and
1649
1697
  * resume, `wake` for the idle cap, the measurement of an agent build, the frame rate, and `step`
1650
1698
  * for tests and tools.
@@ -1679,6 +1727,7 @@ function createTimeApi(ctx) {
1679
1727
  isRunning: () => ctx.state.running,
1680
1728
  wake: () => wakeClock(ctx),
1681
1729
  wakes: () => ctx.state.wakes,
1730
+ frameListeners: () => countFrameListeners(ctx),
1682
1731
  measure: (listener) => {
1683
1732
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) {
1684
1733
  const { measure } = ctx.state;
@@ -8045,8 +8094,9 @@ const WORLD_PHASES = [
8045
8094
  ];
8046
8095
  /**
8047
8096
  * Gives a timed system its row: the row of an earlier system of the same name, else the next one.
8048
- * `ms` grows (a new array, the old values copied) only when the row is past its end: at
8049
- * registration, never per frame.
8097
+ * A new row replaces `names` with a new array, so a reader that kept the old one sees the growth
8098
+ * by identity, and the old array stays as it was. `ms` grows (a new array, the old values copied)
8099
+ * only when the row is past its end. Both happen at registration, never per frame.
8050
8100
  *
8051
8101
  * @param state - The ecs branch of the plugin state.
8052
8102
  * @param timing - The timing table.
@@ -8055,7 +8105,7 @@ const WORLD_PHASES = [
8055
8105
  function giveRow(state, timing, name) {
8056
8106
  if (state.timingSlots.has(name)) return;
8057
8107
  const row = timing.names.length;
8058
- timing.names.push(name);
8108
+ timing.names = [...timing.names, name];
8059
8109
  state.timingSlots.set(name, row);
8060
8110
  if (row < timing.ms.length) return;
8061
8111
  const grown = new Float64Array(timing.ms.length * 2);
@@ -8341,6 +8391,7 @@ function createEcsApi(ctx) {
8341
8391
  diff: (from, to) => diffHistory(state.history, from, to),
8342
8392
  schema: () => schemaOf(state.types.values()),
8343
8393
  ownerOf: (entity) => state.owners.get(entity),
8394
+ count: () => state.owners.size,
8344
8395
  onOwnerLeft: (fn) => {
8345
8396
  state.ownerLeft.push(fn);
8346
8397
  return () => {
@@ -10612,6 +10663,7 @@ function exposeEcs(ecs, projection) {
10612
10663
  changed: ecs.changed,
10613
10664
  typeOf: ecs.typeOf,
10614
10665
  ownerOf: ecs.ownerOf,
10666
+ count: ecs.count,
10615
10667
  mode: ecs.mode,
10616
10668
  setMode: (mode) => {
10617
10669
  ecs.setMode(mode);
@@ -11298,18 +11350,27 @@ function someOf(entities, test) {
11298
11350
  return false;
11299
11351
  }
11300
11352
  /**
11353
+ * Empties a set when it holds something. V8 gives an empty set a new table at every `clear()`, so clearing the empty
11354
+ * sets of a still screen every frame allocated about 18 MB in 20 000 frames.
11355
+ *
11356
+ * @param set - The set.
11357
+ */
11358
+ function emptyOut(set) {
11359
+ if (set.size > 0) set.clear();
11360
+ }
11361
+ /**
11301
11362
  * Empties the marks of a decision.
11302
11363
  *
11303
11364
  * @param state - The demand state.
11304
11365
  */
11305
11366
  function clearMarks(state) {
11306
- state.marked.clear();
11307
- state.pending.clear();
11308
- state.loose.clear();
11309
- state.visual.clear();
11310
- state.moved.clear();
11311
- state.published.clear();
11312
- state.wholes.clear();
11367
+ emptyOut(state.marked);
11368
+ emptyOut(state.pending);
11369
+ emptyOut(state.loose);
11370
+ emptyOut(state.visual);
11371
+ emptyOut(state.moved);
11372
+ emptyOut(state.published);
11373
+ emptyOut(state.wholes);
11313
11374
  state.counted = false;
11314
11375
  }
11315
11376
  /**
@@ -11426,7 +11487,7 @@ function createDemandApi(ctx, deps) {
11426
11487
  if (drawn) {
11427
11488
  state.drawnOnce = true;
11428
11489
  state.requests = 0;
11429
- state.sinceDraw.clear();
11490
+ emptyOut(state.sinceDraw);
11430
11491
  }
11431
11492
  return drawn;
11432
11493
  };
@@ -11443,7 +11504,7 @@ function createDemandApi(ctx, deps) {
11443
11504
  }
11444
11505
  if ((typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) && state.verify.on) {
11445
11506
  verifyFrame(ctx, state, host, state.frame);
11446
- if (state.frame.reason === "verify") state.sinceDraw.clear();
11507
+ if (state.frame.reason === "verify") emptyOut(state.sinceDraw);
11447
11508
  }
11448
11509
  };
11449
11510
  /**
@@ -12363,7 +12424,12 @@ function stopHost(state) {
12363
12424
  //#endregion
12364
12425
  //#region src/plugins/renderer/monitor/units.ts
12365
12426
  /**
12366
- * @file renderer/monitor — the byte and rounding helpers the counters and the performance log share (LEAD-FIXES-3 O24).
12427
+ * @file renderer/monitor — the byte and rounding helpers the counters and the performance log share (LEAD-FIXES-3
12428
+ * O24), and the column readers: one per typed-array kind, never one shared. V8 keeps one load feedback per function,
12429
+ * inlined copies included: a reader that saw more than four array kinds goes generic, and every double it reads on the
12430
+ * per-frame path comes back boxed, a heap number a read. A reader that sees one kind stays monomorphic and reads the
12431
+ * double unboxed. The parameter type holds the rule: `cellF32` takes a `Float32Array` and nothing else. One read out of
12432
+ * bounds (slot -1) boxes a reader's doubles the same way, for every caller: a slot that may not exist is checked first.
12367
12433
  */
12368
12434
  /** Bytes of one MiB. */
12369
12435
  const BYTES_PER_MB$1 = 1048576;
@@ -12383,18 +12449,87 @@ function roundTo(value, decimals) {
12383
12449
  return Math.round(value * factor) / factor;
12384
12450
  }
12385
12451
  /**
12386
- * Reads one value of a column; a slot past the end reads NaN, "not measured". The one place a typed-array read meets
12387
- * `noUncheckedIndexedAccess`.
12452
+ * Reads one value of a `Float64Array` column; a slot past the end reads NaN, "not measured".
12453
+ *
12454
+ * @param column - The column.
12455
+ * @param index - The slot.
12456
+ * @returns The value.
12457
+ * @example
12458
+ * ```ts
12459
+ * cellF64(Float64Array.of(2.5), 0); // 2.5
12460
+ * ```
12461
+ */
12462
+ function cellF64(column, index) {
12463
+ return column[index] ?? NaN;
12464
+ }
12465
+ /**
12466
+ * Reads one value of a `Float32Array` column; a slot past the end reads NaN, "not measured".
12467
+ *
12468
+ * @param column - The column.
12469
+ * @param index - The slot.
12470
+ * @returns The value.
12471
+ * @example
12472
+ * ```ts
12473
+ * cellF32(Float32Array.of(2.5), 1); // NaN
12474
+ * ```
12475
+ */
12476
+ function cellF32(column, index) {
12477
+ return column[index] ?? NaN;
12478
+ }
12479
+ /**
12480
+ * Reads one value of a `Uint32Array` column; a slot past the end reads NaN.
12481
+ *
12482
+ * @param column - The column.
12483
+ * @param index - The slot.
12484
+ * @returns The value.
12485
+ * @example
12486
+ * ```ts
12487
+ * cellU32(Uint32Array.of(4812), 0); // 4812
12488
+ * ```
12489
+ */
12490
+ function cellU32(column, index) {
12491
+ return column[index] ?? NaN;
12492
+ }
12493
+ /**
12494
+ * Reads one value of an `Int32Array` column; a slot past the end reads NaN.
12495
+ *
12496
+ * @param column - The column.
12497
+ * @param index - The slot.
12498
+ * @returns The value.
12499
+ * @example
12500
+ * ```ts
12501
+ * cellI32(Int32Array.of(-1), 0); // -1
12502
+ * ```
12503
+ */
12504
+ function cellI32(column, index) {
12505
+ return column[index] ?? NaN;
12506
+ }
12507
+ /**
12508
+ * Reads one value of a `Uint16Array` column; a slot past the end reads NaN.
12388
12509
  *
12389
- * @param column - A typed array or any list of numbers.
12510
+ * @param column - The column.
12390
12511
  * @param index - The slot.
12391
12512
  * @returns The value.
12392
12513
  * @example
12393
12514
  * ```ts
12394
- * cell(Float32Array.of(2.5), 0); // 2.5
12515
+ * cellU16(Uint16Array.of(0xff_ff), 0); // 65535
12395
12516
  * ```
12396
12517
  */
12397
- function cell(column, index) {
12518
+ function cellU16(column, index) {
12519
+ return column[index] ?? NaN;
12520
+ }
12521
+ /**
12522
+ * Reads one value of a `Uint8Array` column; a slot past the end reads NaN.
12523
+ *
12524
+ * @param column - The column.
12525
+ * @param index - The slot.
12526
+ * @returns The value.
12527
+ * @example
12528
+ * ```ts
12529
+ * cellU8(Uint8Array.of(1), 0); // 1
12530
+ * ```
12531
+ */
12532
+ function cellU8(column, index) {
12398
12533
  return column[index] ?? NaN;
12399
12534
  }
12400
12535
  //#endregion
@@ -12438,7 +12573,7 @@ function sourceBytes(source) {
12438
12573
  * isTarget({ uploadMethodId: "unknown", resource: null }); // true
12439
12574
  * ```
12440
12575
  */
12441
- function isTarget(source) {
12576
+ function isTarget$1(source) {
12442
12577
  return source.uploadMethodId === "unknown" && (source.resource === null || source.resource === void 0);
12443
12578
  }
12444
12579
  /**
@@ -12473,7 +12608,7 @@ function textureUsage(app, kept) {
12473
12608
  usage.count += 1;
12474
12609
  usage.bytes += bytes;
12475
12610
  if (kept.has(source)) usage.kept += bytes;
12476
- else if (isTarget(source)) usage.targets += bytes;
12611
+ else if (isTarget$1(source)) usage.targets += bytes;
12477
12612
  else usage.art += bytes;
12478
12613
  }
12479
12614
  return usage;
@@ -14141,6 +14276,7 @@ function createRecord$1(kctx, pixi, entity, view, slots, category) {
14141
14276
  still: 0,
14142
14277
  churn: 0,
14143
14278
  calm: false,
14279
+ quiet: 0,
14144
14280
  lastDrawn: kctx.state.frame,
14145
14281
  depth: 0,
14146
14282
  bytes: 0,
@@ -14708,6 +14844,7 @@ function stepRecord(kctx, pixi, matrices, record, view) {
14708
14844
  if (eligible(kctx, record, view, NOW)) stepEligible(kctx, pixi, matrices, record, view);
14709
14845
  else stepIneligible(kctx, record, view);
14710
14846
  if (drawingChanged(kctx, record, BEFORE)) dirtyAbove(kctx, record);
14847
+ record.quiet = record.dirtyS1 ? 0 : record.quiet + 1;
14711
14848
  record.dirtyS1 = false;
14712
14849
  record.dirtyS2 = false;
14713
14850
  record.dirtyS3 = false;
@@ -14872,21 +15009,21 @@ function shownRecords(kctx) {
14872
15009
  return kctx.state.records.filter((record) => record.mode === "kept" && record.quad?.visible === true);
14873
15010
  }
14874
15011
  /**
14875
- * Reads the stage once more with every picture drawn live: the kept records bypassed, the entries of every range
14876
- * lifted and its sprite hidden. The records draw their pictures again after the read.
15012
+ * Runs a draw with every picture drawn live: the kept records bypassed, the entries of every range with a sprite lifted
15013
+ * and its sprite hidden. The records draw their pictures again after it, the ranges show their sprites again.
14877
15014
  *
14878
15015
  * @param kctx - The kept context.
14879
- * @param ranges - The ranges whose sprite stood in the frame.
14880
15016
  * @param bypass - Draws every kept record live (true) or as its picture again (false).
14881
- * @param resolution - The resolution of the read, the renderer's.
14882
- * @returns The pixels drawn live, or `undefined` when Pixi could not read them.
15017
+ * @param draw - The render or the read.
15018
+ * @returns What `draw` returned.
14883
15019
  */
14884
- function renderLive(kctx, ranges, bypass, resolution) {
15020
+ function drawLive(kctx, bypass, draw) {
15021
+ const ranges = [...kctx.state.ranges.values()].filter((range) => range.sprite !== void 0);
14885
15022
  const lift = ranges.flatMap((range) => range.entries);
14886
15023
  const hide = ranges.flatMap((range) => range.sprite === void 0 ? [] : [range.sprite]);
14887
15024
  bypass(true);
14888
15025
  try {
14889
- return kctx.sync.liftFor(kctx.hidden, lift, hide, "freeze", () => kctx.host.pixels(resolution));
15026
+ return kctx.sync.liftFor(kctx.hidden, lift, hide, "freeze", draw);
14890
15027
  } finally {
14891
15028
  bypass(false);
14892
15029
  }
@@ -14926,7 +15063,7 @@ function verifyPictures(kctx, bypass, recover) {
14926
15063
  const resolution = host.resolution();
14927
15064
  const drawn = host.pixels(resolution);
14928
15065
  if (drawn === void 0) return;
14929
- const live = renderLive(kctx, ranges, bypass, resolution);
15066
+ const live = drawLive(kctx, bypass, () => host.pixels(resolution));
14930
15067
  if (live === void 0 || live.length !== drawn.length) return;
14931
15068
  const screen = host.screen();
14932
15069
  const width = screen === void 0 ? 0 : Math.round(screen.width * resolution);
@@ -15150,7 +15287,7 @@ function createKeptApi(ctx, deps) {
15150
15287
  flushRanges(kctx);
15151
15288
  },
15152
15289
  after: () => {
15153
- if ((typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) && deps.demand.verifying()) verifyPictures(kctx, bypass, () => {
15290
+ if ((typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) && deps.demand.verifying() && !ctx.state.sync.debug.overdraw) verifyPictures(kctx, bypass, () => {
15154
15291
  for (const record of state.records) if (record.mode === "kept") live(record);
15155
15292
  });
15156
15293
  },
@@ -15182,6 +15319,10 @@ function createKeptApi(ctx, deps) {
15182
15319
  return refreshes;
15183
15320
  },
15184
15321
  bypass,
15322
+ live: (draw) => {
15323
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) return drawLive(kctx, bypass, draw);
15324
+ return draw();
15325
+ },
15185
15326
  enabled: (on) => {
15186
15327
  const next = on && state.allowed;
15187
15328
  if (next === state.on) return;
@@ -15541,9 +15682,9 @@ function overlapsLongFrame(ring, start, end) {
15541
15682
  */
15542
15683
  function annotateLongFrame(perf, slot) {
15543
15684
  const c = perf.ring.columns;
15544
- const t = cell(c.t, slot);
15545
- const gap = cell(c.gapMs, slot);
15546
- return overlapsLongFrame(perf.longFrames, Number.isNaN(gap) ? t : t - gap, t + cell(c.workMs, slot));
15685
+ const t = cellF64(c.t, slot);
15686
+ const gap = cellF32(c.gapMs, slot);
15687
+ return overlapsLongFrame(perf.longFrames, Number.isNaN(gap) ? t : t - gap, t + cellF32(c.workMs, slot));
15547
15688
  }
15548
15689
  /**
15549
15690
  * The heap the page uses, in bytes: Chromium's `performance.memory.usedJSHeapSize`, read through a type guard.
@@ -15563,6 +15704,348 @@ function heapMb() {
15563
15704
  return Number.isNaN(bytes) ? null : roundTo(bytes / BYTES_PER_MB$1, 2);
15564
15705
  }
15565
15706
  //#endregion
15707
+ //#region src/plugins/renderer/monitor/types.ts
15708
+ /**
15709
+ * Indices of the per-frame counters in `MonitorSinks.counts`. Not public.
15710
+ */
15711
+ const COUNT = {
15712
+ passes: 0,
15713
+ stencilPasses: 1,
15714
+ filterApplies: 2,
15715
+ filterPixels: 3,
15716
+ uploads: 4,
15717
+ uploadBytes: 5,
15718
+ compiles: 6,
15719
+ marks: 7,
15720
+ rebuilds: 8,
15721
+ targetsCreated: 9,
15722
+ targetsDestroyed: 10
15723
+ };
15724
+ /**
15725
+ * What this application measures, as bits of `PerfState.measuring`. Not public.
15726
+ */
15727
+ const MEASURE = {
15728
+ passes: 1,
15729
+ stencilPasses: 2,
15730
+ draws: 4,
15731
+ counted: 8,
15732
+ compiles: 16,
15733
+ gpu: 32,
15734
+ targets: 64
15735
+ };
15736
+ //#endregion
15737
+ //#region src/plugins/renderer/monitor/problems.ts
15738
+ /** Pixels per megapixel. */
15739
+ const PIXELS_PER_MPX$1 = 1e6;
15740
+ /** The window of the `retakes` rule, in seconds: two flushes. */
15741
+ const RETAKES_SECONDS = 2;
15742
+ /**
15743
+ * Adds one frame to the accumulators of the rules: for a drawn frame, each owner's run of drawn frames its filter
15744
+ * applied in (a drawn frame without an apply ends the run), its applies and pixels of the second; for every frame, the
15745
+ * render targets it made and dropped, a create in an idle frame and a create soon after a pool trim. Indexed stores
15746
+ * into the typed arrays of the perf state.
15747
+ *
15748
+ * @param pc - The perf context.
15749
+ * @param measure - What `time` measured of the frame.
15750
+ */
15751
+ function accumulateProblems(pc, measure) {
15752
+ if (pc.perf.drawn) accumulateOwners(pc, measure);
15753
+ accumulateTargets(pc, measure);
15754
+ }
15755
+ /**
15756
+ * Adds a drawn frame to each owner's run: a frame its filter applied in grows the run and the second's applies and
15757
+ * pixels, a frame without an apply ends the run.
15758
+ *
15759
+ * @param pc - The perf context.
15760
+ * @param measure - What `time` measured of the frame.
15761
+ */
15762
+ function accumulateOwners(pc, measure) {
15763
+ const { problems } = pc.perf;
15764
+ const { owners } = pc.sinks;
15765
+ for (let slot = 0; slot < 32; slot += 1) {
15766
+ if (cellF64(owners.ids, slot) === 0) continue;
15767
+ if (cellF64(owners.applies, slot) === 0) {
15768
+ problems.run[slot] = 0;
15769
+ continue;
15770
+ }
15771
+ if (cellF64(problems.run, slot) === 0) problems.runSince[slot] = measure.frame;
15772
+ problems.run[slot] = cellF64(problems.run, slot) + 1;
15773
+ problems.secFrames[slot] = cellF64(problems.secFrames, slot) + 1;
15774
+ problems.secPixels[slot] = cellF64(problems.secPixels, slot) + cellF64(owners.pixels, slot);
15775
+ }
15776
+ }
15777
+ /**
15778
+ * Adds a frame's render targets to the second: the made and dropped ones, a create in an idle frame, a create within
15779
+ * `churnAfterTrimMs` after a pool trim.
15780
+ *
15781
+ * @param pc - The perf context.
15782
+ * @param measure - What `time` measured of the frame.
15783
+ */
15784
+ function accumulateTargets(pc, measure) {
15785
+ const { problems } = pc.perf;
15786
+ const { counts } = pc.sinks;
15787
+ const created = cellF64(counts, COUNT.targetsCreated);
15788
+ problems.secTargets += created + cellF64(counts, COUNT.targetsDestroyed);
15789
+ if (created === 0) return;
15790
+ if (measure.idle && !problems.idleCreate) {
15791
+ problems.idleCreate = true;
15792
+ problems.createFrame = measure.frame;
15793
+ }
15794
+ if (!problems.trimCreate && measure.at > problems.trimAt && measure.at - problems.trimAt <= problems.thresholds.churnAfterTrimMs) {
15795
+ problems.trimCreate = true;
15796
+ problems.createFrame = measure.frame;
15797
+ }
15798
+ }
15799
+ /**
15800
+ * The label of an entity's view now.
15801
+ *
15802
+ * @param pc - The perf context.
15803
+ * @param entity - The entity.
15804
+ * @returns The label of its view's display object, or `null` once the view left.
15805
+ */
15806
+ function viewLabel(pc, entity) {
15807
+ return pc.ctx.state.sync.views.get(entity)?.object.label ?? null;
15808
+ }
15809
+ /**
15810
+ * Tells whether a filtered view stands still unkept: its record is live while kept pictures are on, its subtree did
15811
+ * not change and its filter applied in the last `stillFrames` drawn frames, over `stillMpx` megapixels a frame.
15812
+ *
15813
+ * @param pc - The perf context.
15814
+ * @param slot - The owner's slot.
15815
+ * @param record - The owner's kept record, or `undefined` without one.
15816
+ * @returns True when the rule holds.
15817
+ */
15818
+ function standsStill$1(pc, slot, record) {
15819
+ const { problems } = pc.perf;
15820
+ const { stillFrames, stillMpx } = problems.thresholds;
15821
+ const kept = pc.ctx.state.kept;
15822
+ const frames = cellF64(problems.secFrames, slot);
15823
+ if (!(record !== void 0 && record.mode === "live" && kept.on && !kept.bypassed)) return false;
15824
+ if (!(frames > 0 && record.quiet >= stillFrames && cellF64(problems.run, slot) >= stillFrames)) return false;
15825
+ return cellF64(problems.secPixels, slot) / frames / PIXELS_PER_MPX$1 >= stillMpx;
15826
+ }
15827
+ /**
15828
+ * Judges `filter-still` for one owner: a crossing opens the problem with its reason, a stop clears it.
15829
+ *
15830
+ * @param pc - The perf context.
15831
+ * @param slot - The owner's slot.
15832
+ * @param owner - The owner.
15833
+ * @param out - The details written now.
15834
+ */
15835
+ function judgeStillOwner(pc, slot, owner, out) {
15836
+ const { problems } = pc.perf;
15837
+ const { owners, kindNames } = pc.sinks;
15838
+ const record = pc.ctx.state.kept.byEntity.get(owner);
15839
+ const still = standsStill$1(pc, slot, record);
15840
+ const open = cellU8(problems.stillOpen, slot) === 1;
15841
+ if (still && !open && record !== void 0) {
15842
+ const kind = cellU8(owners.kinds, slot);
15843
+ const frames = cellF64(problems.run, slot);
15844
+ problems.stillOpen[slot] = 1;
15845
+ out.push({
15846
+ rule: "filter-still",
15847
+ severity: "warn",
15848
+ owner,
15849
+ view: viewLabel(pc, owner),
15850
+ filter: kind === 0 ? null : kindNames[kind - 1] ?? "other",
15851
+ value: Math.min(frames, record.quiet),
15852
+ threshold: problems.thresholds.stillFrames,
15853
+ since: cellF64(problems.runSince, slot),
15854
+ frames,
15855
+ reason: record.refused > 0 ? "budget" : "not-eligible"
15856
+ });
15857
+ } else if (!still && open) {
15858
+ problems.stillOpen[slot] = 0;
15859
+ out.push({
15860
+ rule: "filter-still",
15861
+ cleared: true,
15862
+ owner,
15863
+ view: viewLabel(pc, owner)
15864
+ });
15865
+ }
15866
+ }
15867
+ /**
15868
+ * Judges `filter-still` for every owner of the table: a crossing opens the problem with its reason (`"budget"` when
15869
+ * the kept budget refused the picture, `"not-eligible"` otherwise), a stop clears it; an owner whose filter applied in
15870
+ * no frame of the second leaves its slot.
15871
+ *
15872
+ * @param pc - The perf context.
15873
+ * @param out - The details written now.
15874
+ */
15875
+ function judgeStill(pc, out) {
15876
+ const { problems } = pc.perf;
15877
+ const { owners } = pc.sinks;
15878
+ for (let slot = 0; slot < 32; slot += 1) {
15879
+ const owner = cellF64(owners.ids, slot);
15880
+ if (owner === 0) continue;
15881
+ judgeStillOwner(pc, slot, owner, out);
15882
+ if (cellF64(problems.secFrames, slot) === 0) {
15883
+ owners.ids[slot] = 0;
15884
+ problems.run[slot] = 0;
15885
+ }
15886
+ problems.secFrames[slot] = 0;
15887
+ problems.secPixels[slot] = 0;
15888
+ }
15889
+ }
15890
+ /**
15891
+ * Why `target-churn` opens: a create soon after a pool trim wins over a create in an idle frame, and both over the
15892
+ * rate.
15893
+ *
15894
+ * @param problems - The problem state.
15895
+ * @returns The reason of the problem line.
15896
+ */
15897
+ function churnReason(problems) {
15898
+ if (problems.trimCreate) return "after-trim";
15899
+ if (problems.idleCreate) return "idle";
15900
+ return "rate";
15901
+ }
15902
+ /**
15903
+ * Judges `target-churn`: `churnPerSecond` targets made and dropped in each of `churnSeconds` seconds in a row, a
15904
+ * create in an idle frame, or a create within `churnAfterTrimMs` after a pool trim. One problem for the whole canvas.
15905
+ *
15906
+ * @param pc - The perf context.
15907
+ * @param out - The details written now.
15908
+ */
15909
+ function judgeChurn(pc, out) {
15910
+ const { problems } = pc.perf;
15911
+ const { churnPerSecond, churnSeconds } = problems.thresholds;
15912
+ const busy = problems.secTargets >= churnPerSecond;
15913
+ if (busy && problems.churnRun === 0) problems.churnSince = pc.perf.frame;
15914
+ problems.churnRun = busy ? problems.churnRun + 1 : 0;
15915
+ const steady = problems.churnRun >= churnSeconds;
15916
+ const churns = steady || problems.idleCreate || problems.trimCreate;
15917
+ if (churns && !problems.churnOpen) {
15918
+ const since = steady ? problems.churnSince : problems.createFrame;
15919
+ problems.churnOpen = true;
15920
+ out.push({
15921
+ rule: "target-churn",
15922
+ severity: "warn",
15923
+ owner: null,
15924
+ view: null,
15925
+ filter: null,
15926
+ value: problems.secTargets,
15927
+ threshold: churnPerSecond,
15928
+ since,
15929
+ frames: Math.max(1, pc.perf.frame - since),
15930
+ reason: churnReason(problems)
15931
+ });
15932
+ } else if (!churns && problems.churnOpen) {
15933
+ problems.churnOpen = false;
15934
+ out.push({
15935
+ rule: "target-churn",
15936
+ cleared: true,
15937
+ owner: null,
15938
+ view: null
15939
+ });
15940
+ }
15941
+ problems.secTargets = 0;
15942
+ problems.idleCreate = false;
15943
+ problems.trimCreate = false;
15944
+ }
15945
+ /**
15946
+ * The stage-1 takes of a kept record in the last two seconds, its first take left out: the takes now less the ones of
15947
+ * two flushes back (one back for a record that came since, none for a record of this second).
15948
+ *
15949
+ * @param state - The problem state.
15950
+ * @param record - The record.
15951
+ * @returns Its retakes in the window.
15952
+ */
15953
+ function retakesOf(state, record) {
15954
+ const before = state.takes[1].get(record.entity) ?? state.takes[0].get(record.entity) ?? 1;
15955
+ return Math.max(0, record.s1Version - Math.max(1, before));
15956
+ }
15957
+ /**
15958
+ * Judges `retakes`: a kept view taken again `RETAKES_LIMIT` times or more in two seconds. Its `value` is the takes per
15959
+ * second. A record that left clears its problem. The takes of every record are kept for the next two judgments.
15960
+ *
15961
+ * @param pc - The perf context.
15962
+ * @param out - The details written now.
15963
+ */
15964
+ function judgeRetakes(pc, out) {
15965
+ const { problems } = pc.perf;
15966
+ const { records } = pc.ctx.state.kept;
15967
+ const now = /* @__PURE__ */ new Map();
15968
+ const since = problems.takesFrame[1];
15969
+ for (const record of records) {
15970
+ const { entity } = record;
15971
+ const count = retakesOf(problems, record);
15972
+ const open = problems.retakesOpen.has(entity);
15973
+ now.set(entity, record.s1Version);
15974
+ if (count >= 3 && !open) {
15975
+ problems.retakesOpen.add(entity);
15976
+ out.push({
15977
+ rule: "retakes",
15978
+ severity: "info",
15979
+ owner: entity,
15980
+ view: viewLabel(pc, entity),
15981
+ filter: record.slots[0]?.kind ?? null,
15982
+ value: roundTo(count / RETAKES_SECONDS, 2),
15983
+ threshold: 3 / RETAKES_SECONDS,
15984
+ since,
15985
+ frames: Math.max(1, pc.perf.frame - since)
15986
+ });
15987
+ } else if (count < 3 && open) {
15988
+ problems.retakesOpen.delete(entity);
15989
+ out.push({
15990
+ rule: "retakes",
15991
+ cleared: true,
15992
+ owner: entity,
15993
+ view: viewLabel(pc, entity)
15994
+ });
15995
+ }
15996
+ }
15997
+ for (const entity of problems.retakesOpen) {
15998
+ if (now.has(entity)) continue;
15999
+ problems.retakesOpen.delete(entity);
16000
+ out.push({
16001
+ rule: "retakes",
16002
+ cleared: true,
16003
+ owner: entity,
16004
+ view: viewLabel(pc, entity)
16005
+ });
16006
+ }
16007
+ problems.takes = [now, problems.takes[0]];
16008
+ problems.takesFrame = [pc.perf.frame, problems.takesFrame[0]];
16009
+ }
16010
+ /**
16011
+ * The once-a-second judgment of the three rules, at the flush: the details of the problems that opened or cleared.
16012
+ * Allocates: the flush is allowed to.
16013
+ *
16014
+ * @param pc - The perf context.
16015
+ * @returns The details, in rule order.
16016
+ */
16017
+ function judgeProblems(pc) {
16018
+ const out = [];
16019
+ judgeStill(pc, out);
16020
+ judgeChurn(pc, out);
16021
+ judgeRetakes(pc, out);
16022
+ return out;
16023
+ }
16024
+ /**
16025
+ * Forgets every accumulator and every open problem: the renderer stopped.
16026
+ *
16027
+ * @param pc - The perf context's sinks and perf state.
16028
+ * @param pc.perf - The perf state.
16029
+ * @param pc.sinks - The sinks.
16030
+ */
16031
+ function resetProblems(pc) {
16032
+ const { problems } = pc.perf;
16033
+ pc.sinks.owners.ids.fill(0);
16034
+ problems.run.fill(0);
16035
+ problems.secFrames.fill(0);
16036
+ problems.secPixels.fill(0);
16037
+ problems.stillOpen.fill(0);
16038
+ problems.secTargets = 0;
16039
+ problems.idleCreate = false;
16040
+ problems.trimCreate = false;
16041
+ problems.trimAt = NaN;
16042
+ problems.churnRun = 0;
16043
+ problems.churnOpen = false;
16044
+ problems.takes = [/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map()];
16045
+ problems.takesFrame = [0, 0];
16046
+ problems.retakesOpen.clear();
16047
+ }
16048
+ //#endregion
15566
16049
  //#region src/plugins/renderer/monitor/log-types.ts
15567
16050
  /**
15568
16051
  * The version every line carries as `v`.
@@ -15593,6 +16076,35 @@ const PERF_CAUSES = [
15593
16076
  "longFrame",
15594
16077
  "gc?"
15595
16078
  ];
16079
+ /**
16080
+ * What a `shader` event line reports, in its `what`: a render pipeline (WebGPU) or a program (WebGL) created, or a
16081
+ * WebGPU shader module compiled. New values are only ever appended.
16082
+ *
16083
+ * @example
16084
+ * ```ts
16085
+ * // The editor counts the pipelines of a recording without a switch over strings it does not know.
16086
+ * const pipelines = lines.filter(line => line.kind === "event" && line.name === "shader" && line.detail.what === SHADER_WHATS[0]).length; // 14
16087
+ * ```
16088
+ */
16089
+ const SHADER_WHATS = ["compile", "module"];
16090
+ /**
16091
+ * The rules of the `problem` event lines, judged once a second: `filter-still` (a filtered view stood still and was
16092
+ * filtered again in every drawn frame), `target-churn` (render targets made and dropped all the time, or right after a
16093
+ * pool trim, or while idle), `retakes` (a kept view taken again and again). New rules are only ever appended.
16094
+ *
16095
+ * @example
16096
+ * ```ts
16097
+ * // The editor reads the rule of the first problem a recording opened: a fog blur filtered again on a still board.
16098
+ * const problems = lines.filter((line): line is PerfEventLine => line.kind === "event" && line.name === "problem");
16099
+ * const opened = problems.find(line => line.detail.cleared !== true);
16100
+ * PROBLEM_RULES.find(rule => rule === opened?.detail.rule); // "filter-still"
16101
+ * ```
16102
+ */
16103
+ const PROBLEM_RULES = [
16104
+ "filter-still",
16105
+ "target-churn",
16106
+ "retakes"
16107
+ ];
15596
16108
  //#endregion
15597
16109
  //#region src/plugins/renderer/monitor/ring.ts
15598
16110
  /**
@@ -15637,12 +16149,26 @@ const CHANGE_NAMES = [
15637
16149
  "bundle",
15638
16150
  "restore"
15639
16151
  ];
15640
- /** The pass kinds by their index in the pass ring. */
16152
+ /**
16153
+ * The pass kinds by their index in the pass ring's `kind` column: 0 the canvas, 1 a texture, 2 a filter. A root export,
16154
+ * so a reader of the ring names a pass's kind without building a record.
16155
+ *
16156
+ * @example
16157
+ * ```ts
16158
+ * // The editor's pass strip names the newest pass of a debug build from the ring.
16159
+ * const { passes } = app.renderer.monitor.frames();
16160
+ * PASS_KINDS[passes.kind[passes.newest] ?? 0]; // "filter"
16161
+ * ```
16162
+ */
15641
16163
  const PASS_KINDS = [
15642
16164
  "canvas",
15643
16165
  "texture",
15644
16166
  "filter"
15645
16167
  ];
16168
+ /** The prefix of the label of a pass that took or refreshed a kept picture (`kept:<view>#n`). */
16169
+ const KEPT_PREFIX = "kept:";
16170
+ /** The largest draw count the 16-bit `drawsAt` column holds. */
16171
+ const DRAWS_MAX = 65535;
15646
16172
  /**
15647
16173
  * Creates the ring of the changes with a subject: 64 slots, empty.
15648
16174
  *
@@ -15684,7 +16210,7 @@ function noteSubject(events, frame, change, what) {
15684
16210
  function subjectOf(events, frame, bit) {
15685
16211
  for (let age = 0; age < events.size; age += 1) {
15686
16212
  const slot = (events.next - 1 - age + 64) % 64;
15687
- if (cell(events.frames, slot) === frame && cell(events.kinds, slot) === bit) return events.whats[slot] ?? null;
16213
+ if (cellU32(events.frames, slot) === frame && cellU8(events.kinds, slot) === bit) return events.whats[slot] ?? null;
15688
16214
  }
15689
16215
  return null;
15690
16216
  }
@@ -15732,7 +16258,9 @@ function createColumns(capacity) {
15732
16258
  fltApplies: new Uint16Array(capacity * 4),
15733
16259
  fltMpx: new Float32Array(capacity * 4),
15734
16260
  kept: new Float32Array(capacity).fill(NaN),
15735
- retakes: new Float32Array(capacity).fill(NaN)
16261
+ retakes: new Float32Array(capacity).fill(NaN),
16262
+ targetsCreated: new Float32Array(capacity).fill(NaN),
16263
+ targetsDestroyed: new Float32Array(capacity).fill(NaN)
15736
16264
  };
15737
16265
  }
15738
16266
  /**
@@ -15754,11 +16282,86 @@ function createPasses(capacity) {
15754
16282
  width: new Uint16Array(capacity),
15755
16283
  height: new Uint16Array(capacity),
15756
16284
  gpuMs: new Float32Array(capacity),
15757
- label: Array.from({ length: capacity }).fill(null)
16285
+ label: Array.from({ length: capacity }).fill(null),
16286
+ owner: new Uint32Array(capacity),
16287
+ kind2: new Uint8Array(capacity),
16288
+ drawsAt: new Uint16Array(capacity),
16289
+ format: Array.from({ length: capacity }).fill(null),
16290
+ resolution: new Float32Array(capacity).fill(NaN)
15758
16291
  };
15759
16292
  }
15760
16293
  /**
15761
- * Copies one column of `count` frames, oldest first, into the caller's array, an indexed loop over the ring.
16294
+ * Copies one `Float32Array` column of `count` frames, oldest first, into the caller's array, an indexed loop over the
16295
+ * ring. One copy per array kind, as one column reader per kind: its loads and stores see one kind of array, and a
16296
+ * double is copied unboxed.
16297
+ *
16298
+ * @param target - The caller's array; nothing is copied without one.
16299
+ * @param source - The ring's column.
16300
+ * @param stride - Values per frame.
16301
+ * @param ring - The ring.
16302
+ * @param oldest - Slot of the oldest frame to copy.
16303
+ * @param count - Frames to copy.
16304
+ */
16305
+ function copyF32(target, source, stride, ring, oldest, count) {
16306
+ if (target === void 0) return;
16307
+ for (let index = 0; index < count; index += 1) {
16308
+ const slot = (oldest + index) % ring.capacity;
16309
+ for (let value = 0; value < stride; value += 1) target[index * stride + value] = cellF32(source, slot * stride + value);
16310
+ }
16311
+ }
16312
+ /**
16313
+ * Copies one `Float64Array` column of `count` frames, oldest first, into the caller's array: `copyF32` for its kind.
16314
+ *
16315
+ * @param target - The caller's array; nothing is copied without one.
16316
+ * @param source - The ring's column.
16317
+ * @param stride - Values per frame.
16318
+ * @param ring - The ring.
16319
+ * @param oldest - Slot of the oldest frame to copy.
16320
+ * @param count - Frames to copy.
16321
+ */
16322
+ function copyF64(target, source, stride, ring, oldest, count) {
16323
+ if (target === void 0) return;
16324
+ for (let index = 0; index < count; index += 1) {
16325
+ const slot = (oldest + index) % ring.capacity;
16326
+ for (let value = 0; value < stride; value += 1) target[index * stride + value] = cellF64(source, slot * stride + value);
16327
+ }
16328
+ }
16329
+ /**
16330
+ * Copies one `Uint32Array` column of `count` frames, oldest first, into the caller's array: `copyF32` for its kind.
16331
+ *
16332
+ * @param target - The caller's array; nothing is copied without one.
16333
+ * @param source - The ring's column.
16334
+ * @param stride - Values per frame.
16335
+ * @param ring - The ring.
16336
+ * @param oldest - Slot of the oldest frame to copy.
16337
+ * @param count - Frames to copy.
16338
+ */
16339
+ function copyU32(target, source, stride, ring, oldest, count) {
16340
+ if (target === void 0) return;
16341
+ for (let index = 0; index < count; index += 1) {
16342
+ const slot = (oldest + index) % ring.capacity;
16343
+ for (let value = 0; value < stride; value += 1) target[index * stride + value] = cellU32(source, slot * stride + value);
16344
+ }
16345
+ }
16346
+ /**
16347
+ * Copies one `Uint16Array` column of `count` frames, oldest first, into the caller's array: `copyF32` for its kind.
16348
+ *
16349
+ * @param target - The caller's array; nothing is copied without one.
16350
+ * @param source - The ring's column.
16351
+ * @param stride - Values per frame.
16352
+ * @param ring - The ring.
16353
+ * @param oldest - Slot of the oldest frame to copy.
16354
+ * @param count - Frames to copy.
16355
+ */
16356
+ function copyU16(target, source, stride, ring, oldest, count) {
16357
+ if (target === void 0) return;
16358
+ for (let index = 0; index < count; index += 1) {
16359
+ const slot = (oldest + index) % ring.capacity;
16360
+ for (let value = 0; value < stride; value += 1) target[index * stride + value] = cellU16(source, slot * stride + value);
16361
+ }
16362
+ }
16363
+ /**
16364
+ * Copies one `Uint8Array` column of `count` frames, oldest first, into the caller's array: `copyF32` for its kind.
15762
16365
  *
15763
16366
  * @param target - The caller's array; nothing is copied without one.
15764
16367
  * @param source - The ring's column.
@@ -15767,11 +16370,11 @@ function createPasses(capacity) {
15767
16370
  * @param oldest - Slot of the oldest frame to copy.
15768
16371
  * @param count - Frames to copy.
15769
16372
  */
15770
- function copyColumn(target, source, stride, ring, oldest, count) {
16373
+ function copyU8(target, source, stride, ring, oldest, count) {
15771
16374
  if (target === void 0) return;
15772
16375
  for (let index = 0; index < count; index += 1) {
15773
16376
  const slot = (oldest + index) % ring.capacity;
15774
- for (let value = 0; value < stride; value += 1) target[index * stride + value] = cell(source, slot * stride + value);
16377
+ for (let value = 0; value < stride; value += 1) target[index * stride + value] = cellU8(source, slot * stride + value);
15775
16378
  }
15776
16379
  }
15777
16380
  /**
@@ -15791,43 +16394,45 @@ function readRing(ring, into, since) {
15791
16394
  result.lost = available - count;
15792
16395
  if (count === 0) return result;
15793
16396
  const oldest = (ring.newest - count + 1 + ring.capacity) % ring.capacity;
15794
- copyColumn(into.frame, columns.frame, 1, ring, oldest, count);
15795
- copyColumn(into.t, columns.t, 1, ring, oldest, count);
15796
- copyColumn(into.gapMs, columns.gapMs, 1, ring, oldest, count);
15797
- copyColumn(into.capMs, columns.capMs, 1, ring, oldest, count);
15798
- copyColumn(into.skipped, columns.skipped, 1, ring, oldest, count);
15799
- copyColumn(into.startMs, columns.startMs, 1, ring, oldest, count);
15800
- copyColumn(into.rateMs, columns.rateMs, 1, ring, oldest, count);
15801
- copyColumn(into.budgetMs, columns.budgetMs, 1, ring, oldest, count);
15802
- copyColumn(into.phaseMs, columns.phaseMs, 6, ring, oldest, count);
15803
- copyColumn(into.agentMs, columns.agentMs, 1, ring, oldest, count);
15804
- copyColumn(into.workMs, columns.workMs, 1, ring, oldest, count);
15805
- copyColumn(into.drawn, columns.drawn, 1, ring, oldest, count);
15806
- copyColumn(into.idle, columns.idle, 1, ring, oldest, count);
15807
- copyColumn(into.stepped, columns.stepped, 1, ring, oldest, count);
15808
- copyColumn(into.passes, columns.passes, 1, ring, oldest, count);
15809
- copyColumn(into.stencilPasses, columns.stencilPasses, 1, ring, oldest, count);
15810
- copyColumn(into.draws, columns.draws, 1, ring, oldest, count);
15811
- copyColumn(into.filterApplies, columns.filterApplies, 1, ring, oldest, count);
15812
- copyColumn(into.filterMpx, columns.filterMpx, 1, ring, oldest, count);
15813
- copyColumn(into.uploads, columns.uploads, 1, ring, oldest, count);
15814
- copyColumn(into.uploadMb, columns.uploadMb, 1, ring, oldest, count);
15815
- copyColumn(into.compiles, columns.compiles, 1, ring, oldest, count);
15816
- copyColumn(into.marks, columns.marks, 1, ring, oldest, count);
15817
- copyColumn(into.rebuilds, columns.rebuilds, 1, ring, oldest, count);
15818
- copyColumn(into.gpuMs, columns.gpuMs, 1, ring, oldest, count);
15819
- copyColumn(into.gpuPartial, columns.gpuPartial, 1, ring, oldest, count);
15820
- copyColumn(into.path, columns.path, 1, ring, oldest, count);
15821
- copyColumn(into.changed, columns.changed, 1, ring, oldest, count);
15822
- copyColumn(into.causes, columns.causes, 1, ring, oldest, count);
15823
- copyColumn(into.reason, columns.reason, 1, ring, oldest, count);
15824
- copyColumn(into.sysIdx, columns.sysIdx, 5, ring, oldest, count);
15825
- copyColumn(into.sysMs, columns.sysMs, 5, ring, oldest, count);
15826
- copyColumn(into.fltKind, columns.fltKind, 4, ring, oldest, count);
15827
- copyColumn(into.fltApplies, columns.fltApplies, 4, ring, oldest, count);
15828
- copyColumn(into.fltMpx, columns.fltMpx, 4, ring, oldest, count);
15829
- copyColumn(into.kept, columns.kept, 1, ring, oldest, count);
15830
- copyColumn(into.retakes, columns.retakes, 1, ring, oldest, count);
16397
+ copyU32(into.frame, columns.frame, 1, ring, oldest, count);
16398
+ copyF64(into.t, columns.t, 1, ring, oldest, count);
16399
+ copyF32(into.gapMs, columns.gapMs, 1, ring, oldest, count);
16400
+ copyF32(into.capMs, columns.capMs, 1, ring, oldest, count);
16401
+ copyU8(into.skipped, columns.skipped, 1, ring, oldest, count);
16402
+ copyF32(into.startMs, columns.startMs, 1, ring, oldest, count);
16403
+ copyF32(into.rateMs, columns.rateMs, 1, ring, oldest, count);
16404
+ copyF32(into.budgetMs, columns.budgetMs, 1, ring, oldest, count);
16405
+ copyF32(into.phaseMs, columns.phaseMs, 6, ring, oldest, count);
16406
+ copyF32(into.agentMs, columns.agentMs, 1, ring, oldest, count);
16407
+ copyF32(into.workMs, columns.workMs, 1, ring, oldest, count);
16408
+ copyU8(into.drawn, columns.drawn, 1, ring, oldest, count);
16409
+ copyU8(into.idle, columns.idle, 1, ring, oldest, count);
16410
+ copyU8(into.stepped, columns.stepped, 1, ring, oldest, count);
16411
+ copyF32(into.passes, columns.passes, 1, ring, oldest, count);
16412
+ copyF32(into.stencilPasses, columns.stencilPasses, 1, ring, oldest, count);
16413
+ copyF32(into.draws, columns.draws, 1, ring, oldest, count);
16414
+ copyF32(into.filterApplies, columns.filterApplies, 1, ring, oldest, count);
16415
+ copyF32(into.filterMpx, columns.filterMpx, 1, ring, oldest, count);
16416
+ copyF32(into.uploads, columns.uploads, 1, ring, oldest, count);
16417
+ copyF32(into.uploadMb, columns.uploadMb, 1, ring, oldest, count);
16418
+ copyF32(into.compiles, columns.compiles, 1, ring, oldest, count);
16419
+ copyF32(into.marks, columns.marks, 1, ring, oldest, count);
16420
+ copyF32(into.rebuilds, columns.rebuilds, 1, ring, oldest, count);
16421
+ copyF32(into.gpuMs, columns.gpuMs, 1, ring, oldest, count);
16422
+ copyU8(into.gpuPartial, columns.gpuPartial, 1, ring, oldest, count);
16423
+ copyU16(into.path, columns.path, 1, ring, oldest, count);
16424
+ copyU8(into.changed, columns.changed, 1, ring, oldest, count);
16425
+ copyU8(into.causes, columns.causes, 1, ring, oldest, count);
16426
+ copyU8(into.reason, columns.reason, 1, ring, oldest, count);
16427
+ copyU16(into.sysIdx, columns.sysIdx, 5, ring, oldest, count);
16428
+ copyF32(into.sysMs, columns.sysMs, 5, ring, oldest, count);
16429
+ copyU8(into.fltKind, columns.fltKind, 4, ring, oldest, count);
16430
+ copyU16(into.fltApplies, columns.fltApplies, 4, ring, oldest, count);
16431
+ copyF32(into.fltMpx, columns.fltMpx, 4, ring, oldest, count);
16432
+ copyF32(into.kept, columns.kept, 1, ring, oldest, count);
16433
+ copyF32(into.retakes, columns.retakes, 1, ring, oldest, count);
16434
+ copyF32(into.targetsCreated, columns.targetsCreated, 1, ring, oldest, count);
16435
+ copyF32(into.targetsDestroyed, columns.targetsDestroyed, 1, ring, oldest, count);
15831
16436
  return result;
15832
16437
  }
15833
16438
  /**
@@ -15864,7 +16469,8 @@ function createFrameRing(capacity, events = createEventRing(), kindNames = []) {
15864
16469
  */
15865
16470
  const EMPTY_RING = /*#__PURE__*/ createFrameRing(0);
15866
16471
  /**
15867
- * Records one render pass in the pass ring, the label by reference.
16472
+ * Records one render pass in the pass ring, the label by reference. The detail of the pass (its owner, filter kind,
16473
+ * draw count, format and resolution) goes into the same slot with `writePassDetail`, right after.
15868
16474
  *
15869
16475
  * @param passes - The pass ring.
15870
16476
  * @param frame - The frame number.
@@ -15887,12 +16493,36 @@ function writePass(passes, frame, index, kind, stencil, width, height, label) {
15887
16493
  passes.height[slot] = height;
15888
16494
  passes.gpuMs[slot] = NaN;
15889
16495
  passes.label[slot] = label;
16496
+ passes.owner[slot] = 0;
16497
+ passes.kind2[slot] = 0;
16498
+ passes.drawsAt[slot] = 0;
16499
+ passes.format[slot] = null;
16500
+ passes.resolution[slot] = NaN;
15890
16501
  passes.newest = slot;
15891
16502
  passes.size = Math.min(passes.size + 1, passes.capacity);
15892
16503
  passes.written = sequence + 1;
15893
16504
  return sequence;
15894
16505
  }
15895
16506
  /**
16507
+ * Writes the detail of the newest pass: the entity whose filter drew it, the filter kind (1-based, 0 none), the draw
16508
+ * count of the frame when it began, and its target's format (by reference) and resolution, read from the target's
16509
+ * source here so no double is handed over. Indexed stores only.
16510
+ *
16511
+ * @param passes - The pass ring, its newest slot the pass `writePass` just wrote.
16512
+ * @param owner - The entity, 0 for none.
16513
+ * @param kind2 - The interned filter kind plus one, 0 for none.
16514
+ * @param drawsAt - The frame's draws so far.
16515
+ * @param source - The source of the target's color texture, or `undefined`.
16516
+ */
16517
+ function writePassDetail(passes, owner, kind2, drawsAt, source) {
16518
+ const slot = passes.newest;
16519
+ passes.owner[slot] = owner;
16520
+ passes.kind2[slot] = kind2;
16521
+ passes.drawsAt[slot] = Math.min(drawsAt, DRAWS_MAX);
16522
+ passes.format[slot] = source?.format ?? null;
16523
+ passes.resolution[slot] = source?.resolution ?? NaN;
16524
+ }
16525
+ /**
15896
16526
  * The commit number of the frame in a slot: `written − 1` for the newest.
15897
16527
  *
15898
16528
  * @param ring - The ring.
@@ -15959,13 +16589,13 @@ function spikeCauses(frame, agentMs, budget, previousCapMs) {
15959
16589
  return causes;
15960
16590
  }
15961
16591
  /**
15962
- * Tells whether a ring is the monitor's own: the one that knows the names its columns index.
16592
+ * Tells whether a ring is the monitor's own: the one that keeps the subjects of its changes.
15963
16593
  *
15964
16594
  * @param ring - A ring.
15965
16595
  * @returns True for a ring of `monitor.frames()`.
15966
16596
  */
15967
16597
  function isPerfRing(ring) {
15968
- return "events" in ring && "kindNames" in ring && "systemNames" in ring;
16598
+ return "events" in ring;
15969
16599
  }
15970
16600
  /**
15971
16601
  * Milliseconds or megabytes of a column as a record carries them: 2 decimals, so a `Float32Array`
@@ -16012,17 +16642,17 @@ function namesOf$3(bits, names) {
16012
16642
  * @returns Kind, applies and megapixels, the most pixels first.
16013
16643
  */
16014
16644
  function filterKindsAt(ring, slot) {
16015
- const names = isPerfRing(ring) ? ring.kindNames : [];
16645
+ const names = ring.kindNames;
16016
16646
  const { columns } = ring;
16017
16647
  const kinds = [];
16018
16648
  for (let place = 0; place < 4; place += 1) {
16019
16649
  const at = slot * 4 + place;
16020
- const kind = cell(columns.fltKind, at);
16650
+ const kind = cellU8(columns.fltKind, at);
16021
16651
  if (kind === 0) continue;
16022
16652
  kinds.push({
16023
16653
  kind: names[kind - 1] ?? "other",
16024
- applies: cell(columns.fltApplies, at),
16025
- mpx: two(cell(columns.fltMpx, at))
16654
+ applies: cellU16(columns.fltApplies, at),
16655
+ mpx: two(cellF32(columns.fltMpx, at))
16026
16656
  });
16027
16657
  }
16028
16658
  return kinds;
@@ -16036,16 +16666,16 @@ function filterKindsAt(ring, slot) {
16036
16666
  */
16037
16667
  function systemsAt(ring, slot) {
16038
16668
  const { columns } = ring;
16039
- if (Number.isNaN(cell(columns.sysMs, slot * 5))) return null;
16040
- const names = isPerfRing(ring) ? ring.systemNames : [];
16669
+ if (Number.isNaN(cellF32(columns.sysMs, slot * 5))) return null;
16670
+ const names = ring.systemNames;
16041
16671
  const systems = [];
16042
16672
  for (let place = 0; place < 5; place += 1) {
16043
16673
  const at = slot * 5 + place;
16044
- const index = cell(columns.sysIdx, at);
16674
+ const index = cellU16(columns.sysIdx, at);
16045
16675
  if (index === 65535) continue;
16046
16676
  systems.push({
16047
16677
  name: names[index] ?? String(index),
16048
- ms: two(cell(columns.sysMs, at))
16678
+ ms: two(cellF32(columns.sysMs, at))
16049
16679
  });
16050
16680
  }
16051
16681
  return systems;
@@ -16058,39 +16688,73 @@ function systemsAt(ring, slot) {
16058
16688
  * @returns One entry per change, in bit order.
16059
16689
  */
16060
16690
  function changesAt(ring, slot) {
16061
- const bits = cell(ring.columns.changed, slot);
16062
- const frame = cell(ring.columns.frame, slot);
16691
+ const bits = cellU8(ring.columns.changed, slot);
16692
+ const frame = cellU32(ring.columns.frame, slot);
16063
16693
  return namesOf$3(bits, CHANGE_NAMES).map((kind) => ({
16064
16694
  kind,
16065
16695
  what: isPerfRing(ring) ? subjectOf(ring.events, frame, CHANGED[kind]) : null
16066
16696
  }));
16067
16697
  }
16068
16698
  /**
16069
- * The render passes of a frame still in the pass ring, in their order.
16699
+ * The draws of each pass of a frame from the snapshots its passes took when they began: up to the next pass's
16700
+ * snapshot, the last up to the frame's total. Pure: rewrites `draws` in place.
16701
+ *
16702
+ * @param found - The passes of the frame, in order, with `draws` holding their snapshot.
16703
+ * @param total - The frame's draws; NaN where draws are not counted, which makes every `draws` `null`.
16704
+ * @example
16705
+ * ```ts
16706
+ * splitDraws([{ draws: 0 }, { draws: 5 }], 7); // the passes now say 5 and 2
16707
+ * ```
16708
+ */
16709
+ function splitDraws(found, total) {
16710
+ for (let index = 0; index < found.length; index += 1) {
16711
+ const pass = found[index];
16712
+ if (pass === void 0) continue;
16713
+ const next = found[index + 1]?.draws ?? total;
16714
+ pass.draws = Number.isNaN(total) ? null : Math.max(0, next - (pass.draws ?? 0));
16715
+ }
16716
+ }
16717
+ /**
16718
+ * The render passes of a frame still in the pass ring, in their order, with their detail: the entity whose filter drew
16719
+ * each and its view's label, the filter kind, the draws, the target's format and resolution, whether it was a kept
16720
+ * picture's.
16070
16721
  *
16071
16722
  * @param ring - The ring.
16072
16723
  * @param frame - The frame number.
16724
+ * @param total - The frame's draws (its `draws` column); NaN where draws are not counted.
16725
+ * @param viewOf - The label of an entity's view now, or `null` once it left.
16073
16726
  * @returns The passes; empty when none was recorded or they were overwritten.
16074
16727
  */
16075
- function passesOf$1(ring, frame) {
16728
+ function passesOf$1(ring, frame, total = NaN, viewOf = () => null) {
16076
16729
  const { passes } = ring;
16077
16730
  const found = [];
16078
16731
  for (let age = 0; age < passes.size; age += 1) {
16079
16732
  const slot = (passes.newest - age + passes.capacity) % passes.capacity;
16080
- const owner = cell(passes.frame, slot);
16081
- if (owner < frame) break;
16082
- if (owner !== frame) continue;
16083
- const stencil = cell(passes.stencil, slot);
16733
+ const at = cellU32(passes.frame, slot);
16734
+ if (at < frame) break;
16735
+ if (at !== frame) continue;
16736
+ const stencil = cellU8(passes.stencil, slot);
16737
+ const label = passes.label[slot] ?? null;
16738
+ const owner = cellU32(passes.owner, slot);
16739
+ const kind2 = cellU8(passes.kind2, slot);
16084
16740
  found.unshift({
16085
- index: cell(passes.index, slot),
16086
- kind: PASS_KINDS[cell(passes.kind, slot)] ?? "texture",
16087
- label: passes.label[slot] ?? null,
16741
+ index: cellU8(passes.index, slot),
16742
+ kind: PASS_KINDS[cellU8(passes.kind, slot)] ?? "texture",
16743
+ label,
16088
16744
  stencil: stencil === 255 ? null : stencil === 1,
16089
- w: cell(passes.width, slot),
16090
- h: cell(passes.height, slot),
16091
- gpuMs: measured(cell(passes.gpuMs, slot))
16745
+ w: cellU16(passes.width, slot),
16746
+ h: cellU16(passes.height, slot),
16747
+ gpuMs: measured(cellF32(passes.gpuMs, slot)),
16748
+ owner: owner === 0 ? null : owner,
16749
+ view: owner === 0 ? null : viewOf(owner),
16750
+ filter: kind2 === 0 ? null : ring.kindNames[kind2 - 1] ?? "other",
16751
+ draws: cellU16(passes.drawsAt, slot),
16752
+ format: passes.format[slot] ?? null,
16753
+ resolution: measured(cellF32(passes.resolution, slot)),
16754
+ kept: label?.startsWith(KEPT_PREFIX) === true
16092
16755
  });
16093
16756
  }
16757
+ splitDraws(found, total);
16094
16758
  return found;
16095
16759
  }
16096
16760
  /**
@@ -16103,56 +16767,56 @@ function passesOf$1(ring, frame) {
16103
16767
  function recordAt(ring, slot) {
16104
16768
  const c = ring.columns;
16105
16769
  const phase = slot * 6;
16106
- const applies = measured(cell(c.filterApplies, slot));
16107
- const uploads = measured(cell(c.uploads, slot));
16108
- const kept = measured(cell(c.kept, slot));
16109
- return {
16110
- frame: cell(c.frame, slot),
16111
- t: two(cell(c.t, slot)),
16112
- gapMs: measured(cell(c.gapMs, slot)),
16113
- capMs: two(cell(c.capMs, slot)),
16114
- rateMs: two(cell(c.rateMs, slot)),
16115
- budgetMs: two(cell(c.budgetMs, slot)),
16116
- skipped: cell(c.skipped, slot),
16117
- startMs: two(cell(c.startMs, slot)),
16770
+ const applies = measured(cellF32(c.filterApplies, slot));
16771
+ const uploads = measured(cellF32(c.uploads, slot));
16772
+ const kept = measured(cellF32(c.kept, slot));
16773
+ return {
16774
+ frame: cellU32(c.frame, slot),
16775
+ t: two(cellF64(c.t, slot)),
16776
+ gapMs: measured(cellF32(c.gapMs, slot)),
16777
+ capMs: two(cellF32(c.capMs, slot)),
16778
+ rateMs: two(cellF32(c.rateMs, slot)),
16779
+ budgetMs: two(cellF32(c.budgetMs, slot)),
16780
+ skipped: cellU8(c.skipped, slot),
16781
+ startMs: two(cellF32(c.startMs, slot)),
16118
16782
  phases: {
16119
- input: two(cell(c.phaseMs, phase)),
16120
- animate: two(cell(c.phaseMs, phase + 1)),
16121
- layout: two(cell(c.phaseMs, phase + 2)),
16122
- sync: two(cell(c.phaseMs, phase + 3)),
16123
- signals: two(cell(c.phaseMs, phase + 4)),
16124
- render: two(cell(c.phaseMs, phase + 5)),
16125
- agent: two(cell(c.agentMs, slot))
16126
- },
16127
- workMs: two(cell(c.workMs, slot)),
16128
- drawn: cell(c.drawn, slot) === 1,
16129
- idle: cell(c.idle, slot) === 1,
16130
- stepped: cell(c.stepped, slot) === 1,
16131
- reason: DEMAND_REASONS[cell(c.reason, slot)] ?? null,
16132
- passes: measured(cell(c.passes, slot)),
16133
- stencilPasses: measured(cell(c.stencilPasses, slot)),
16134
- draws: measured(cell(c.draws, slot)),
16783
+ input: two(cellF32(c.phaseMs, phase)),
16784
+ animate: two(cellF32(c.phaseMs, phase + 1)),
16785
+ layout: two(cellF32(c.phaseMs, phase + 2)),
16786
+ sync: two(cellF32(c.phaseMs, phase + 3)),
16787
+ signals: two(cellF32(c.phaseMs, phase + 4)),
16788
+ render: two(cellF32(c.phaseMs, phase + 5)),
16789
+ agent: two(cellF32(c.agentMs, slot))
16790
+ },
16791
+ workMs: two(cellF32(c.workMs, slot)),
16792
+ drawn: cellU8(c.drawn, slot) === 1,
16793
+ idle: cellU8(c.idle, slot) === 1,
16794
+ stepped: cellU8(c.stepped, slot) === 1,
16795
+ reason: DEMAND_REASONS[cellU8(c.reason, slot)] ?? null,
16796
+ passes: measured(cellF32(c.passes, slot)),
16797
+ stencilPasses: measured(cellF32(c.stencilPasses, slot)),
16798
+ draws: measured(cellF32(c.draws, slot)),
16135
16799
  filters: applies === null ? null : {
16136
16800
  applies,
16137
- mpx: two(cell(c.filterMpx, slot)),
16801
+ mpx: two(cellF32(c.filterMpx, slot)),
16138
16802
  kinds: filterKindsAt(ring, slot)
16139
16803
  },
16140
16804
  uploads: uploads === null ? null : {
16141
16805
  count: uploads,
16142
- mb: two(cell(c.uploadMb, slot))
16806
+ mb: two(cellF32(c.uploadMb, slot))
16143
16807
  },
16144
- compiles: measured(cell(c.compiles, slot)),
16145
- marks: measured(cell(c.marks, slot)),
16146
- rebuilds: measured(cell(c.rebuilds, slot)),
16808
+ compiles: measured(cellF32(c.compiles, slot)),
16809
+ marks: measured(cellF32(c.marks, slot)),
16810
+ rebuilds: measured(cellF32(c.rebuilds, slot)),
16147
16811
  systems: systemsAt(ring, slot),
16148
- gpuMs: measured(cell(c.gpuMs, slot)),
16149
- gpuPartial: cell(c.gpuPartial, slot) === 1,
16150
- path: ring.paths[cell(c.path, slot)] ?? "…",
16812
+ gpuMs: measured(cellF32(c.gpuMs, slot)),
16813
+ gpuPartial: cellU8(c.gpuPartial, slot) === 1,
16814
+ path: ring.paths[cellU16(c.path, slot)] ?? "…",
16151
16815
  changed: changesAt(ring, slot),
16152
- causes: namesOf$3(cell(c.causes, slot), PERF_CAUSES),
16816
+ causes: namesOf$3(cellU8(c.causes, slot), PERF_CAUSES),
16153
16817
  kept: kept === null ? null : {
16154
16818
  pictures: kept,
16155
- retakes: cell(c.retakes, slot)
16819
+ retakes: cellF32(c.retakes, slot)
16156
16820
  }
16157
16821
  };
16158
16822
  }
@@ -16175,63 +16839,68 @@ function frameAt(ring, age) {
16175
16839
  return recordAt(ring, (ring.newest - age + ring.capacity) % ring.capacity);
16176
16840
  }
16177
16841
  //#endregion
16178
- //#region src/plugins/renderer/monitor/types.ts
16179
- /**
16180
- * Indices of the per-frame counters in `MonitorSinks.counts`. Not public.
16181
- */
16182
- const COUNT = {
16183
- passes: 0,
16184
- stencilPasses: 1,
16185
- filterApplies: 2,
16186
- filterPixels: 3,
16187
- uploads: 4,
16188
- uploadBytes: 5,
16189
- compiles: 6,
16190
- marks: 7,
16191
- rebuilds: 8
16192
- };
16193
- /**
16194
- * What this application measures, as bits of `PerfState.measuring`. Not public.
16195
- */
16196
- const MEASURE = {
16197
- passes: 1,
16198
- stencilPasses: 2,
16199
- draws: 4,
16200
- counted: 8,
16201
- compiles: 16,
16202
- gpu: 32
16203
- };
16204
- //#endregion
16205
16842
  //#region src/plugins/renderer/monitor/compiles.ts
16206
16843
  /**
16207
16844
  * @file renderer/monitor — pipeline compiles of an agent build (R5). Pixi creates its WebGPU pipelines in a private
16208
16845
  * method, so no subclass sees a creation; the count is taken one level lower, on GPU objects that belong to this
16209
16846
  * application, never on a prototype and never on a Pixi object (OQ4): own properties on the engine's own device, and
16210
16847
  * `linkProgram` on this application's WebGL context. The remover deletes the own property, so the prototype's method
16211
- * shows through again. A restore attaches to the new device or context.
16848
+ * shows through again. A restore attaches to the new device or context. Each record also notes where it happened:
16849
+ * the pass of the frame, and the filter kind and its owner while a filter applies; a compile of a plain batch has
16850
+ * none. The flush turns the records into `shader` events.
16212
16851
  */
16213
16852
  /**
16214
- * Counts one compile of the frame and keeps it in the next of the 64 pre-made records: its frame, its label by
16215
- * reference and the CPU time of the creation call, the part a frame waits for.
16853
+ * Keeps one compile or shader module in the next of the 64 pre-made records: its frame, its label by reference, the
16854
+ * CPU time of the creation call, the part a frame waits for, and the pass, filter kind and owner of the moment.
16216
16855
  *
16217
16856
  * @param sinks - The sinks.
16218
16857
  * @param frame - The frame running now.
16219
- * @param label - The pipeline's label, or `null`.
16858
+ * @param label - The pipeline's or module's label, or `null`.
16220
16859
  * @param ms - The time of the call.
16860
+ * @param module - True for a WebGPU shader module.
16221
16861
  */
16222
- function recordCompile(sinks, frame, label, ms) {
16862
+ function writeRecord(sinks, frame, label, ms, module) {
16223
16863
  const ring = sinks.compiles;
16224
16864
  const record = ring.records[ring.next];
16225
- sinks.counts[COUNT.compiles] = cell(sinks.counts, COUNT.compiles) + 1;
16865
+ const applying = sinks.filterDepth > 0;
16226
16866
  ring.next = (ring.next + 1) % ring.records.length;
16227
16867
  ring.size = Math.min(ring.size + 1, ring.records.length);
16868
+ ring.written += 1;
16228
16869
  if (record === void 0) return;
16229
16870
  record.frame = frame;
16230
16871
  record.label = label;
16231
16872
  record.ms = ms;
16873
+ record.module = module;
16874
+ record.pass = sinks.inFrame ? cellF64(sinks.counts, COUNT.passes) - 1 : -1;
16875
+ record.kind = applying ? sinks.filterKind + 1 : 0;
16876
+ record.owner = applying ? sinks.filterOwner : 0;
16232
16877
  }
16233
16878
  /**
16234
- * The compiles of one frame still in the ring, oldest first.
16879
+ * Counts one compile of the frame and keeps its record.
16880
+ *
16881
+ * @param sinks - The sinks.
16882
+ * @param frame - The frame running now.
16883
+ * @param label - The pipeline's label, or `null`.
16884
+ * @param ms - The time of the call.
16885
+ */
16886
+ function recordCompile(sinks, frame, label, ms) {
16887
+ sinks.counts[COUNT.compiles] = cellF64(sinks.counts, COUNT.compiles) + 1;
16888
+ writeRecord(sinks, frame, label, ms, false);
16889
+ }
16890
+ /**
16891
+ * Keeps the record of one shader module: not a compile of the frame's count, a `shader` event of its own.
16892
+ *
16893
+ * @param sinks - The sinks.
16894
+ * @param frame - The frame running now.
16895
+ * @param label - The module's label, or `null`.
16896
+ * @param ms - The time of the call.
16897
+ */
16898
+ function recordModule(sinks, frame, label, ms) {
16899
+ sinks.compiles.shaderModules += 1;
16900
+ writeRecord(sinks, frame, label, ms, true);
16901
+ }
16902
+ /**
16903
+ * The compiles of one frame still in the ring, oldest first; shader modules left out.
16235
16904
  *
16236
16905
  * @param sinks - The sinks.
16237
16906
  * @param frame - The frame number.
@@ -16242,7 +16911,7 @@ function compilesOf(sinks, frame) {
16242
16911
  const found = [];
16243
16912
  for (let age = ring.size - 1; age >= 0; age -= 1) {
16244
16913
  const record = ring.records[(ring.next - 1 - age + ring.records.length) % ring.records.length];
16245
- if (record?.frame === frame) found.push({
16914
+ if (record?.frame === frame && !record.module) found.push({
16246
16915
  label: record.label,
16247
16916
  ms: record.ms
16248
16917
  });
@@ -16250,6 +16919,40 @@ function compilesOf(sinks, frame) {
16250
16919
  return found;
16251
16920
  }
16252
16921
  /**
16922
+ * The details of the `shader` events not sent yet, oldest first, from the records still in the ring (the 64 newest);
16923
+ * marks them sent. Built at the flush: it allocates.
16924
+ *
16925
+ * @param pc - The perf context.
16926
+ * @param viewOf - The label of an entity's view now, or `null`.
16927
+ * @returns One detail per compile or module.
16928
+ */
16929
+ function takeShaderEvents(pc, viewOf) {
16930
+ const ring = pc.sinks.compiles;
16931
+ const { kindNames } = pc.sinks;
16932
+ const first = Math.max(ring.sent, ring.written - ring.records.length);
16933
+ const boot = pc.perf.firstDrawnFrame;
16934
+ const details = [];
16935
+ for (let sequence = first; sequence < ring.written; sequence += 1) {
16936
+ const record = ring.records[sequence % ring.records.length];
16937
+ if (record === void 0) continue;
16938
+ details.push({
16939
+ what: record.module ? SHADER_WHATS[1] : SHADER_WHATS[0],
16940
+ label: record.label,
16941
+ ms: roundTo(record.ms, 2),
16942
+ frame: record.frame,
16943
+ boot: Number.isNaN(boot) || record.frame <= boot,
16944
+ during: {
16945
+ pass: record.pass < 0 ? null : record.pass,
16946
+ filter: record.kind === 0 ? null : kindNames[record.kind - 1] ?? "other",
16947
+ owner: record.owner === 0 ? null : record.owner,
16948
+ view: record.owner === 0 ? null : viewOf(record.owner)
16949
+ }
16950
+ });
16951
+ }
16952
+ ring.sent = ring.written;
16953
+ return details;
16954
+ }
16955
+ /**
16253
16956
  * Counts the render pipelines and the shader modules the engine's own device creates.
16254
16957
  *
16255
16958
  * @param pc - The perf context.
@@ -16280,14 +16983,17 @@ function wrapDevice(pc, device) {
16280
16983
  configurable: true,
16281
16984
  writable: true,
16282
16985
  /**
16283
- * Creates the shader module and counts it for the header, not per frame.
16986
+ * Creates the shader module and keeps its record: counted for the header and sent as a `shader` event, not a
16987
+ * compile of the frame.
16284
16988
  *
16285
16989
  * @param descriptor - The module descriptor.
16286
16990
  * @returns The module.
16287
16991
  */
16288
16992
  value: function countedModule(descriptor) {
16289
- sinks.compiles.shaderModules += 1;
16290
- return createShaderModule.call(this, descriptor);
16993
+ const started = pageNow();
16994
+ const made = createShaderModule.call(this, descriptor);
16995
+ recordModule(sinks, pc.perf.frame, descriptor.label ?? null, pageNow() - started);
16996
+ return made;
16291
16997
  }
16292
16998
  });
16293
16999
  return () => {
@@ -16334,6 +17040,414 @@ function wrapGl(pc, gl) {
16334
17040
  function attachCompileCounting(pc, target) {
16335
17041
  return "createRenderPipeline" in target ? wrapDevice(pc, target) : wrapGl(pc, target);
16336
17042
  }
17043
+ /**
17044
+ * Creates the estimate: no interval yet, no period.
17045
+ *
17046
+ * @returns The estimate.
17047
+ */
17048
+ function createTickEstimate() {
17049
+ return {
17050
+ samples: /* @__PURE__ */ new Float32Array(9),
17051
+ sorted: /* @__PURE__ */ new Float32Array(9),
17052
+ size: 0,
17053
+ next: 0,
17054
+ periodMs: Float64Array.of(NaN)
17055
+ };
17056
+ }
17057
+ /**
17058
+ * Forgets every interval: the next frames start a new estimate.
17059
+ *
17060
+ * @param ticks - The estimate.
17061
+ */
17062
+ function resetTicks(ticks) {
17063
+ ticks.size = 0;
17064
+ ticks.next = 0;
17065
+ ticks.periodMs[0] = NaN;
17066
+ }
17067
+ /**
17068
+ * Adds the tick interval of a frame and snaps the median of the last nine to a display rate: `periodMs[0]` becomes
17069
+ * that rate's period, or NaN while the median matches none. A stepped frame, a frame without a gap and a gap over
17070
+ * 50 ms (a hitch, a page away) add nothing. The median is taken by an insertion sort into `sorted`: nine numbers.
17071
+ *
17072
+ * @param ticks - The estimate.
17073
+ * @param measure - What `time` measured of the frame; read, never kept.
17074
+ */
17075
+ function noteTicks(ticks, measure) {
17076
+ if (measure.stepped || Number.isNaN(measure.gapMs) || measure.gapMs > 50) return;
17077
+ const { samples, sorted } = ticks;
17078
+ samples[ticks.next] = measure.gapMs / (measure.skipped + 1);
17079
+ ticks.next = (ticks.next + 1) % 9;
17080
+ ticks.size = Math.min(ticks.size + 1, 9);
17081
+ for (let index = 0; index < ticks.size; index += 1) {
17082
+ const value = cellF32(samples, index);
17083
+ let place = index;
17084
+ while (place > 0 && cellF32(sorted, place - 1) > value) {
17085
+ sorted[place] = cellF32(sorted, place - 1);
17086
+ place -= 1;
17087
+ }
17088
+ sorted[place] = value;
17089
+ }
17090
+ const rate = displayIndexOf(cellF32(sorted, ticks.size >> 1));
17091
+ const hz = rate === -1 ? void 0 : DISPLAY_RATES[rate];
17092
+ ticks.periodMs[0] = hz === void 0 ? NaN : 1e3 / hz;
17093
+ }
17094
+ //#endregion
17095
+ //#region src/plugins/renderer/monitor/state.ts
17096
+ /** Compiles the compile ring keeps. */
17097
+ const COMPILE_SLOTS = 64;
17098
+ /** Long frames the long-frame ring keeps. */
17099
+ const LONG_FRAME_SLOTS = 64;
17100
+ /** Top scripts a long frame keeps. */
17101
+ const SCRIPT_SLOTS = 3;
17102
+ /** Rows the per-second system accumulators hold before they first grow. */
17103
+ const SYSTEM_ROWS = 16;
17104
+ /** Drawn frames a filtered view stands still, filtered in every one, before `filter-still` (U5). */
17105
+ const STILL_FRAMES = 60;
17106
+ /** Megapixels a still filtered view filters a frame before `filter-still`. */
17107
+ const STILL_MPX = .25;
17108
+ /** Render targets made and dropped a second that count as churn. */
17109
+ const CHURN_PER_SECOND = 4;
17110
+ /** Seconds in a row of churn before `target-churn`. */
17111
+ const CHURN_SECONDS = 3;
17112
+ /** A target made this soon after a pool trim is churn, ms. */
17113
+ const CHURN_AFTER_TRIM_MS = 1e3;
17114
+ /**
17115
+ * Creates an empty map. It lives in its own non-exported function because lint rule L5 refuses
17116
+ * a collection built inside an exported declaration.
17117
+ *
17118
+ * @returns An empty map.
17119
+ */
17120
+ function emptyMap$7() {
17121
+ return /* @__PURE__ */ new Map();
17122
+ }
17123
+ /**
17124
+ * Creates an empty weak set. Same reason as `emptyMap`.
17125
+ *
17126
+ * @returns An empty weak set.
17127
+ */
17128
+ function emptyWeakSet$1() {
17129
+ return /* @__PURE__ */ new WeakSet();
17130
+ }
17131
+ /**
17132
+ * Creates an empty set. Same reason as `emptyMap`.
17133
+ *
17134
+ * @returns An empty set.
17135
+ */
17136
+ function emptySet$6() {
17137
+ return /* @__PURE__ */ new Set();
17138
+ }
17139
+ /**
17140
+ * Creates the compile ring: 64 pre-made records.
17141
+ *
17142
+ * @returns The ring, empty.
17143
+ */
17144
+ function createCompileRing() {
17145
+ return {
17146
+ records: Array.from({ length: COMPILE_SLOTS }, () => ({
17147
+ frame: 0,
17148
+ label: null,
17149
+ ms: 0,
17150
+ module: false,
17151
+ pass: -1,
17152
+ kind: 0,
17153
+ owner: 0
17154
+ })),
17155
+ next: 0,
17156
+ size: 0,
17157
+ shaderModules: 0,
17158
+ written: 0,
17159
+ sent: 0
17160
+ };
17161
+ }
17162
+ /**
17163
+ * Creates the owner table of the filter applies: `OWNER_SLOTS` empty slots.
17164
+ *
17165
+ * @returns The table.
17166
+ */
17167
+ function createOwnerTable() {
17168
+ return {
17169
+ ids: /* @__PURE__ */ new Float64Array(32),
17170
+ applies: /* @__PURE__ */ new Float64Array(32),
17171
+ pixels: /* @__PURE__ */ new Float64Array(32),
17172
+ kinds: /* @__PURE__ */ new Uint8Array(32)
17173
+ };
17174
+ }
17175
+ /**
17176
+ * Creates the count table: room for `COUNT_SLOTS` counts, none registered.
17177
+ *
17178
+ * @returns The table.
17179
+ */
17180
+ function createCountTable() {
17181
+ return {
17182
+ names: [],
17183
+ reads: [],
17184
+ failed: emptySet$6()
17185
+ };
17186
+ }
17187
+ /**
17188
+ * Creates the problem accumulators with the thresholds of `renderer.monitor`, a key left out at its default: a game
17189
+ * that sets `monitor` replaces the whole object (shallow merge).
17190
+ *
17191
+ * @param config - `renderer.monitor`.
17192
+ * @returns The accumulators, no problem open.
17193
+ */
17194
+ function createProblemState(config) {
17195
+ return {
17196
+ thresholds: {
17197
+ stillFrames: config.stillFrames ?? STILL_FRAMES,
17198
+ stillMpx: config.stillMpx ?? STILL_MPX,
17199
+ churnPerSecond: config.churnPerSecond ?? CHURN_PER_SECOND,
17200
+ churnSeconds: config.churnSeconds ?? CHURN_SECONDS,
17201
+ churnAfterTrimMs: config.churnAfterTrimMs ?? CHURN_AFTER_TRIM_MS
17202
+ },
17203
+ run: /* @__PURE__ */ new Float64Array(32),
17204
+ runSince: /* @__PURE__ */ new Float64Array(32),
17205
+ secFrames: /* @__PURE__ */ new Float64Array(32),
17206
+ secPixels: /* @__PURE__ */ new Float64Array(32),
17207
+ stillOpen: /* @__PURE__ */ new Uint8Array(32),
17208
+ secTargets: 0,
17209
+ idleCreate: false,
17210
+ trimCreate: false,
17211
+ createFrame: 0,
17212
+ trimAt: NaN,
17213
+ churnRun: 0,
17214
+ churnSince: 0,
17215
+ churnOpen: false,
17216
+ takes: [emptyMap$7(), emptyMap$7()],
17217
+ takesFrame: [0, 0],
17218
+ retakesOpen: emptySet$6()
17219
+ };
17220
+ }
17221
+ /**
17222
+ * Creates the long-frame ring: 64 pre-made records with three pre-made scripts each.
17223
+ *
17224
+ * @returns The ring, empty, observing nothing.
17225
+ */
17226
+ function createLongFrameRing() {
17227
+ return {
17228
+ records: Array.from({ length: LONG_FRAME_SLOTS }, () => ({
17229
+ startMs: 0,
17230
+ durationMs: 0,
17231
+ blockingMs: NaN,
17232
+ renderStartMs: NaN,
17233
+ styleLayoutMs: NaN,
17234
+ scripts: Array.from({ length: SCRIPT_SLOTS }, () => ({
17235
+ invoker: "",
17236
+ type: "",
17237
+ source: "",
17238
+ fn: "",
17239
+ ms: 0,
17240
+ forcedLayoutMs: 0
17241
+ })),
17242
+ scriptCount: 0
17243
+ })),
17244
+ next: 0,
17245
+ size: 0,
17246
+ type: void 0,
17247
+ disconnect: void 0
17248
+ };
17249
+ }
17250
+ /**
17251
+ * Creates the sinks the counting classes write into. Its identity never changes; in a production
17252
+ * build it stays at zero.
17253
+ *
17254
+ * @param draws - The draw counter of the monitor state; a fresh one when left out.
17255
+ * @returns The sinks.
17256
+ */
17257
+ function createSinks(draws) {
17258
+ return {
17259
+ draws: draws ?? {
17260
+ frame: 0,
17261
+ last: 0
17262
+ },
17263
+ counts: new Float64Array(Object.keys(COUNT).length),
17264
+ inFrame: false,
17265
+ filterDepth: 0,
17266
+ filterKind: 0,
17267
+ kindNames: ["other"],
17268
+ kindApplies: /* @__PURE__ */ new Float64Array(32),
17269
+ kindPixels: /* @__PURE__ */ new Float64Array(32),
17270
+ filterKinds: void 0,
17271
+ filterOwners: void 0,
17272
+ filterOwner: 0,
17273
+ owners: createOwnerTable(),
17274
+ compiles: createCompileRing(),
17275
+ passes: void 0,
17276
+ measuring: false,
17277
+ rebuildsWindow: 0
17278
+ };
17279
+ }
17280
+ /**
17281
+ * Creates the performance monitor of an agent build. Nothing large is allocated here: the ring
17282
+ * gets its capacity at the first attach, once per renderer state.
17283
+ *
17284
+ * @param config - `renderer.monitor`.
17285
+ * @param sinks - The sinks of the monitor state.
17286
+ * @returns The perf state.
17287
+ */
17288
+ function createPerfState(config, sinks) {
17289
+ const events = createEventRing();
17290
+ return {
17291
+ budget: config.budgetMs,
17292
+ frames: config.frames,
17293
+ ring: createFrameRing(0, events, sinks.kindNames),
17294
+ scratch: /* @__PURE__ */ new Float32Array(0),
17295
+ staged: /* @__PURE__ */ new Float64Array(2),
17296
+ ticks: createTickEstimate(),
17297
+ rate: void 0,
17298
+ drawn: false,
17299
+ flags: 0,
17300
+ pathIds: emptyMap$7(),
17301
+ pathId: 0,
17302
+ restPath: void 0,
17303
+ produced: 0,
17304
+ measuring: 0,
17305
+ lastHeap: NaN,
17306
+ secSum: new Float64Array(SYSTEM_ROWS),
17307
+ secMax: new Float32Array(SYSTEM_ROWS),
17308
+ gapHist: /* @__PURE__ */ new Uint16Array(81),
17309
+ listenersSum: 0,
17310
+ events,
17311
+ display: void 0,
17312
+ pending: /* @__PURE__ */ new Float64Array(64),
17313
+ pendingCount: 0,
17314
+ lastFlushAt: NaN,
17315
+ agentMs: 0,
17316
+ agentStart: NaN,
17317
+ frame: 0,
17318
+ listeners: [],
17319
+ waiting: [],
17320
+ failed: emptyWeakSet$1(),
17321
+ shell: "unknown",
17322
+ startup: void 0,
17323
+ firstDrawnAt: NaN,
17324
+ firstDrawnFrame: NaN,
17325
+ counts: createCountTable(),
17326
+ problems: createProblemState(config),
17327
+ timer: void 0,
17328
+ longFrames: createLongFrameRing(),
17329
+ detach: [],
17330
+ attached: false
17331
+ };
17332
+ }
17333
+ /**
17334
+ * Creates the initial monitor state: no frame seen, an empty window, no capture waiting, no draw
17335
+ * counted, zeroed sinks, and in an agent build the performance monitor.
17336
+ *
17337
+ * @param config - `renderer.monitor`.
17338
+ * @returns The monitor branch of the plugin state.
17339
+ */
17340
+ function createMonitorState(config) {
17341
+ const draws = {
17342
+ frame: 0,
17343
+ last: 0
17344
+ };
17345
+ const sinks = createSinks(draws);
17346
+ return {
17347
+ frameStart: void 0,
17348
+ lastStart: void 0,
17349
+ windowMs: 0,
17350
+ intervals: 0,
17351
+ workMs: 0,
17352
+ frames: 0,
17353
+ fps: 0,
17354
+ drawnIntervals: 0,
17355
+ drawnFps: 0,
17356
+ lastDrawn: false,
17357
+ frameMs: 0,
17358
+ redrawsOpen: 0,
17359
+ rebuildsOpen: 0,
17360
+ redraws: 0,
17361
+ rebuilds: 0,
17362
+ captures: [],
17363
+ draws,
17364
+ sinks,
17365
+ perf: typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? createPerfState(config, sinks) : void 0,
17366
+ passes: {
17367
+ last: 0,
17368
+ open: 0,
17369
+ max: 0
17370
+ },
17371
+ passDetach: []
17372
+ };
17373
+ }
17374
+ //#endregion
17375
+ //#region src/plugins/renderer/monitor/counts.ts
17376
+ /**
17377
+ * The error of a register when the table is full.
17378
+ *
17379
+ * @returns The error.
17380
+ */
17381
+ function fullTable() {
17382
+ return /* @__PURE__ */ new Error(`[game] Monitor count table is full (16).\n Remove a registered count or raise the table size.`);
17383
+ }
17384
+ /**
17385
+ * Registers a count, or gives a registered name its new reader.
17386
+ *
17387
+ * @param table - The count table.
17388
+ * @param name - The count's name.
17389
+ * @param read - Answers the count now.
17390
+ * @returns The remover; a second call, or a call after the name got another reader, does nothing.
17391
+ * @throws {Error} When the table holds `COUNT_SLOTS` other counts.
17392
+ */
17393
+ function registerCount(table, name, read) {
17394
+ const at = table.names.indexOf(name);
17395
+ if (at !== -1) table.reads[at] = read;
17396
+ else if (table.names.length >= 16) throw fullTable();
17397
+ else {
17398
+ table.names.push(name);
17399
+ table.reads.push(read);
17400
+ }
17401
+ return () => {
17402
+ const index = table.names.indexOf(name);
17403
+ if (index === -1 || table.reads[index] !== read) return;
17404
+ table.names.splice(index, 1);
17405
+ table.reads.splice(index, 1);
17406
+ };
17407
+ }
17408
+ /**
17409
+ * Forgets every registered count: the renderer stopped.
17410
+ *
17411
+ * @param table - The count table.
17412
+ */
17413
+ function clearCounts(table) {
17414
+ table.names.length = 0;
17415
+ table.reads.length = 0;
17416
+ table.failed.clear();
17417
+ }
17418
+ /**
17419
+ * The live object counts now: the five the renderer reads itself, then each registered count by its name. A reader
17420
+ * that throws is left out and warned about once.
17421
+ *
17422
+ * @param pc - The perf context.
17423
+ * @returns A fresh object.
17424
+ */
17425
+ function readCounts(pc) {
17426
+ const { world, clock, time } = pc.ctx.deps;
17427
+ const table = pc.perf.counts;
17428
+ const counts = {
17429
+ entities: world.ecs.count(),
17430
+ views: pc.ctx.state.sync.views.size,
17431
+ timers: clock.pending(),
17432
+ frameListeners: time.frameListeners(),
17433
+ clockListeners: clock.listeners()
17434
+ };
17435
+ for (let index = 0; index < table.names.length; index += 1) {
17436
+ const name = table.names[index] ?? "";
17437
+ try {
17438
+ counts[name] = table.reads[index]?.() ?? 0;
17439
+ } catch (error) {
17440
+ if (!table.failed.has(name)) {
17441
+ table.failed.add(name);
17442
+ pc.ctx.log.warn("renderer: a monitor count failed", {
17443
+ name,
17444
+ error
17445
+ });
17446
+ }
17447
+ }
17448
+ }
17449
+ return counts;
17450
+ }
16337
17451
  //#endregion
16338
17452
  //#region src/plugins/renderer/monitor/log.ts
16339
17453
  /** What follows the Android model in a user agent: `…; SM-A266B Build/…`. */
@@ -16345,7 +17459,7 @@ const UA_WEBVIEW = /; wv\)/;
16345
17459
  /** Bytes of one KiB. */
16346
17460
  const BYTES_PER_KB = 1024;
16347
17461
  /**
16348
- * Spec 10's build id of the page: `"debug-20261014T153012Z"`, `"debug-full-…"` or `"dev"`; never a version string.
17462
+ * Spec 10's build id of the page: `"debug-20261014T153012Z"` or `"dev"`; never a version string.
16349
17463
  *
16350
17464
  * @returns The id, or `null` on a page without one.
16351
17465
  */
@@ -16353,6 +17467,15 @@ function buildId() {
16353
17467
  return typeof __MOKU_GAME_BUILD__ === "string" ? __MOKU_GAME_BUILD__ : null;
16354
17468
  }
16355
17469
  /**
17470
+ * The bundle identifier the build wrote into the page (`__MOKU_GAME_APP__`), the `.debug` suffix of a native debug
17471
+ * build included.
17472
+ *
17473
+ * @returns The id, or `null` on a page without one.
17474
+ */
17475
+ function appId() {
17476
+ return typeof __MOKU_GAME_APP__ === "string" ? __MOKU_GAME_APP__ : null;
17477
+ }
17478
+ /**
16356
17479
  * Freezes a line and every object and array in it, so a listener may keep it after it returns.
16357
17480
  *
16358
17481
  * @param value - The line, or a part of it.
@@ -16486,6 +17609,7 @@ function measuredOf(pc) {
16486
17609
  if (heapMb() !== null) list.push("heap");
16487
17610
  if (pc.ctx.deps.world.systemTimes() !== void 0) list.push("systems");
16488
17611
  list.push("kept");
17612
+ if ((bits & MEASURE.targets) !== 0) list.push("targets");
16489
17613
  return list;
16490
17614
  }
16491
17615
  /**
@@ -16500,14 +17624,14 @@ function capsOf(ring) {
16500
17624
  let idleFps = 0;
16501
17625
  for (let age = 0; age < ring.size && (maxFps === 0 || idleFps === 0); age += 1) {
16502
17626
  const slot = (ring.newest - age + ring.capacity) % ring.capacity;
16503
- const fps = Math.round(1e3 / cell(c.capMs, slot));
16504
- if (cell(c.idle, slot) === 1) idleFps ||= fps;
17627
+ const fps = Math.round(1e3 / cellF32(c.capMs, slot));
17628
+ if (cellU8(c.idle, slot) === 1) idleFps ||= fps;
16505
17629
  else maxFps ||= fps;
16506
17630
  }
16507
17631
  return {
16508
17632
  maxFps,
16509
17633
  idleFps,
16510
- budgetMs: ring.size === 0 ? 0 : cell(c.budgetMs, ring.newest)
17634
+ budgetMs: ring.size === 0 ? 0 : cellF32(c.budgetMs, ring.newest)
16511
17635
  };
16512
17636
  }
16513
17637
  /**
@@ -16584,7 +17708,9 @@ function headerLine(pc, thermal) {
16584
17708
  budget,
16585
17709
  frames: pc.perf.ring.capacity
16586
17710
  },
16587
- measured: measuredOf(pc)
17711
+ measured: measuredOf(pc),
17712
+ app: { id: appId() },
17713
+ debug: { overdraw: pc.ctx.state.sync.debug.overdraw }
16588
17714
  };
16589
17715
  }
16590
17716
  /**
@@ -16611,7 +17737,7 @@ function meanMax(sum, count, max) {
16611
17737
  */
16612
17738
  function rank(sorted, count, quantile) {
16613
17739
  if (count === 0) return 0;
16614
- return ms(cell(sorted, Math.min(count - 1, Math.max(0, Math.ceil(quantile * count) - 1))));
17740
+ return ms(cellF32(sorted, Math.min(count - 1, Math.max(0, Math.ceil(quantile * count) - 1))));
16615
17741
  }
16616
17742
  /**
16617
17743
  * The slots of a window of the ring, newest first: the frames whose start lies in `[start, end)`.
@@ -16624,9 +17750,9 @@ function rank(sorted, count, quantile) {
16624
17750
  function windowOf$1(ring, start, end) {
16625
17751
  const c = ring.columns;
16626
17752
  let first = 0;
16627
- while (first < ring.size && cell(c.t, slotAt(ring, first)) >= end) first += 1;
17753
+ while (first < ring.size && cellF64(c.t, slotAt(ring, first)) >= end) first += 1;
16628
17754
  let count = 0;
16629
- while (first + count < ring.size && cell(c.t, slotAt(ring, first + count)) >= start) count += 1;
17755
+ while (first + count < ring.size && cellF64(c.t, slotAt(ring, first + count)) >= start) count += 1;
16630
17756
  return {
16631
17757
  first,
16632
17758
  count
@@ -16643,20 +17769,21 @@ function slotAt(ring, age) {
16643
17769
  return (ring.newest - age + ring.capacity) % ring.capacity;
16644
17770
  }
16645
17771
  /**
16646
- * Sorts the values a window gives into the scratch array and answers its spread.
17772
+ * Sorts the values of one column over a window into the scratch array and answers its spread. The column, not a
17773
+ * reader closure: a closure's double comes back boxed from its call, once for every frame of the window.
16647
17774
  *
16648
17775
  * @param pc - The perf context.
16649
17776
  * @param window - The window.
16650
17777
  * @param window.first - Its first age.
16651
17778
  * @param window.count - Its frames.
16652
- * @param read - Reads a frame's value; NaN leaves the frame out.
17779
+ * @param column - A `Float32Array` column of the ring; NaN leaves the frame out.
16653
17780
  * @returns The values kept, and the scratch view sorted over them.
16654
17781
  */
16655
- function sortedOf(pc, window, read) {
17782
+ function sortedOf(pc, window, column) {
16656
17783
  const { ring, scratch } = pc.perf;
16657
17784
  let count = 0;
16658
17785
  for (let age = window.first; age < window.first + window.count; age += 1) {
16659
- const value = read(slotAt(ring, age));
17786
+ const value = cellF32(column, slotAt(ring, age));
16660
17787
  if (!Number.isNaN(value)) {
16661
17788
  scratch[count] = value;
16662
17789
  count += 1;
@@ -16675,8 +17802,8 @@ function sortedOf(pc, window, read) {
16675
17802
  * @param value - The phase's ms.
16676
17803
  */
16677
17804
  function addPhase(totals, index, value) {
16678
- totals.phaseSum[index] = cell(totals.phaseSum, index) + value;
16679
- totals.phaseMax[index] = Math.max(cell(totals.phaseMax, index), value);
17805
+ totals.phaseSum[index] = cellF64(totals.phaseSum, index) + value;
17806
+ totals.phaseMax[index] = Math.max(cellF64(totals.phaseMax, index), value);
16680
17807
  }
16681
17808
  /**
16682
17809
  * Adds one frame of a window: its phases, its counts, its gap. A missed gap is a frame with the `gap` cause: the
@@ -16687,22 +17814,24 @@ function addPhase(totals, index, value) {
16687
17814
  * @param slot - The frame's slot.
16688
17815
  */
16689
17816
  function addFrame(totals, c, slot) {
16690
- const agent = cell(c.agentMs, slot);
16691
- const missed = (cell(c.causes, slot) & CAUSE.gap) !== 0;
16692
- addPhase(totals, 0, cell(c.startMs, slot));
16693
- for (let index = 0; index < 6; index += 1) addPhase(totals, index + 1, cell(c.phaseMs, slot * 6 + index));
17817
+ const agent = cellF32(c.agentMs, slot);
17818
+ const missed = (cellU8(c.causes, slot) & CAUSE.gap) !== 0;
17819
+ addPhase(totals, 0, cellF32(c.startMs, slot));
17820
+ for (let index = 0; index < 6; index += 1) addPhase(totals, index + 1, cellF32(c.phaseMs, slot * 6 + index));
16694
17821
  addPhase(totals, 7, agent);
16695
- totals.idle += cell(c.idle, slot);
16696
- totals.overBudget += cell(c.workMs, slot) - agent > cell(c.budgetMs, slot) ? 1 : 0;
16697
- totals.spikes += cell(c.causes, slot) === 0 ? 0 : 1;
17822
+ totals.idle += cellU8(c.idle, slot);
17823
+ totals.overBudget += cellF32(c.workMs, slot) - agent > cellF32(c.budgetMs, slot) ? 1 : 0;
17824
+ totals.spikes += cellU8(c.causes, slot) === 0 ? 0 : 1;
16698
17825
  totals.missed += missed ? 1 : 0;
16699
- totals.missedDrawn += missed && cell(c.drawn, slot) === 1 ? 1 : 0;
16700
- totals.skipped += cell(c.skipped, slot);
16701
- totals.uploads += cell(c.uploads, slot);
16702
- totals.uploadMb += cell(c.uploadMb, slot);
16703
- totals.compiles += cell(c.compiles, slot);
16704
- totals.rebuilds += cell(c.rebuilds, slot);
16705
- totals.retakes += cell(c.retakes, slot);
17826
+ totals.missedDrawn += missed && cellU8(c.drawn, slot) === 1 ? 1 : 0;
17827
+ totals.skipped += cellU8(c.skipped, slot);
17828
+ totals.uploads += cellF32(c.uploads, slot);
17829
+ totals.uploadMb += cellF32(c.uploadMb, slot);
17830
+ totals.compiles += cellF32(c.compiles, slot);
17831
+ totals.rebuilds += cellF32(c.rebuilds, slot);
17832
+ totals.retakes += cellF32(c.retakes, slot);
17833
+ totals.targetsCreated += cellF32(c.targetsCreated, slot);
17834
+ totals.targetsDestroyed += cellF32(c.targetsDestroyed, slot);
16706
17835
  }
16707
17836
  /**
16708
17837
  * Adds what only a drawn frame has: its passes, draws and filters.
@@ -16713,15 +17842,15 @@ function addFrame(totals, c, slot) {
16713
17842
  */
16714
17843
  function addDrawn(totals, c, slot) {
16715
17844
  totals.drawn += 1;
16716
- totals.passes += cell(c.passes, slot);
16717
- totals.passesMax = Math.max(totals.passesMax, cell(c.passes, slot));
16718
- totals.stencil += cell(c.stencilPasses, slot);
16719
- totals.draws += cell(c.draws, slot);
16720
- totals.drawsMax = Math.max(totals.drawsMax, cell(c.draws, slot));
16721
- totals.applies += cell(c.filterApplies, slot);
16722
- totals.appliesMax = Math.max(totals.appliesMax, cell(c.filterApplies, slot));
16723
- totals.mpx += cell(c.filterMpx, slot);
16724
- totals.keptMax = Math.max(totals.keptMax, cell(c.kept, slot));
17845
+ totals.passes += cellF32(c.passes, slot);
17846
+ totals.passesMax = Math.max(totals.passesMax, cellF32(c.passes, slot));
17847
+ totals.stencil += cellF32(c.stencilPasses, slot);
17848
+ totals.draws += cellF32(c.draws, slot);
17849
+ totals.drawsMax = Math.max(totals.drawsMax, cellF32(c.draws, slot));
17850
+ totals.applies += cellF32(c.filterApplies, slot);
17851
+ totals.appliesMax = Math.max(totals.appliesMax, cellF32(c.filterApplies, slot));
17852
+ totals.mpx += cellF32(c.filterMpx, slot);
17853
+ totals.keptMax = Math.max(totals.keptMax, cellF32(c.kept, slot));
16725
17854
  }
16726
17855
  /**
16727
17856
  * Adds up the frames of a window.
@@ -16758,12 +17887,14 @@ function totalsOf(pc, window) {
16758
17887
  compiles: 0,
16759
17888
  rebuilds: 0,
16760
17889
  keptMax: 0,
16761
- retakes: 0
17890
+ retakes: 0,
17891
+ targetsCreated: 0,
17892
+ targetsDestroyed: 0
16762
17893
  };
16763
17894
  for (let age = window.first; age < window.first + window.count; age += 1) {
16764
17895
  const slot = slotAt(ring, age);
16765
17896
  addFrame(totals, c, slot);
16766
- if (cell(c.drawn, slot) === 1) addDrawn(totals, c, slot);
17897
+ if (cellU8(c.drawn, slot) === 1) addDrawn(totals, c, slot);
16767
17898
  }
16768
17899
  return totals;
16769
17900
  }
@@ -16780,8 +17911,8 @@ function systemsOfSecond(pc, frames) {
16780
17911
  const { secSum, secMax } = pc.perf;
16781
17912
  return timing.names.slice(0, secSum.length).map((name, index) => ({
16782
17913
  name,
16783
- mean: frames === 0 ? 0 : cell(secSum, index) / frames,
16784
- max: cell(secMax, index)
17914
+ mean: frames === 0 ? 0 : cellF64(secSum, index) / frames,
17915
+ max: cellF32(secMax, index)
16785
17916
  })).filter((system) => system.mean > 0).toSorted((first, second) => second.mean - first.mean).slice(0, 5).map((system) => ({
16786
17917
  name: system.name,
16787
17918
  mean: ms(system.mean),
@@ -16837,7 +17968,7 @@ function perDrawn(sum, drawn) {
16837
17968
  */
16838
17969
  function gpuOfSecond(pc, window) {
16839
17970
  const { columns } = pc.perf.ring;
16840
- const gpu = sortedOf(pc, window, (slot) => cell(columns.gpuMs, slot));
17971
+ const gpu = sortedOf(pc, window, columns.gpuMs);
16841
17972
  return {
16842
17973
  p50: rank(gpu.sorted, gpu.count, .5),
16843
17974
  p90: rank(gpu.sorted, gpu.count, .9),
@@ -16877,24 +18008,24 @@ function sampleLine(pc, start, end) {
16877
18008
  const totals = totalsOf(pc, window);
16878
18009
  const bits = perf.measuring;
16879
18010
  const counted = (bits & MEASURE.counted) !== 0;
16880
- const work = sortedOf(pc, window, (slot) => cell(c.workMs, slot));
18011
+ const work = sortedOf(pc, window, c.workMs);
16881
18012
  const workP50 = rank(work.sorted, work.count, .5);
16882
18013
  const workP90 = rank(work.sorted, work.count, .9);
16883
18014
  const workMax = rank(work.sorted, work.count, 1);
16884
- const gaps = sortedOf(pc, window, (slot) => cell(c.gapMs, slot));
18015
+ const gaps = sortedOf(pc, window, c.gapMs);
16885
18016
  const gapP50 = rank(gaps.sorted, gaps.count, .5);
16886
18017
  const gapP90 = rank(gaps.sorted, gaps.count, .9);
16887
18018
  const gapP99 = rank(gaps.sorted, gaps.count, .99);
16888
18019
  const usage = deps.host.textures();
16889
18020
  const counts = deps.sync.counts();
16890
18021
  const drawn = totals.drawn;
16891
- const phase = (index) => meanMax(cell(totals.phaseSum, index), frames, cell(totals.phaseMax, index));
18022
+ const phase = (index) => meanMax(cellF64(totals.phaseSum, index), frames, cellF64(totals.phaseMax, index));
16892
18023
  return {
16893
18024
  v: 1,
16894
18025
  kind: "sample",
16895
18026
  t: epochOf(end),
16896
18027
  build: buildId(),
16897
- frame: frames === 0 ? 0 : cell(c.frame, slotAt(ring, window.first)),
18028
+ frame: frames === 0 ? 0 : cellU32(c.frame, slotAt(ring, window.first)),
16898
18029
  windowMs: ms(end - start),
16899
18030
  path: ring.paths[perf.pathId] ?? "…",
16900
18031
  ticks: frames + totals.skipped,
@@ -16962,7 +18093,12 @@ function sampleLine(pc, start, end) {
16962
18093
  pooled: counts.pooled,
16963
18094
  resolution: deps.host.resolution(),
16964
18095
  monitorMs: frames === 0 ? 0 : roundTo(perf.listenersSum / frames, 3),
16965
- spikes: totals.spikes
18096
+ spikes: totals.spikes,
18097
+ counts: readCounts(pc),
18098
+ targets: (bits & MEASURE.targets) === 0 ? null : {
18099
+ created: totals.targetsCreated,
18100
+ destroyed: totals.targetsDestroyed
18101
+ }
16966
18102
  };
16967
18103
  }
16968
18104
  /**
@@ -16989,7 +18125,7 @@ function spikeLine(pc, slot) {
16989
18125
  ms: ms(compile.ms)
16990
18126
  })),
16991
18127
  filters: record.filters?.kinds ?? [],
16992
- passes: passesOf$1(ring, record.frame),
18128
+ passes: passesOf$1(ring, record.frame, cellF32(ring.columns.draws, slot), (entity) => viewLabel(pc, entity)),
16993
18129
  longFrames: longFramesIn(longFrames, start, record.t + record.workMs).map((entry) => entryOf(entry))
16994
18130
  };
16995
18131
  }
@@ -17066,10 +18202,10 @@ function framesOver(ring, record) {
17066
18202
  let last = NaN;
17067
18203
  for (let age = ring.size - 1; age >= 0; age -= 1) {
17068
18204
  const slot = slotAt(ring, age);
17069
- const t = cell(c.t, slot);
17070
- if (t < end && t + cell(c.workMs, slot) > record.startMs) {
17071
- if (Number.isNaN(first)) first = cell(c.frame, slot);
17072
- last = cell(c.frame, slot);
18205
+ const t = cellF64(c.t, slot);
18206
+ if (t < end && t + cellF32(c.workMs, slot) > record.startMs) {
18207
+ if (Number.isNaN(first)) first = cellU32(c.frame, slot);
18208
+ last = cellU32(c.frame, slot);
17073
18209
  }
17074
18210
  }
17075
18211
  return Number.isNaN(first) ? null : [first, last];
@@ -17200,248 +18336,6 @@ function buildStartup(pc) {
17200
18336
  }
17201
18337
  };
17202
18338
  }
17203
- /**
17204
- * Creates the estimate: no interval yet, no period.
17205
- *
17206
- * @returns The estimate.
17207
- */
17208
- function createTickEstimate() {
17209
- return {
17210
- samples: /* @__PURE__ */ new Float32Array(9),
17211
- sorted: /* @__PURE__ */ new Float32Array(9),
17212
- size: 0,
17213
- next: 0,
17214
- periodMs: Float64Array.of(NaN)
17215
- };
17216
- }
17217
- /**
17218
- * Forgets every interval: the next frames start a new estimate.
17219
- *
17220
- * @param ticks - The estimate.
17221
- */
17222
- function resetTicks(ticks) {
17223
- ticks.size = 0;
17224
- ticks.next = 0;
17225
- ticks.periodMs[0] = NaN;
17226
- }
17227
- /**
17228
- * Adds the tick interval of a frame and snaps the median of the last nine to a display rate: `periodMs[0]` becomes
17229
- * that rate's period, or NaN while the median matches none. A stepped frame, a frame without a gap and a gap over
17230
- * 50 ms (a hitch, a page away) add nothing. The median is taken by an insertion sort into `sorted`: nine numbers.
17231
- *
17232
- * @param ticks - The estimate.
17233
- * @param measure - What `time` measured of the frame; read, never kept.
17234
- */
17235
- function noteTicks(ticks, measure) {
17236
- if (measure.stepped || Number.isNaN(measure.gapMs) || measure.gapMs > 50) return;
17237
- const { samples, sorted } = ticks;
17238
- samples[ticks.next] = measure.gapMs / (measure.skipped + 1);
17239
- ticks.next = (ticks.next + 1) % 9;
17240
- ticks.size = Math.min(ticks.size + 1, 9);
17241
- for (let index = 0; index < ticks.size; index += 1) {
17242
- const value = cell(samples, index);
17243
- let place = index;
17244
- while (place > 0 && cell(sorted, place - 1) > value) {
17245
- sorted[place] = cell(sorted, place - 1);
17246
- place -= 1;
17247
- }
17248
- sorted[place] = value;
17249
- }
17250
- const rate = displayIndexOf(cell(sorted, ticks.size >> 1));
17251
- const hz = rate === -1 ? void 0 : DISPLAY_RATES[rate];
17252
- ticks.periodMs[0] = hz === void 0 ? NaN : 1e3 / hz;
17253
- }
17254
- //#endregion
17255
- //#region src/plugins/renderer/monitor/state.ts
17256
- /** Compiles the compile ring keeps. */
17257
- const COMPILE_SLOTS = 64;
17258
- /** Long frames the long-frame ring keeps. */
17259
- const LONG_FRAME_SLOTS = 64;
17260
- /** Top scripts a long frame keeps. */
17261
- const SCRIPT_SLOTS = 3;
17262
- /** Rows the per-second system accumulators hold before they first grow. */
17263
- const SYSTEM_ROWS = 16;
17264
- /**
17265
- * Creates an empty map. It lives in its own non-exported function because lint rule L5 refuses
17266
- * a collection built inside an exported declaration.
17267
- *
17268
- * @returns An empty map.
17269
- */
17270
- function emptyMap$7() {
17271
- return /* @__PURE__ */ new Map();
17272
- }
17273
- /**
17274
- * Creates an empty weak set. Same reason as `emptyMap`.
17275
- *
17276
- * @returns An empty weak set.
17277
- */
17278
- function emptyWeakSet$1() {
17279
- return /* @__PURE__ */ new WeakSet();
17280
- }
17281
- /**
17282
- * Creates the compile ring: 64 pre-made records.
17283
- *
17284
- * @returns The ring, empty.
17285
- */
17286
- function createCompileRing() {
17287
- return {
17288
- records: Array.from({ length: COMPILE_SLOTS }, () => ({
17289
- frame: 0,
17290
- label: null,
17291
- ms: 0
17292
- })),
17293
- next: 0,
17294
- size: 0,
17295
- shaderModules: 0
17296
- };
17297
- }
17298
- /**
17299
- * Creates the long-frame ring: 64 pre-made records with three pre-made scripts each.
17300
- *
17301
- * @returns The ring, empty, observing nothing.
17302
- */
17303
- function createLongFrameRing() {
17304
- return {
17305
- records: Array.from({ length: LONG_FRAME_SLOTS }, () => ({
17306
- startMs: 0,
17307
- durationMs: 0,
17308
- blockingMs: NaN,
17309
- renderStartMs: NaN,
17310
- styleLayoutMs: NaN,
17311
- scripts: Array.from({ length: SCRIPT_SLOTS }, () => ({
17312
- invoker: "",
17313
- type: "",
17314
- source: "",
17315
- fn: "",
17316
- ms: 0,
17317
- forcedLayoutMs: 0
17318
- })),
17319
- scriptCount: 0
17320
- })),
17321
- next: 0,
17322
- size: 0,
17323
- type: void 0,
17324
- disconnect: void 0
17325
- };
17326
- }
17327
- /**
17328
- * Creates the sinks the counting classes write into. Its identity never changes; in a production
17329
- * build it stays at zero.
17330
- *
17331
- * @param draws - The draw counter of the monitor state; a fresh one when left out.
17332
- * @returns The sinks.
17333
- */
17334
- function createSinks(draws) {
17335
- return {
17336
- draws: draws ?? {
17337
- frame: 0,
17338
- last: 0
17339
- },
17340
- counts: new Float64Array(Object.keys(COUNT).length),
17341
- inFrame: false,
17342
- filterDepth: 0,
17343
- filterKind: 0,
17344
- kindNames: ["other"],
17345
- kindApplies: /* @__PURE__ */ new Float64Array(32),
17346
- kindPixels: /* @__PURE__ */ new Float64Array(32),
17347
- filterKinds: void 0,
17348
- compiles: createCompileRing(),
17349
- passes: void 0,
17350
- measuring: false,
17351
- rebuildsWindow: 0
17352
- };
17353
- }
17354
- /**
17355
- * Creates the performance monitor of an agent build. Nothing large is allocated here: the ring
17356
- * gets its capacity at the first attach, once per renderer state.
17357
- *
17358
- * @param config - `renderer.monitor`.
17359
- * @param sinks - The sinks of the monitor state.
17360
- * @returns The perf state.
17361
- */
17362
- function createPerfState(config, sinks) {
17363
- const events = createEventRing();
17364
- return {
17365
- budget: config.budgetMs,
17366
- frames: config.frames,
17367
- ring: createFrameRing(0, events, sinks.kindNames),
17368
- scratch: /* @__PURE__ */ new Float32Array(0),
17369
- staged: /* @__PURE__ */ new Float64Array(2),
17370
- ticks: createTickEstimate(),
17371
- rate: void 0,
17372
- drawn: false,
17373
- flags: 0,
17374
- pathIds: emptyMap$7(),
17375
- pathId: 0,
17376
- restPath: void 0,
17377
- produced: 0,
17378
- measuring: 0,
17379
- lastHeap: NaN,
17380
- secSum: new Float64Array(SYSTEM_ROWS),
17381
- secMax: new Float32Array(SYSTEM_ROWS),
17382
- gapHist: /* @__PURE__ */ new Uint16Array(81),
17383
- listenersSum: 0,
17384
- events,
17385
- display: void 0,
17386
- pending: /* @__PURE__ */ new Float64Array(64),
17387
- pendingCount: 0,
17388
- lastFlushAt: NaN,
17389
- agentMs: 0,
17390
- agentStart: NaN,
17391
- frame: 0,
17392
- listeners: [],
17393
- waiting: [],
17394
- failed: emptyWeakSet$1(),
17395
- shell: "unknown",
17396
- startup: void 0,
17397
- firstDrawnAt: NaN,
17398
- timer: void 0,
17399
- longFrames: createLongFrameRing(),
17400
- detach: [],
17401
- attached: false
17402
- };
17403
- }
17404
- /**
17405
- * Creates the initial monitor state: no frame seen, an empty window, no capture waiting, no draw
17406
- * counted, zeroed sinks, and in an agent build the performance monitor.
17407
- *
17408
- * @param config - `renderer.monitor`.
17409
- * @returns The monitor branch of the plugin state.
17410
- */
17411
- function createMonitorState(config) {
17412
- const draws = {
17413
- frame: 0,
17414
- last: 0
17415
- };
17416
- const sinks = createSinks(draws);
17417
- return {
17418
- frameStart: void 0,
17419
- lastStart: void 0,
17420
- windowMs: 0,
17421
- intervals: 0,
17422
- workMs: 0,
17423
- frames: 0,
17424
- fps: 0,
17425
- drawnIntervals: 0,
17426
- drawnFps: 0,
17427
- lastDrawn: false,
17428
- frameMs: 0,
17429
- redrawsOpen: 0,
17430
- rebuildsOpen: 0,
17431
- redraws: 0,
17432
- rebuilds: 0,
17433
- captures: [],
17434
- draws,
17435
- sinks,
17436
- perf: typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? createPerfState(config, sinks) : void 0,
17437
- passes: {
17438
- last: 0,
17439
- open: 0,
17440
- max: 0
17441
- },
17442
- passDetach: []
17443
- };
17444
- }
17445
18339
  /** A gap spike is annotated `compile` for a compile in one of this many frames before it: the stall comes later. */
17446
18340
  const COMPILE_LOOKBACK = 4;
17447
18341
  /** What a refused promise of the shell leads to: nothing, the log goes on. */
@@ -17555,7 +18449,7 @@ function addPending(perf, sequence) {
17555
18449
  */
17556
18450
  function compiledBefore(ring, slot) {
17557
18451
  const age = (ring.newest - slot + ring.capacity) % ring.capacity;
17558
- for (let back = 1; back <= COMPILE_LOOKBACK && age + back < ring.size; back += 1) if (cell(ring.columns.compiles, (slot - back + ring.capacity) % ring.capacity) > 0) return true;
18452
+ for (let back = 1; back <= COMPILE_LOOKBACK && age + back < ring.size; back += 1) if (cellF32(ring.columns.compiles, (slot - back + ring.capacity) % ring.capacity) > 0) return true;
17559
18453
  return false;
17560
18454
  }
17561
18455
  /**
@@ -17570,10 +18464,10 @@ function compiledBefore(ring, slot) {
17570
18464
  function annotate(pc, slot) {
17571
18465
  const { ring } = pc.perf;
17572
18466
  const c = ring.columns;
17573
- const late = (cell(c.causes, slot) & CAUSE.gap) !== 0;
18467
+ const late = (cellU8(c.causes, slot) & CAUSE.gap) !== 0;
17574
18468
  let bits = 0;
17575
- if (cell(c.compiles, slot) > 0 || late && compiledBefore(ring, slot)) bits |= CAUSE.compile;
17576
- if (cell(c.uploadMb, slot) >= 1) bits |= CAUSE.upload;
18469
+ if (cellF32(c.compiles, slot) > 0 || late && compiledBefore(ring, slot)) bits |= CAUSE.compile;
18470
+ if (cellF32(c.uploadMb, slot) >= 1) bits |= CAUSE.upload;
17577
18471
  if (annotateLongFrame(pc.perf, slot)) bits |= CAUSE.longFrame;
17578
18472
  return bits;
17579
18473
  }
@@ -17590,11 +18484,11 @@ function flushSpikes(pc, windowStart) {
17590
18484
  const c = ring.columns;
17591
18485
  let kept = 0;
17592
18486
  for (let index = 0; index < perf.pendingCount; index += 1) {
17593
- const sequence = cell(perf.pending, index);
18487
+ const sequence = cellF64(perf.pending, index);
17594
18488
  const slot = slotOfSequence(ring, sequence);
17595
18489
  if (slot === -1) continue;
17596
- if (cell(c.gpuPartial, slot) === 0 || cell(c.t, slot) < windowStart) {
17597
- c.causes[slot] = cell(c.causes, slot) | annotate(pc, slot);
18490
+ if (cellU8(c.gpuPartial, slot) === 0 || cellF64(c.t, slot) < windowStart) {
18491
+ c.causes[slot] = cellU8(c.causes, slot) | annotate(pc, slot);
17598
18492
  deliver(pc, spikeLine(pc, slot));
17599
18493
  } else {
17600
18494
  perf.pending[kept] = sequence;
@@ -17616,10 +18510,10 @@ function flushLoads(pc, start, end) {
17616
18510
  const window = windowOf$1(ring, start, end);
17617
18511
  for (let age = window.first + window.count - 1; age >= window.first; age -= 1) {
17618
18512
  const slot = slotAt(ring, age);
17619
- const frame = cell(c.frame, slot);
17620
- if (Number.isNaN(firstDrawnAt) || cell(c.t, slot) <= firstDrawnAt) continue;
17621
- const compiles = cell(c.compiles, slot);
17622
- const mb = cell(c.uploadMb, slot);
18513
+ const frame = cellU32(c.frame, slot);
18514
+ if (Number.isNaN(firstDrawnAt) || cellF64(c.t, slot) <= firstDrawnAt) continue;
18515
+ const compiles = cellF32(c.compiles, slot);
18516
+ const mb = cellF32(c.uploadMb, slot);
17623
18517
  if (compiles > 0) deliver(pc, eventLine("compile", {
17624
18518
  frame,
17625
18519
  count: compiles
@@ -17631,6 +18525,26 @@ function flushLoads(pc, start, end) {
17631
18525
  }
17632
18526
  }
17633
18527
  /**
18528
+ * Writes a `shader` event for every compile and shader module not sent yet that the ring still holds, boot compiles
18529
+ * included: a listener that came after the boot still gets them at its first flush.
18530
+ *
18531
+ * @param pc - The perf context.
18532
+ */
18533
+ function flushShaders(pc) {
18534
+ for (const detail of takeShaderEvents(pc, (entity) => viewLabel(pc, entity))) deliver(pc, eventLine("shader", detail, detail.frame));
18535
+ }
18536
+ /**
18537
+ * Judges the problem rules of the second and writes a `problem` event for each that opened or cleared; without a
18538
+ * listener the rules are judged all the same, so their accumulators stay per second.
18539
+ *
18540
+ * @param pc - The perf context.
18541
+ */
18542
+ function flushProblems(pc) {
18543
+ const details = judgeProblems(pc);
18544
+ if (pc.perf.listeners.length === 0) return;
18545
+ for (const detail of details) deliver(pc, eventLine("problem", detail, pc.perf.frame));
18546
+ }
18547
+ /**
17634
18548
  * Starts a new second: the histogram, the system sums and maxima and the monitor's own cost go back to 0, the system
17635
18549
  * accumulators grown when the world registered more systems than they hold.
17636
18550
  *
@@ -17647,7 +18561,7 @@ function nextSecond(pc) {
17647
18561
  perf.secSum.fill(0);
17648
18562
  perf.secMax.fill(0);
17649
18563
  perf.listenersSum = 0;
17650
- perf.lastFlushAt = cell(perf.staged, 1);
18564
+ perf.lastFlushAt = cellF64(perf.staged, 1);
17651
18565
  }
17652
18566
  /**
17653
18567
  * The flush, once a second of page clock and on a pause: the GPU batch closes, the startup line is built once after
@@ -17660,7 +18574,7 @@ function nextSecond(pc) {
17660
18574
  function flushPerf(pc) {
17661
18575
  const { perf } = pc;
17662
18576
  const start = perf.lastFlushAt;
17663
- const windowEnd = cell(perf.staged, 1);
18577
+ const windowEnd = cellF64(perf.staged, 1);
17664
18578
  perf.timer?.close();
17665
18579
  if (perf.startup === void 0 && !Number.isNaN(perf.firstDrawnAt)) {
17666
18580
  const startup = deepFreeze(buildStartup(pc));
@@ -17671,7 +18585,9 @@ function flushPerf(pc) {
17671
18585
  deliver(pc, sampleLine(pc, start, windowEnd));
17672
18586
  flushSpikes(pc, start);
17673
18587
  flushLoads(pc, start, windowEnd);
18588
+ flushShaders(pc);
17674
18589
  } else perf.pendingCount = 0;
18590
+ flushProblems(pc);
17675
18591
  nextSecond(pc);
17676
18592
  }
17677
18593
  /**
@@ -17727,6 +18643,8 @@ function openFrame(sinks) {
17727
18643
  sinks.counts.fill(0);
17728
18644
  sinks.kindApplies.fill(0);
17729
18645
  sinks.kindPixels.fill(0);
18646
+ sinks.owners.applies.fill(0);
18647
+ sinks.owners.pixels.fill(0);
17730
18648
  sinks.inFrame = true;
17731
18649
  }
17732
18650
  /**
@@ -17742,21 +18660,24 @@ function writeCounters(pc, slot) {
17742
18660
  const { counts } = sinks;
17743
18661
  const bits = perf.measuring;
17744
18662
  const counted = (bits & MEASURE.counted) !== 0;
17745
- c.passes[slot] = (bits & MEASURE.passes) === 0 ? NaN : cell(counts, COUNT.passes);
17746
- c.stencilPasses[slot] = (bits & MEASURE.stencilPasses) === 0 ? NaN : cell(counts, COUNT.stencilPasses);
18663
+ c.passes[slot] = (bits & MEASURE.passes) === 0 ? NaN : cellF64(counts, COUNT.passes);
18664
+ c.stencilPasses[slot] = (bits & MEASURE.stencilPasses) === 0 ? NaN : cellF64(counts, COUNT.stencilPasses);
17747
18665
  c.draws[slot] = (bits & MEASURE.draws) === 0 ? NaN : sinks.draws.last;
17748
- c.filterApplies[slot] = counted ? cell(counts, COUNT.filterApplies) : NaN;
17749
- c.filterMpx[slot] = counted ? cell(counts, COUNT.filterPixels) / PIXELS_PER_MPX : NaN;
17750
- c.uploads[slot] = counted ? cell(counts, COUNT.uploads) : NaN;
17751
- c.uploadMb[slot] = counted ? cell(counts, COUNT.uploadBytes) / BYTES_PER_MB$1 : NaN;
17752
- c.compiles[slot] = (bits & MEASURE.compiles) === 0 ? NaN : cell(counts, COUNT.compiles);
18666
+ c.filterApplies[slot] = counted ? cellF64(counts, COUNT.filterApplies) : NaN;
18667
+ c.filterMpx[slot] = counted ? cellF64(counts, COUNT.filterPixels) / PIXELS_PER_MPX : NaN;
18668
+ c.uploads[slot] = counted ? cellF64(counts, COUNT.uploads) : NaN;
18669
+ c.uploadMb[slot] = counted ? cellF64(counts, COUNT.uploadBytes) / BYTES_PER_MB$1 : NaN;
18670
+ c.compiles[slot] = (bits & MEASURE.compiles) === 0 ? NaN : cellF64(counts, COUNT.compiles);
17753
18671
  const decided = pc.deps.demand.frame();
17754
18672
  c.marks[slot] = decided.marks;
17755
18673
  c.reason[slot] = reasonIndex(decided.reason);
17756
- c.rebuilds[slot] = perf.produced === 0 ? NaN : cell(counts, COUNT.rebuilds);
18674
+ c.rebuilds[slot] = perf.produced === 0 ? NaN : cellF64(counts, COUNT.rebuilds);
17757
18675
  const kept = pc.ctx.state.kept;
17758
18676
  c.kept[slot] = perf.drawn ? kept.drawnPictures : 0;
17759
18677
  c.retakes[slot] = kept.frameTakes;
18678
+ const targets = (bits & MEASURE.targets) !== 0;
18679
+ c.targetsCreated[slot] = targets ? cellF64(counts, COUNT.targetsCreated) : NaN;
18680
+ c.targetsDestroyed[slot] = targets ? cellF64(counts, COUNT.targetsDestroyed) : NaN;
17760
18681
  }
17761
18682
  /**
17762
18683
  * The index of a draw reason in ring order, `NO_REASON` for a name the order does not know.
@@ -17785,11 +18706,11 @@ function reasonIndex(reason) {
17785
18706
  */
17786
18707
  function insertTop(indices, values, base, stride, index, value) {
17787
18708
  let place = stride;
17788
- while (place > 0 && value > cell(values, base + place - 1)) place -= 1;
18709
+ while (place > 0 && value > cellF32(values, base + place - 1)) place -= 1;
17789
18710
  if (place === stride) return;
17790
18711
  for (let move = stride - 1; move > place; move -= 1) {
17791
- indices[base + move] = cell(indices, base + move - 1);
17792
- values[base + move] = cell(values, base + move - 1);
18712
+ indices[base + move] = indices[base + move - 1] ?? 0;
18713
+ values[base + move] = cellF32(values, base + move - 1);
17793
18714
  }
17794
18715
  indices[base + place] = index;
17795
18716
  values[base + place] = value;
@@ -17815,7 +18736,7 @@ function writeSystems(pc, slot, timing) {
17815
18736
  ring.systemNames = timing.names;
17816
18737
  const count = Math.min(timing.names.length, timing.ms.length);
17817
18738
  for (let index = 0; index < count; index += 1) {
17818
- const ms = cell(timing.ms, index);
18739
+ const ms = cellF64(timing.ms, index);
17819
18740
  if (ms > 0) insertTop(c.sysIdx, c.sysMs, base, 5, index, ms);
17820
18741
  }
17821
18742
  }
@@ -17835,12 +18756,12 @@ function writeKinds(pc, slot) {
17835
18756
  c.fltApplies.fill(0, base, base + 4);
17836
18757
  const kinds = sinks.kindPixels.length;
17837
18758
  for (let kind = 0; kind < kinds; kind += 1) {
17838
- const pixels = cell(sinks.kindPixels, kind);
18759
+ const pixels = cellF64(sinks.kindPixels, kind);
17839
18760
  if (pixels > 0) insertTop(c.fltKind, c.fltMpx, base, 4, kind + 1, pixels / PIXELS_PER_MPX);
17840
18761
  }
17841
18762
  for (let place = 0; place < 4; place += 1) {
17842
- const kind = cell(c.fltKind, base + place);
17843
- if (kind !== 0) c.fltApplies[base + place] = cell(sinks.kindApplies, kind - 1);
18763
+ const kind = cellU8(c.fltKind, base + place);
18764
+ if (kind !== 0) c.fltApplies[base + place] = cellF64(sinks.kindApplies, kind - 1);
17844
18765
  }
17845
18766
  }
17846
18767
  /**
@@ -17854,15 +18775,15 @@ function writeKinds(pc, slot) {
17854
18775
  function accumulate(perf, measure, timing) {
17855
18776
  if (!Number.isNaN(measure.gapMs)) {
17856
18777
  const bucket = bucketOf(measure.gapMs);
17857
- perf.gapHist[bucket] = cell(perf.gapHist, bucket) + 1;
18778
+ perf.gapHist[bucket] = cellU16(perf.gapHist, bucket) + 1;
17858
18779
  }
17859
18780
  perf.listenersSum += measure.listenersMs;
17860
18781
  if (timing === void 0) return;
17861
18782
  const count = Math.min(timing.names.length, timing.ms.length, perf.secSum.length);
17862
18783
  for (let index = 0; index < count; index += 1) {
17863
- const ms = cell(timing.ms, index);
17864
- perf.secSum[index] = cell(perf.secSum, index) + ms;
17865
- if (ms > cell(perf.secMax, index)) perf.secMax[index] = ms;
18784
+ const ms = cellF64(timing.ms, index);
18785
+ perf.secSum[index] = cellF64(perf.secSum, index) + ms;
18786
+ if (ms > cellF32(perf.secMax, index)) perf.secMax[index] = ms;
17866
18787
  }
17867
18788
  }
17868
18789
  /**
@@ -17888,10 +18809,10 @@ function turned(pc) {
17888
18809
  * @param measure - What `time` measured of the frame.
17889
18810
  */
17890
18811
  function stageBudget(perf, measure) {
17891
- const ticksSlower = Number.isNaN(measure.displayMs) && cell(perf.ticks.periodMs, 0) > measure.rateMs;
18812
+ const ticksSlower = Number.isNaN(measure.displayMs) && cellF64(perf.ticks.periodMs, 0) > measure.rateMs;
17892
18813
  perf.staged[0] = measure.rateMs;
17893
18814
  if (measure.displayMs > measure.rateMs) perf.staged[0] = measure.displayMs;
17894
- else if (ticksSlower) perf.staged[0] = cell(perf.ticks.periodMs, 0);
18815
+ else if (ticksSlower) perf.staged[0] = cellF64(perf.ticks.periodMs, 0);
17895
18816
  if (perf.budget !== "rate") perf.staged[0] = perf.budget;
17896
18817
  }
17897
18818
  /**
@@ -17931,7 +18852,7 @@ function gapJudged(pc, measure, previous, littleWork) {
17931
18852
  if (pageHidden()) return false;
17932
18853
  if (previous === -1) return true;
17933
18854
  const c = pc.perf.ring.columns;
17934
- return !(littleWork && measure.gapMs > AWAY_GAP_MS && cell(c.gapMs, previous) > AWAY_GAP_MS) && (pc.perf.drawn || cell(c.drawn, previous) === 1);
18855
+ return !(littleWork && measure.gapMs > AWAY_GAP_MS && cellF32(c.gapMs, previous) > AWAY_GAP_MS) && (pc.perf.drawn || cellU8(c.drawn, previous) === 1);
17935
18856
  }
17936
18857
  /**
17937
18858
  * Writes what `time` measured of the frame into its slot: the clock numbers, the phases, the work, the flags of the
@@ -17952,8 +18873,8 @@ function writeMeasure(pc, slot, measure) {
17952
18873
  c.skipped[slot] = Math.min(SKIPPED_MAX, measure.skipped);
17953
18874
  c.startMs[slot] = measure.startMs;
17954
18875
  c.rateMs[slot] = measure.rateMs;
17955
- c.budgetMs[slot] = cell(perf.staged, 0);
17956
- for (let index = 0; index < 6; index += 1) c.phaseMs[phase + index] = cell(measure.phaseMs, index);
18876
+ c.budgetMs[slot] = cellF64(perf.staged, 0);
18877
+ for (let index = 0; index < 6; index += 1) c.phaseMs[phase + index] = cellF32(measure.phaseMs, index);
17957
18878
  c.agentMs[slot] = perf.agentMs;
17958
18879
  c.workMs[slot] = measure.workMs;
17959
18880
  c.drawn[slot] = perf.drawn ? 1 : 0;
@@ -17978,7 +18899,7 @@ function commitFrame(pc, measure) {
17978
18899
  const slot = ring.written % ring.capacity;
17979
18900
  const previous = ring.size === 0 ? -1 : ring.newest;
17980
18901
  stageBudget(perf, measure);
17981
- const budget = cell(perf.staged, 0);
18902
+ const budget = cellF64(perf.staged, 0);
17982
18903
  const { agentMs } = perf;
17983
18904
  const timing = pc.ctx.deps.world.systemTimes();
17984
18905
  writeMeasure(pc, slot, measure);
@@ -17987,7 +18908,7 @@ function commitFrame(pc, measure) {
17987
18908
  writeKinds(pc, slot);
17988
18909
  const heap = usedHeapBytes();
17989
18910
  const heapDrop = perf.lastHeap - heap;
17990
- let causes = spikeCauses(measure, agentMs, budget, cell(c.capMs, previous));
18911
+ let causes = spikeCauses(measure, agentMs, budget, previous === -1 ? NaN : cellF32(c.capMs, previous));
17991
18912
  const littleWork = measure.workMs - agentMs < budget * GC_WORK_SHARE;
17992
18913
  if ((causes & CAUSE.gap) !== 0 && !gapJudged(pc, measure, previous, littleWork)) causes &= ~CAUSE.gap;
17993
18914
  const cameLate = (causes & CAUSE.gap) !== 0;
@@ -18009,8 +18930,12 @@ function commitFrame(pc, measure) {
18009
18930
  flushPerf(pc);
18010
18931
  }
18011
18932
  accumulate(perf, measure, timing);
18933
+ accumulateProblems(pc, measure);
18012
18934
  if (causes !== 0) addPending(perf, sequenceOf(ring, slot));
18013
- if (perf.drawn && Number.isNaN(perf.firstDrawnAt)) perf.firstDrawnAt = measure.at + measure.workMs;
18935
+ if (perf.drawn && Number.isNaN(perf.firstDrawnAt)) {
18936
+ perf.firstDrawnAt = measure.at + measure.workMs;
18937
+ perf.firstDrawnFrame = measure.frame;
18938
+ }
18014
18939
  if ((perf.flags & (CHANGED.resize | CHANGED.restore)) !== 0 || turned(pc) || retuned(pc)) writeDisplay(pc);
18015
18940
  perf.flags = 0;
18016
18941
  perf.drawn = false;
@@ -18052,6 +18977,19 @@ function labelOf$2(pc, target, kind) {
18052
18977
  return kind === KIND.filter ? pc.sinks.kindNames[pc.sinks.filterKind] ?? null : null;
18053
18978
  }
18054
18979
  /**
18980
+ * Writes the detail of the pass just recorded: for a filter pass the owner and the 1-based kind of the apply running
18981
+ * now, the frame's draws so far, the target's format and resolution.
18982
+ *
18983
+ * @param pc - The perf context.
18984
+ * @param target - The target.
18985
+ * @param kind - The pass kind.
18986
+ */
18987
+ function writeDetail(pc, target, kind) {
18988
+ const { sinks } = pc;
18989
+ const filter = kind === KIND.filter;
18990
+ writePassDetail(pc.perf.ring.passes, filter ? sinks.filterOwner : 0, filter ? sinks.filterKind + 1 : 0, sinks.draws.frame, target.colorTexture?.source);
18991
+ }
18992
+ /**
18055
18993
  * Creates the WebGPU pass sink the counting encoder calls: each pass of a frame goes into the pass ring, with the
18056
18994
  * frame number and its index in the frame, and gets the timer's timestamp writes when there is a timer.
18057
18995
  *
@@ -18064,7 +19002,8 @@ function createPassSink(pc, timer, canvas) {
18064
19002
  return {
18065
19003
  begin: (descriptor, target) => {
18066
19004
  const kind = kindOf$3(pc, target === canvas);
18067
- const sequence = writePass(pc.perf.ring.passes, pc.perf.frame, cell(pc.sinks.counts, COUNT.passes) - 1, kind, descriptor.depthStencilAttachment === void 0 ? 0 : 1, target.pixelWidth, target.pixelHeight, labelOf$2(pc, target, kind));
19005
+ const sequence = writePass(pc.perf.ring.passes, pc.perf.frame, cellF64(pc.sinks.counts, COUNT.passes) - 1, kind, descriptor.depthStencilAttachment === void 0 ? 0 : 1, target.pixelWidth, target.pixelHeight, labelOf$2(pc, target, kind));
19006
+ writeDetail(pc, target, kind);
18068
19007
  timer?.timePass(descriptor, sequence);
18069
19008
  },
18070
19009
  resolve: (commandEncoder) => timer?.resolve(commandEncoder)
@@ -18090,7 +19029,7 @@ function countGlPasses(renderer, sinks, record) {
18090
19029
  */
18091
19030
  const count = (target) => {
18092
19031
  const { counts } = sinks;
18093
- const index = cell(counts, COUNT.passes);
19032
+ const index = cellF64(counts, COUNT.passes);
18094
19033
  counts[COUNT.passes] = index + 1;
18095
19034
  record?.(target, index);
18096
19035
  };
@@ -18136,6 +19075,7 @@ function attachGlPasses(renderer, pc) {
18136
19075
  return countGlPasses(renderer, pc.sinks, (target, index) => {
18137
19076
  const kind = kindOf$3(pc, target.isRoot);
18138
19077
  writePass(pc.perf.ring.passes, pc.perf.frame, index, kind, 255, target.pixelWidth, target.pixelHeight, labelOf$2(pc, target, kind));
19078
+ writeDetail(pc, target, kind);
18139
19079
  });
18140
19080
  }
18141
19081
  /** Queries of one frame slot: two per pass. */
@@ -18179,10 +19119,10 @@ function createBatch() {
18179
19119
  function noteGpuTime(pc, slot) {
18180
19120
  const { ring } = pc.perf;
18181
19121
  const c = ring.columns;
18182
- const before = cell(c.causes, slot);
18183
- const gpuMs = cell(c.gpuMs, slot);
18184
- if (cell(c.stepped, slot) === 1 || (before & CAUSE.gpu) !== 0) return;
18185
- if (Number.isNaN(gpuMs) || gpuMs <= cell(c.budgetMs, slot)) return;
19122
+ const before = cellU8(c.causes, slot);
19123
+ const gpuMs = cellF32(c.gpuMs, slot);
19124
+ if (cellU8(c.stepped, slot) === 1 || (before & CAUSE.gpu) !== 0) return;
19125
+ if (Number.isNaN(gpuMs) || gpuMs <= cellF32(c.budgetMs, slot)) return;
18186
19126
  c.causes[slot] = before | CAUSE.gpu | (before === 0 ? annotate(pc, slot) : 0);
18187
19127
  if (before === 0) addPending(pc.perf, sequenceOf(ring, slot));
18188
19128
  }
@@ -18194,7 +19134,7 @@ function noteGpuTime(pc, slot) {
18194
19134
  * @returns Nanoseconds.
18195
19135
  */
18196
19136
  function stampAt(view, query) {
18197
- return cell(view, 2 * query + 1) * HIGH_WORD + cell(view, 2 * query);
19137
+ return cellU32(view, 2 * query + 1) * HIGH_WORD + cellU32(view, 2 * query);
18198
19138
  }
18199
19139
  /**
18200
19140
  * Writes what the readback says of one frame slot: the time of each pass into its pass-ring entry
@@ -18208,21 +19148,21 @@ function stampAt(view, query) {
18208
19148
  */
18209
19149
  function readFrame(timer, batch, view, frame) {
18210
19150
  const { columns, passes } = timer.pc.perf.ring;
18211
- const count = cell(batch.timed, frame);
19151
+ const count = cellU8(batch.timed, frame);
18212
19152
  let sum = 0;
18213
- let whole = cell(batch.partial, frame) === 0;
19153
+ let whole = cellU8(batch.partial, frame) === 0;
18214
19154
  for (let pass = 0; pass < count; pass += 1) {
18215
19155
  const start = stampAt(view, frame * QUERIES_PER_FRAME + 2 * pass);
18216
19156
  const end = stampAt(view, frame * QUERIES_PER_FRAME + 2 * pass + 1);
18217
- const sequence = cell(batch.passes, frame * 32 + pass);
19157
+ const sequence = cellF64(batch.passes, frame * 32 + pass);
18218
19158
  if (start > 0 && end > start) {
18219
19159
  const ms = (end - start) / NS_PER_MS$1;
18220
19160
  sum += ms;
18221
19161
  if (passes.written - sequence <= passes.capacity) passes.gpuMs[sequence % passes.capacity] = ms;
18222
19162
  } else whole = false;
18223
19163
  }
18224
- const slot = cell(batch.slots, frame);
18225
- if (count === 0 || slot < 0 || cell(columns.frame, slot) !== cell(batch.frames, frame)) return;
19164
+ const slot = cellI32(batch.slots, frame);
19165
+ if (count === 0 || slot < 0 || cellU32(columns.frame, slot) !== cellF64(batch.frames, frame)) return;
18226
19166
  columns.gpuMs[slot] = sum;
18227
19167
  if (whole) columns.gpuPartial[slot] = 0;
18228
19168
  noteGpuTime(timer.pc, slot);
@@ -18299,7 +19239,7 @@ function timePass(timer, descriptor, sequence) {
18299
19239
  if (timer.frameSlot === -1) reserveFrame(timer);
18300
19240
  if (timer.buffer === -1) return;
18301
19241
  const { open } = timer;
18302
- const pass = cell(open.timed, timer.frameSlot);
19242
+ const pass = cellU8(open.timed, timer.frameSlot);
18303
19243
  const write = timer.writes[timer.frameSlot * 32 + pass];
18304
19244
  if (pass >= 32 || write === void 0) {
18305
19245
  open.partial[timer.frameSlot] = 1;
@@ -18320,7 +19260,7 @@ function timePass(timer, descriptor, sequence) {
18320
19260
  function resolveFrame(timer, commandEncoder) {
18321
19261
  if (timer.frameSlot === -1) return;
18322
19262
  const target = timer.buffers[timer.buffer];
18323
- const count = cell(timer.open.timed, timer.frameSlot);
19263
+ const count = cellU8(timer.open.timed, timer.frameSlot);
18324
19264
  const offset = timer.frameSlot * REGION_BYTES;
18325
19265
  if (target === void 0 || count === 0) return;
18326
19266
  commandEncoder.resolveQuerySet(timer.querySet, timer.frameSlot * QUERIES_PER_FRAME, 2 * count, timer.resolveBuffer, offset);
@@ -18467,7 +19407,7 @@ prerender: () => {
18467
19407
  state.tried = true;
18468
19408
  if (state.free === 0) return;
18469
19409
  state.free -= 1;
18470
- state.active = cell(free, state.free);
19410
+ state.active = cellI32(free, state.free);
18471
19411
  const query = pool[state.active];
18472
19412
  if (query !== void 0) gl.beginQuery(extension.TIME_ELAPSED_EXT, query);
18473
19413
  } };
@@ -18487,7 +19427,7 @@ postrender: () => {
18487
19427
  * Takes the query at the head of the queue back into the pool.
18488
19428
  */
18489
19429
  const dequeue = () => {
18490
- free[state.free] = cell(queue, state.head);
19430
+ free[state.free] = cellI32(queue, state.head);
18491
19431
  state.free += 1;
18492
19432
  state.head = (state.head + 1) % POOL_SIZE;
18493
19433
  state.pending -= 1;
@@ -18503,16 +19443,16 @@ postrender: () => {
18503
19443
  return;
18504
19444
  }
18505
19445
  while (state.pending > 0) {
18506
- const query = pool[cell(queue, state.head)];
19446
+ const query = pool[cellI32(queue, state.head)];
18507
19447
  if (query === void 0 || !gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) return;
18508
19448
  const ms = Number(gl.getQueryParameter(query, gl.QUERY_RESULT)) / NS_PER_MS;
18509
- const slot = cell(queueSlots, state.head);
18510
- const frame = cell(queueFrames, state.head);
19449
+ const slot = cellI32(queueSlots, state.head);
19450
+ const frame = cellF64(queueFrames, state.head);
18511
19451
  dequeue();
18512
- if (slot < 0 || cell(columns.frame, slot) !== frame) continue;
18513
- const before = cell(columns.gpuMs, slot);
19452
+ if (slot < 0 || cellU32(columns.frame, slot) !== frame) continue;
19453
+ const before = cellF32(columns.gpuMs, slot);
18514
19454
  columns.gpuMs[slot] = Number.isNaN(before) ? ms : before + ms;
18515
- if (state.pending === 0 || cell(queueFrames, state.head) !== frame) columns.gpuPartial[slot] = 0;
19455
+ if (state.pending === 0 || cellF64(queueFrames, state.head) !== frame) columns.gpuPartial[slot] = 0;
18516
19456
  noteGpuTime(pc, slot);
18517
19457
  }
18518
19458
  };
@@ -18523,7 +19463,7 @@ postrender: () => {
18523
19463
  commit: (slot, frame) => {
18524
19464
  for (let index = 0; index < state.pending; index += 1) {
18525
19465
  const at = (state.head + index) % POOL_SIZE;
18526
- if (cell(queueFrames, at) === frame) queueSlots[at] = slot;
19466
+ if (cellF64(queueFrames, at) === frame) queueSlots[at] = slot;
18527
19467
  }
18528
19468
  if (state.tried) pc.perf.ring.columns.gpuPartial[slot] = 1;
18529
19469
  state.tried = false;
@@ -18670,11 +19610,15 @@ function attachFirst(pc) {
18670
19610
  perf.flags |= CHANGED.restore;
18671
19611
  writeEvent(pc, "device-restored", { kind: host.kind() });
18672
19612
  });
18673
- host.onTrim((reason, freedMb) => writeEvent(pc, "pool-trim", {
18674
- reason,
18675
- freedMb
18676
- }));
19613
+ host.onTrim((reason, freedMb) => {
19614
+ perf.problems.trimAt = pageNow();
19615
+ writeEvent(pc, "pool-trim", {
19616
+ reason,
19617
+ freedMb
19618
+ });
19619
+ });
18677
19620
  deps.sync.onWarm((draws) => writeEvent(pc, "warm", { draws }));
19621
+ deps.sync.onOverdraw((on) => writeEvent(pc, "debug", { overdraw: on }));
18678
19622
  notePath(pc, ctx.deps.flow.state().path, false);
18679
19623
  perf.display = displayOf(pc);
18680
19624
  }
@@ -18740,17 +19684,19 @@ function attachPerf(pc) {
18740
19684
  perf.timer = void 0;
18741
19685
  sinks.passes = void 0;
18742
19686
  sinks.filterKinds = ctx.state.sync.filterKinds;
19687
+ sinks.filterOwners = ctx.state.sync.filterOwners;
18743
19688
  const counted = ctx.state.host.uninstallCounting !== void 0;
18744
19689
  const kind = pc.deps.host.kind();
18745
19690
  let bits = counted ? MEASURE.counted : 0;
19691
+ if (counted && kind === "webgpu") bits |= MEASURE.targets;
18746
19692
  if (kind === "webgpu") bits |= attachGpu(pc, counted);
18747
19693
  else if (kind === "webgl") bits |= attachGl(pc);
18748
19694
  perf.measuring = bits;
18749
19695
  }
18750
19696
  /**
18751
19697
  * Stops the monitor of an agent build: the per-application removers run, the timer stops, the observer disconnects,
18752
- * the listeners, the pending spikes, the paths and the display's ticks go. The ring keeps its arrays (a restart of the same app reuses
18753
- * them), empty.
19698
+ * the listeners, the pending spikes, the paths, the display's ticks, the registered counts and the problem
19699
+ * accumulators go. The ring keeps its arrays (a restart of the same app reuses them), empty.
18754
19700
  *
18755
19701
  * @param perf - The perf state.
18756
19702
  * @param sinks - The sinks.
@@ -18783,7 +19729,14 @@ function stopPerf(perf, sinks) {
18783
19729
  resetTicks(perf.ticks);
18784
19730
  sinks.passes = void 0;
18785
19731
  sinks.filterKinds = void 0;
19732
+ sinks.filterOwners = void 0;
19733
+ sinks.filterOwner = 0;
18786
19734
  sinks.inFrame = false;
19735
+ clearCounts(perf.counts);
19736
+ resetProblems({
19737
+ perf,
19738
+ sinks
19739
+ });
18787
19740
  }
18788
19741
  //#endregion
18789
19742
  //#region src/plugins/renderer/monitor/window.ts
@@ -18887,6 +19840,10 @@ function resetWindow(state) {
18887
19840
  */
18888
19841
  const noStream = () => {};
18889
19842
  /**
19843
+ * The remover `counts.register` answers in a build without the agent flag: it does nothing.
19844
+ */
19845
+ const noCount = () => {};
19846
+ /**
18890
19847
  * The detail of an event written without one.
18891
19848
  */
18892
19849
  const NO_DETAIL = Object.freeze({});
@@ -18954,7 +19911,7 @@ function createMonitorApi(ctx, deps) {
18954
19911
  * @param drawn - True when the render phase drew the frame.
18955
19912
  */
18956
19913
  const notePasses = (drawn) => {
18957
- const count = cell(sinks.counts, COUNT.passes);
19914
+ const count = cellF64(sinks.counts, COUNT.passes);
18958
19915
  if (drawn && count > 0) {
18959
19916
  passes.last = count;
18960
19917
  passes.open = Math.max(passes.open, count);
@@ -18984,12 +19941,17 @@ function createMonitorApi(ctx, deps) {
18984
19941
  kept: deps.kept.stats()
18985
19942
  };
18986
19943
  if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) return stats;
18987
- return {
19944
+ const dev = {
18988
19945
  ...stats,
18989
19946
  drawCalls: state.draws.last,
18990
19947
  redraws: roundTo(state.redraws, 1),
18991
19948
  rebuilds: roundTo(state.rebuilds, 1)
18992
19949
  };
19950
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) return {
19951
+ ...dev,
19952
+ counts: readCounts(pc)
19953
+ };
19954
+ return dev;
18993
19955
  },
18994
19956
  capture: async (options = {}) => {
18995
19957
  if ((typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) && (typeof __MOKU_GAME_AGENT__ === "undefined" || !__MOKU_GAME_AGENT__)) return;
@@ -19015,6 +19977,10 @@ function createMonitorApi(ctx, deps) {
19015
19977
  endAgent: () => {
19016
19978
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) endAgent(pc);
19017
19979
  },
19980
+ counts: { register: (name, read) => {
19981
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) return registerCount(pc.perf.counts, name, read);
19982
+ return noCount;
19983
+ } },
19018
19984
  setResolution: (resolution) => {
19019
19985
  if (!takesResolution(resolution)) throw new RangeError(resolutionMessage(resolution));
19020
19986
  return deps.host.overrideResolution(resolution);
@@ -19062,7 +20028,7 @@ function createMonitorApi(ctx, deps) {
19062
20028
  count: (_counter, amount) => {
19063
20029
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) {
19064
20030
  pc.perf.produced |= 1;
19065
- pc.sinks.counts[COUNT.rebuilds] = cell(pc.sinks.counts, COUNT.rebuilds) + amount;
20031
+ pc.sinks.counts[COUNT.rebuilds] = cellF64(pc.sinks.counts, COUNT.rebuilds) + amount;
19066
20032
  }
19067
20033
  },
19068
20034
  path: (path, rest) => {
@@ -19509,21 +20475,33 @@ function filtersOf(state, entity) {
19509
20475
  return state.filters.get(entity) ?? [];
19510
20476
  }
19511
20477
  /**
19512
- * Notes the kind of a slot's filter for the performance log: the filter itself, the stage filters
19513
- * of its inputs and, for a blur, the two pass filters Pixi applies in its place. A slot without a
19514
- * kind is left out: it counts as "other".
20478
+ * The Pixi filters a slot draws through: the filter itself, the stage filters of its inputs and, for a blur, the two
20479
+ * pass filters Pixi applies in its place.
20480
+ *
20481
+ * @param slot - The slot.
20482
+ * @returns The filters.
20483
+ */
20484
+ function filtersIn(slot) {
20485
+ const { filter } = slot;
20486
+ const found = [filter, ...slot.inputs?.stages ?? []];
20487
+ if ("blurXFilter" in filter && isFilterObject(filter.blurXFilter)) found.push(filter.blurXFilter);
20488
+ if ("blurYFilter" in filter && isFilterObject(filter.blurYFilter)) found.push(filter.blurYFilter);
20489
+ return found;
20490
+ }
20491
+ /**
20492
+ * Notes the kind and the owner of a slot's filters for the performance log: every filter it draws through. A slot
20493
+ * without a kind counts as "other".
19515
20494
  *
19516
20495
  * @param state - The sync branch of the plugin state.
20496
+ * @param entity - The entity the slot hangs on.
19517
20497
  * @param slot - The slot.
19518
20498
  */
19519
- function nameKind(state, slot) {
19520
- const { filter, kind } = slot;
19521
- if (kind === void 0) return;
19522
- state.filterKinds.set(filter, kind);
19523
- const stages = slot.inputs?.stages;
19524
- if (stages !== void 0) for (const stage of stages) state.filterKinds.set(stage, kind);
19525
- if ("blurXFilter" in filter && isFilterObject(filter.blurXFilter)) state.filterKinds.set(filter.blurXFilter, kind);
19526
- if ("blurYFilter" in filter && isFilterObject(filter.blurYFilter)) state.filterKinds.set(filter.blurYFilter, kind);
20499
+ function nameFilters(state, entity, slot) {
20500
+ const { kind } = slot;
20501
+ for (const filter of filtersIn(slot)) {
20502
+ state.filterOwners.set(filter, entity);
20503
+ if (kind !== void 0) state.filterKinds.set(filter, kind);
20504
+ }
19527
20505
  }
19528
20506
  /**
19529
20507
  * Tells whether a property a filter may carry holds an object: the pass filter Pixi's blur keeps under it. Pure.
@@ -19552,7 +20530,7 @@ function isFilterObject(value) {
19552
20530
  function setFilters(state, entity, slots, mark, kept) {
19553
20531
  if (slots.length === 0) state.filters.delete(entity);
19554
20532
  else state.filters.set(entity, [...slots]);
19555
- if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) for (const slot of slots) nameKind(state, slot);
20533
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) for (const slot of slots) nameFilters(state, entity, slot);
19556
20534
  const view = state.views.get(entity);
19557
20535
  if (view !== void 0) applyFilters(view, slots, () => mark(entity), kept.filterTarget(view));
19558
20536
  else if (slots.length > 0) state.viewlessEntries = true;
@@ -23488,6 +24466,7 @@ function stopSync(state) {
23488
24466
  stopWarm(state);
23489
24467
  state.held.clear();
23490
24468
  state.redraws = 0;
24469
+ state.overdraw.listeners.length = 0;
23491
24470
  clearLayers(state);
23492
24471
  destroyPools(state);
23493
24472
  state.root?.destroy({ children: true });
@@ -23633,7 +24612,8 @@ function withDeps$8(ctx) {
23633
24612
  * oracle; `demand` settles the
23634
24613
  * frame (the dev check, the pixel oracle), and `monitor` closes it and hands a drawn one to a
23635
24614
  * waiting capture; last, `host` trims Pixi's texture pool when a rest or a scene change is due. The display objects
23636
- * are written by the `sync` system.
24615
+ * are written by the `sync` system. While the overdraw heat map of a dev or debug build is on, every frame asks to be
24616
+ * drawn and its stage pass is drawn as the map (`sync.drawOverdraw`).
23637
24617
  *
23638
24618
  * With `onDemand`, a frame in which nothing visible changed is not drawn: the canvas keeps the
23639
24619
  * last picture (WebGPU presents a canvas texture only when one is rendered; a WebGL canvas keeps
@@ -23655,11 +24635,13 @@ function renderFrame(ctx, modules) {
23655
24635
  modules.sync.stepOverlays();
23656
24636
  modules.sync.drainOverlayEvents(modules.monitor);
23657
24637
  modules.kept.collect();
24638
+ const overdraw = modules.sync.overdrawing();
24639
+ if (overdraw) modules.demand.request();
23658
24640
  const drawn = modules.demand.decide(modules.monitor.waiting());
23659
24641
  if (drawn) {
23660
24642
  const probed = modules.sync.warmStep();
23661
24643
  modules.kept.flush();
23662
- if (modules.host.render()) {
24644
+ if (overdraw ? modules.sync.drawOverdraw(modules.host.render) : modules.host.render()) {
23663
24645
  ctx.deps.time.frameDrawn();
23664
24646
  if (probed) ctx.deps.time.frameProbed();
23665
24647
  }
@@ -23753,6 +24735,61 @@ function internKind(sinks, kind) {
23753
24735
  return sinks.kindNames.length - 1;
23754
24736
  }
23755
24737
  /**
24738
+ * The slot of an owner in the owner table of the frame: its own slot, else the first free one, which it takes. An
24739
+ * indexed search over at most 32 slots: no allocation. A full table tracks no more owners.
24740
+ *
24741
+ * @param owners - The owner table.
24742
+ * @param owner - The entity, not 0.
24743
+ * @returns The slot, or -1 when the table is full.
24744
+ */
24745
+ function ownerSlot(owners, owner) {
24746
+ let free = -1;
24747
+ for (let slot = 0; slot < 32; slot += 1) {
24748
+ const id = cellF64(owners.ids, slot);
24749
+ if (id === owner) return slot;
24750
+ if (id === 0 && free === -1) free = slot;
24751
+ }
24752
+ if (free !== -1) owners.ids[free] = owner;
24753
+ return free;
24754
+ }
24755
+ /**
24756
+ * Counts one filter apply of a frame: its pixels (width times height times resolution squared), its interned kind and
24757
+ * its owner, into the frame's counters, the kinds' and the owner table, and notes the kind and the owner being applied
24758
+ * so a pass begun inside is named by them. A plain function apart from the class: indexed stores only.
24759
+ *
24760
+ * @param sinks - The sinks.
24761
+ * @param filter - The filter applied.
24762
+ * @param input - Its input texture.
24763
+ */
24764
+ function countApply(sinks, filter, input) {
24765
+ const { counts, owners } = sinks;
24766
+ const resolution = input.source.resolution;
24767
+ const pixels = input.frame.width * input.frame.height * resolution * resolution;
24768
+ const kind = internKind(sinks, sinks.filterKinds?.get(filter));
24769
+ const owner = sinks.filterOwners?.get(filter) ?? 0;
24770
+ const slot = owner === 0 ? -1 : ownerSlot(owners, owner);
24771
+ counts[COUNT.filterApplies] = cellF64(counts, COUNT.filterApplies) + 1;
24772
+ counts[COUNT.filterPixels] = cellF64(counts, COUNT.filterPixels) + pixels;
24773
+ sinks.kindApplies[kind] = cellF64(sinks.kindApplies, kind) + 1;
24774
+ sinks.kindPixels[kind] = cellF64(sinks.kindPixels, kind) + pixels;
24775
+ sinks.filterKind = kind;
24776
+ sinks.filterOwner = owner;
24777
+ if (slot !== -1) {
24778
+ owners.applies[slot] = cellF64(owners.applies, slot) + 1;
24779
+ owners.pixels[slot] = cellF64(owners.pixels, slot) + pixels;
24780
+ owners.kinds[slot] = kind + 1;
24781
+ }
24782
+ }
24783
+ /**
24784
+ * Tells whether a source is a render target: it has no upload method, as `countUpload` tells them apart.
24785
+ *
24786
+ * @param source - The source.
24787
+ * @returns True for a render target.
24788
+ */
24789
+ function isTarget(source) {
24790
+ return source.uploadMethodId === "unknown";
24791
+ }
24792
+ /**
23756
24793
  * The bytes one upload of a source sends: 4 a pixel, or for a compressed source the sum of its
23757
24794
  * levels' `byteLength`.
23758
24795
  *
@@ -23778,8 +24815,8 @@ function countUpload(sinks, source) {
23778
24815
  if (source.uploadMethodId === "unknown") return;
23779
24816
  if (source.resource === null || source.resource === void 0) return;
23780
24817
  const { counts } = sinks;
23781
- counts[COUNT.uploads] = cell(counts, COUNT.uploads) + 1;
23782
- counts[COUNT.uploadBytes] = cell(counts, COUNT.uploadBytes) + uploadBytes(source);
24818
+ counts[COUNT.uploads] = cellF64(counts, COUNT.uploads) + 1;
24819
+ counts[COUNT.uploadBytes] = cellF64(counts, COUNT.uploadBytes) + uploadBytes(source);
23783
24820
  }
23784
24821
  /**
23785
24822
  * Builds the seven counting subclasses from the module object. They are made at call time,
@@ -23804,7 +24841,7 @@ function createCountingClasses(pixi, sinks) {
23804
24841
  * @param instructionSet - The instruction set being rebuilt.
23805
24842
  */
23806
24843
  buildStart(instructionSet) {
23807
- if (sinks.inFrame) sinks.counts[COUNT.rebuilds] = cell(sinks.counts, COUNT.rebuilds) + 1;
24844
+ if (sinks.inFrame) sinks.counts[COUNT.rebuilds] = cellF64(sinks.counts, COUNT.rebuilds) + 1;
23808
24845
  sinks.rebuildsWindow += 1;
23809
24846
  super.buildStart(instructionSet);
23810
24847
  }
@@ -23885,8 +24922,8 @@ function createCountingClasses(pixi, sinks) {
23885
24922
  if (sinks.inFrame) {
23886
24923
  const { counts } = sinks;
23887
24924
  const { descriptor } = gpuRenderTarget;
23888
- counts[COUNT.passes] = cell(counts, COUNT.passes) + 1;
23889
- if (descriptor.depthStencilAttachment !== void 0) counts[COUNT.stencilPasses] = cell(counts, COUNT.stencilPasses) + 1;
24925
+ counts[COUNT.passes] = cellF64(counts, COUNT.passes) + 1;
24926
+ if (descriptor.depthStencilAttachment !== void 0) counts[COUNT.stencilPasses] = cellF64(counts, COUNT.stencilPasses) + 1;
23890
24927
  sinks.passes?.begin(descriptor, this.owner.renderTarget.renderTarget);
23891
24928
  }
23892
24929
  return super.beginRenderPass(gpuRenderTarget);
@@ -23905,34 +24942,50 @@ function createCountingClasses(pixi, sinks) {
23905
24942
  class CountingFilterSystem extends pixi.FilterSystem {
23906
24943
  static extension = pixi.FilterSystem.extension;
23907
24944
  /**
23908
- * Counts the apply and its area inside a frame, notes the kind being applied so a pass begun
23909
- * inside is a filter pass named by it, then applies.
24945
+ * Counts the apply and its area inside a frame, notes the kind and the owner being applied so a pass begun
24946
+ * inside is a filter pass named by them, then applies. Its four arguments are named, not spread: a rest array
24947
+ * would be made at every apply.
23910
24948
  *
23911
- * @param args - The filter, its input, the output and the clear.
24949
+ * @param filter - The filter.
24950
+ * @param input - Its input texture.
24951
+ * @param output - Where it draws.
24952
+ * @param clear - Whether the output is cleared first.
23912
24953
  */
23913
- applyFilter(...args) {
23914
- const [filter, input] = args;
23915
- if (sinks.inFrame) {
23916
- const { counts } = sinks;
23917
- const pixels = input.frame.width * input.frame.height * input.source.resolution ** 2;
23918
- const kind = internKind(sinks, sinks.filterKinds?.get(filter));
23919
- counts[COUNT.filterApplies] = cell(counts, COUNT.filterApplies) + 1;
23920
- counts[COUNT.filterPixels] = cell(counts, COUNT.filterPixels) + pixels;
23921
- sinks.kindApplies[kind] = cell(sinks.kindApplies, kind) + 1;
23922
- sinks.kindPixels[kind] = cell(sinks.kindPixels, kind) + pixels;
23923
- sinks.filterKind = kind;
23924
- }
24954
+ applyFilter(filter, input, output, clear) {
24955
+ if (sinks.inFrame) countApply(sinks, filter, input);
23925
24956
  sinks.filterDepth += 1;
23926
24957
  try {
23927
- super.applyFilter(...args);
24958
+ super.applyFilter(filter, input, output, clear);
23928
24959
  } finally {
23929
24960
  sinks.filterDepth -= 1;
23930
24961
  }
23931
24962
  }
23932
24963
  }
23933
- /** Counts every upload of the WebGPU texture system. */
24964
+ /**
24965
+ * Counts every upload of the WebGPU texture system, and every render target it creates and drops: a target's
24966
+ * source is created once in `initSource` and dropped once in `onSourceUnload` (a destroy, a pool trim, a lost
24967
+ * device). Pixi calls `initSource` again for a source it already made (it answers the same texture then): the set of
24968
+ * the targets made tells a new one.
24969
+ */
23934
24970
  class CountingGpuTextureSystem extends pixi.GpuTextureSystem {
23935
24971
  static extension = pixi.GpuTextureSystem.extension;
24972
+ /** The render-target sources this system made and has not dropped. */
24973
+ targets = /* @__PURE__ */ new WeakSet();
24974
+ /**
24975
+ * Makes the GPU texture of a source, and counts a render target made for the first time.
24976
+ *
24977
+ * @param source - The source.
24978
+ * @returns The GPU texture.
24979
+ */
24980
+ initSource(source) {
24981
+ const made = isTarget(source) && !this.targets.has(source);
24982
+ const texture = super.initSource(source);
24983
+ if (made) {
24984
+ this.targets.add(source);
24985
+ sinks.counts[COUNT.targetsCreated] = cellF64(sinks.counts, COUNT.targetsCreated) + 1;
24986
+ }
24987
+ return texture;
24988
+ }
23936
24989
  /**
23937
24990
  * Uploads the source, then counts it; the first upload goes through here too.
23938
24991
  *
@@ -23942,6 +24995,15 @@ function createCountingClasses(pixi, sinks) {
23942
24995
  super.onSourceUpdate(source);
23943
24996
  countUpload(sinks, source);
23944
24997
  }
24998
+ /**
24999
+ * Drops the source's GPU texture, and counts a render target dropped.
25000
+ *
25001
+ * @param source - The source.
25002
+ */
25003
+ onSourceUnload(source) {
25004
+ if (this.targets.delete(source)) sinks.counts[COUNT.targetsDestroyed] = cellF64(sinks.counts, COUNT.targetsDestroyed) + 1;
25005
+ super.onSourceUnload(source);
25006
+ }
23945
25007
  }
23946
25008
  /** Counts every upload of the WebGL texture system. */
23947
25009
  class CountingGlTextureSystem extends pixi.GlTextureSystem {
@@ -24034,7 +25096,7 @@ function installPassCounting(pixi, sinks) {
24034
25096
  * @returns The pass encoder.
24035
25097
  */
24036
25098
  beginRenderPass(gpuRenderTarget) {
24037
- if (sinks.inFrame) sinks.counts[COUNT.passes] = cell(sinks.counts, COUNT.passes) + 1;
25099
+ if (sinks.inFrame) sinks.counts[COUNT.passes] = cellF64(sinks.counts, COUNT.passes) + 1;
24038
25100
  return super.beginRenderPass(gpuRenderTarget);
24039
25101
  }
24040
25102
  }
@@ -24370,6 +25432,162 @@ function layerOf(sctx, entity) {
24370
25432
  return ecs.get(top, Layer)?.name;
24371
25433
  }
24372
25434
  //#endregion
25435
+ //#region src/plugins/renderer/sync/overdraw.ts
25436
+ /** The alpha one layer adds: 16 layers make white. */
25437
+ const LAYER_ALPHA = 1 / 16;
25438
+ /** The blend mode whose alpha adds up: `src + dst`. */
25439
+ const ADDITIVE = "add-npm";
25440
+ /**
25441
+ * The colour matrix of the heat map: red, green and blue are the frame's alpha, the alpha is 1. Rows of five: the
25442
+ * weights of red, green, blue and alpha, and an offset.
25443
+ *
25444
+ * @returns A fresh matrix.
25445
+ */
25446
+ function alphaToGrey() {
25447
+ return [
25448
+ 0,
25449
+ 0,
25450
+ 0,
25451
+ 1,
25452
+ 0,
25453
+ 0,
25454
+ 0,
25455
+ 0,
25456
+ 1,
25457
+ 0,
25458
+ 0,
25459
+ 0,
25460
+ 0,
25461
+ 1,
25462
+ 0,
25463
+ 0,
25464
+ 0,
25465
+ 0,
25466
+ 0,
25467
+ 1
25468
+ ];
25469
+ }
25470
+ /**
25471
+ * Creates the overdraw state: off, an empty restore table, no matrix yet.
25472
+ *
25473
+ * @returns The state.
25474
+ */
25475
+ function createOverdrawState() {
25476
+ return {
25477
+ nodes: [],
25478
+ alphas: [],
25479
+ blends: [],
25480
+ filters: [],
25481
+ rootFilters: void 0,
25482
+ matrix: void 0,
25483
+ listeners: []
25484
+ };
25485
+ }
25486
+ /**
25487
+ * The filters a node holds now: Pixi answers `undefined` before the first write and `null` after a clear.
25488
+ *
25489
+ * @param node - The node.
25490
+ * @returns The filters, or `undefined` when it holds none.
25491
+ */
25492
+ function heldBy(node) {
25493
+ const held = node.filters;
25494
+ return held === null || held === void 0 || held.length === 0 ? void 0 : held;
25495
+ }
25496
+ /**
25497
+ * Notes a node's alpha, blend mode and filters in the restore table and gives it the overdraw state.
25498
+ *
25499
+ * @param state - The overdraw state.
25500
+ * @param node - The node.
25501
+ * @param alpha - Its overdraw alpha when it draws at all.
25502
+ */
25503
+ function swapNode(state, node, alpha) {
25504
+ const filters = heldBy(node);
25505
+ state.nodes.push(node);
25506
+ state.alphas.push(node.alpha);
25507
+ state.blends.push(node.blendMode);
25508
+ state.filters.push(filters);
25509
+ if (node.alpha > 0) node.alpha = alpha;
25510
+ node.blendMode = ADDITIVE;
25511
+ if (filters !== void 0) node.filters = null;
25512
+ }
25513
+ /**
25514
+ * Gives a subtree the overdraw state, a node that is not drawn (invisible, or not renderable) left as it is with its
25515
+ * children.
25516
+ *
25517
+ * @param state - The overdraw state.
25518
+ * @param top - The subtree's top.
25519
+ * @param alpha - The top's overdraw alpha.
25520
+ */
25521
+ function swapTree(state, top, alpha) {
25522
+ const open = [top];
25523
+ for (let node = open.pop(); node !== void 0; node = open.pop()) {
25524
+ if (!node.visible || !node.renderable) continue;
25525
+ swapNode(state, node, node === top ? alpha : 1);
25526
+ for (const child of node.children) open.push(child);
25527
+ }
25528
+ }
25529
+ /**
25530
+ * The colour matrix of the heat map, made once per Pixi module.
25531
+ *
25532
+ * @param state - The overdraw state.
25533
+ * @param pixi - The Pixi module.
25534
+ * @returns The filter.
25535
+ */
25536
+ function matrixOf(state, pixi) {
25537
+ if (state.matrix !== void 0) return state.matrix;
25538
+ const matrix = new pixi.ColorMatrixFilter();
25539
+ matrix.matrix = alphaToGrey();
25540
+ state.matrix = matrix;
25541
+ return matrix;
25542
+ }
25543
+ /**
25544
+ * Puts every node back as the restore table holds it, the root's filters last; the table is empty after.
25545
+ *
25546
+ * @param state - The overdraw state.
25547
+ * @param root - The root.
25548
+ */
25549
+ function restoreAll(state, root) {
25550
+ for (let index = state.nodes.length - 1; index >= 0; index -= 1) {
25551
+ const node = state.nodes[index];
25552
+ if (node === void 0) continue;
25553
+ node.alpha = state.alphas[index] ?? 1;
25554
+ node.blendMode = state.blends[index] ?? "inherit";
25555
+ const filters = state.filters[index];
25556
+ if (filters !== void 0) node.filters = [...filters];
25557
+ }
25558
+ root.filters = state.rootFilters === void 0 ? null : [...state.rootFilters];
25559
+ state.nodes.length = 0;
25560
+ state.alphas.length = 0;
25561
+ state.blends.length = 0;
25562
+ state.filters.length = 0;
25563
+ state.rootFilters = void 0;
25564
+ }
25565
+ /**
25566
+ * Draws the stage pass as the overdraw heat map: every kept picture live, every layer and node in the overdraw state
25567
+ * and the matrix on the root for the one `render` call, and everything back right after it, also when it throws.
25568
+ * Without a root or a Pixi module it only renders.
25569
+ *
25570
+ * @param sctx - Domain context of the sync module.
25571
+ * @param render - The stage pass.
25572
+ * @returns What `render` answered.
25573
+ */
25574
+ function drawOverdraw(sctx, render) {
25575
+ const { root } = sctx.ctx.state.sync;
25576
+ const pixi = sctx.deps.host.pixi();
25577
+ if (root === void 0 || pixi === void 0) return render();
25578
+ const state = sctx.ctx.state.sync.overdraw;
25579
+ return sctx.deps.kept.live(() => {
25580
+ try {
25581
+ state.rootFilters = heldBy(root);
25582
+ for (const layer of root.children) swapTree(state, layer, LAYER_ALPHA);
25583
+ root.filters = [matrixOf(state, pixi)];
25584
+ return render();
25585
+ } finally {
25586
+ restoreAll(state, root);
25587
+ }
25588
+ });
25589
+ }
25590
+ //#endregion
24373
25591
  //#region src/plugins/renderer/sync/overlay-look.ts
24374
25592
  /**
24375
25593
  * Makes the dim of one overlay: a sprite of Pixi's white texture, never a view.
@@ -24569,7 +25787,7 @@ function stepParts(sctx) {
24569
25787
  if (overlay.parts.size === 0 && overlay.offTops.size === 0) return;
24570
25788
  const hud = hudLayerOf(state);
24571
25789
  const ecs = sctx.ctx.deps.world.ecs;
24572
- overlay.nextOff.clear();
25790
+ if (overlay.nextOff.size > 0) overlay.nextOff.clear();
24573
25791
  for (const entity of overlay.parts) {
24574
25792
  const view = state.views.get(entity);
24575
25793
  if (view === void 0) continue;
@@ -24942,7 +26160,7 @@ function stepOverlays(sctx) {
24942
26160
  const overlay = state.overlay;
24943
26161
  if (state.root === void 0 || overlaysIdle(overlay)) return;
24944
26162
  const wholes = sctx.deps.demand.wholes();
24945
- overlay.flipped.clear();
26163
+ if (overlay.flipped.size > 0) overlay.flipped.clear();
24946
26164
  if (wholes.has("layers")) clearRange(sctx);
24947
26165
  for (const record of overlay.records.values()) record.seen = false;
24948
26166
  for (const entity of overlay.entities) seeOverlay(sctx, entity);
@@ -25445,7 +26663,7 @@ function createSyncApi(ctx, deps) {
25445
26663
  * view. A still frame (nothing added, removed or left without a view) builds no set and walks no entry.
25446
26664
  */
25447
26665
  const pass = () => {
25448
- state.held.clear();
26666
+ if (state.held.size > 0) state.held.clear();
25449
26667
  if (state.removed.size > 0) {
25450
26668
  for (const entity of state.removed) {
25451
26669
  guard(entity, () => dropView(sctx, entity));
@@ -25561,6 +26779,16 @@ function createSyncApi(ctx, deps) {
25561
26779
  },
25562
26780
  verifyFrames: (on) => deps.demand.verifyFrames(on),
25563
26781
  kept: (on) => deps.kept.enabled(on),
26782
+ overdraw: (on) => {
26783
+ const next = on && (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__);
26784
+ if (state.debug.overdraw === next) return;
26785
+ state.debug.overdraw = next;
26786
+ deps.demand.request();
26787
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) {
26788
+ ctx.log.info("renderer:overdraw", { on: next });
26789
+ for (const listener of state.overdraw.listeners) listener(next);
26790
+ }
26791
+ },
25564
26792
  overlays: () => overlayReports(sctx),
25565
26793
  state: () => ({
25566
26794
  ...state.debug,
@@ -25583,6 +26811,14 @@ function createSyncApi(ctx, deps) {
25583
26811
  onWarm: (fn) => {
25584
26812
  state.warm.listeners.push(fn);
25585
26813
  },
26814
+ overdrawing: () => state.debug.overdraw,
26815
+ drawOverdraw: (render) => {
26816
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) return drawOverdraw(sctx, render);
26817
+ return render();
26818
+ },
26819
+ onOverdraw: (fn) => {
26820
+ state.overdraw.listeners.push(fn);
26821
+ },
25586
26822
  loopsHeld: (entity, reveals) => loopsHeld(sctx, entity) || unseenHolds(sctx, entity, reveals),
25587
26823
  takeRedraws: () => {
25588
26824
  const redraws = state.redraws;
@@ -26184,7 +27420,8 @@ function createRendererApi(ctx) {
26184
27420
  event: monitor.event,
26185
27421
  beginAgent: monitor.beginAgent,
26186
27422
  endAgent: monitor.endAgent,
26187
- setResolution: monitor.setResolution
27423
+ setResolution: monitor.setResolution,
27424
+ counts: monitor.counts
26188
27425
  },
26189
27426
  invalidate: demand.invalidate,
26190
27427
  stats: monitor.stats,
@@ -26268,6 +27505,9 @@ function createHandlers$5(ctx) {
26268
27505
  //#endregion
26269
27506
  //#region src/plugins/renderer/sync/state.ts
26270
27507
  /**
27508
+ * @file renderer/sync — state factory.
27509
+ */
27510
+ /**
26271
27511
  * Creates an empty collection. It lives in its own non-exported function because lint rule L5
26272
27512
  * refuses a collection built inside an exported declaration.
26273
27513
  *
@@ -26354,6 +27594,7 @@ function createWarmState() {
26354
27594
  * @returns The sync branch of the plugin state.
26355
27595
  */
26356
27596
  function createSyncState(debug) {
27597
+ const overdrawInDebugBuild = debug.overdraw === true && (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__);
26357
27598
  return {
26358
27599
  root: void 0,
26359
27600
  layerList: void 0,
@@ -26376,13 +27617,18 @@ function createSyncState(debug) {
26376
27617
  reparented: emptySet$4(),
26377
27618
  orphans: emptySet$4(),
26378
27619
  mismatched: emptySet$4(),
26379
- debug: { nineSlice: debug.nineSlice === true },
27620
+ debug: {
27621
+ nineSlice: debug.nineSlice === true,
27622
+ overdraw: overdrawInDebugBuild
27623
+ },
27624
+ overdraw: createOverdrawState(),
26380
27625
  outlinesStale: false,
26381
27626
  filters: emptyMap$6(),
26382
27627
  areas: emptyMap$6(),
26383
27628
  viewlessEntries: false,
26384
27629
  warm: createWarmState(),
26385
27630
  filterKinds: emptyWeakMap$1(),
27631
+ filterOwners: emptyWeakMap$1(),
26386
27632
  objectHidden: emptyWeakMap$1(),
26387
27633
  viewOfTop: emptyWeakMap$1(),
26388
27634
  overlay: createOverlayState(),
@@ -26553,11 +27799,17 @@ const rendererPlugin = /*#__PURE__*/ createPlugin$1("renderer", {
26553
27799
  debug: {
26554
27800
  nineSlice: false,
26555
27801
  verifyFrames: false,
26556
- kept: true
27802
+ kept: true,
27803
+ overdraw: false
26557
27804
  },
26558
27805
  monitor: {
26559
27806
  budgetMs: "rate",
26560
- frames: 1024
27807
+ frames: 1024,
27808
+ stillFrames: 60,
27809
+ stillMpx: .25,
27810
+ churnPerSecond: 4,
27811
+ churnSeconds: 3,
27812
+ churnAfterTrimMs: 1e3
26561
27813
  },
26562
27814
  onDemand: false,
26563
27815
  keptMb: 64,
@@ -41466,11 +42718,72 @@ function stepGestures(ctx, time) {
41466
42718
  pointer.id = ctx.state.pointerId ?? -1;
41467
42719
  }
41468
42720
  //#endregion
42721
+ //#region src/plugins/input/state.ts
42722
+ /**
42723
+ * Creates the initial input state: an empty sample queue, no active pointer and no gesture. Every
42724
+ * duration starts at zero; the plugin never reads a device clock.
42725
+ *
42726
+ * @param _ctx - Minimal context.
42727
+ * @param _ctx.config - Resolved plugin config.
42728
+ * @returns A fresh state, owned by one app.
42729
+ */
42730
+ function createInputState(_ctx) {
42731
+ return {
42732
+ samples: [],
42733
+ phase: "idle",
42734
+ pointerId: void 0,
42735
+ moved: false,
42736
+ entity: void 0,
42737
+ key: void 0,
42738
+ start: {
42739
+ x: 0,
42740
+ y: 0
42741
+ },
42742
+ grabOffset: {
42743
+ x: 0,
42744
+ y: 0
42745
+ },
42746
+ pressedMs: 0,
42747
+ hovered: void 0,
42748
+ pointerOver: void 0,
42749
+ parent: void 0,
42750
+ restScale: void 0,
42751
+ heldPose: void 0,
42752
+ unmute: void 0,
42753
+ carried: [],
42754
+ path: [],
42755
+ traceIntent: void 0,
42756
+ traceClosed: false,
42757
+ lastPoint: void 0,
42758
+ canvas: void 0,
42759
+ offFrame: void 0,
42760
+ detach: void 0,
42761
+ tapListeners: [],
42762
+ keyListeners: [],
42763
+ pointerListeners: [],
42764
+ detachKeys: void 0,
42765
+ wake: void 0,
42766
+ cursor: void 0,
42767
+ controls: []
42768
+ };
42769
+ }
42770
+ /**
42771
+ * Counts the listeners and controls other code registered: `onTap`, `onPointer`, `onKey` and
42772
+ * `controls.add`, four lengths, nothing allocated.
42773
+ *
42774
+ * @param state - The input state.
42775
+ * @returns The number of registrations whose remover has not run.
42776
+ */
42777
+ function countListeners(state) {
42778
+ return state.tapListeners.length + state.pointerListeners.length + state.keyListeners.length + state.controls.length;
42779
+ }
42780
+ //#endregion
41469
42781
  //#region src/plugins/input/lifecycle.ts
41470
42782
  /**
41471
42783
  * @file input plugin — lifecycle functions: dependency resolution, the frame step registered in
41472
- * `onInit`, the pointer listeners opened in `onStart` and the teardown that closes exactly what
41473
- * was opened.
42784
+ * `onInit`, the pointer listeners opened in `onStart` (and, in a debug build, the count of the
42785
+ * listeners other code registered, pushed into the renderer's monitor) and the teardown that
42786
+ * closes exactly what was opened.
41474
42787
  */
41475
42788
  /**
41476
42789
  * Resolves the dependency APIs `time`, `flow`, `world` and `renderer` with `ctx.require`.
@@ -41514,12 +42827,17 @@ function initInput(ctx) {
41514
42827
  /**
41515
42828
  * Opens the pointer listeners on the canvas the renderer created and the one `keydown` listener
41516
42829
  * on `window`. Without a DOM the renderer has no canvas: nothing is attached, the plugin is
41517
- * inert, and `app.input.*` still answers the gate.
42830
+ * inert, and `app.input.*` still answers the gate. In an agent build the count of the listeners
42831
+ * and controls other code registered goes into the renderer monitor's leak counts as
42832
+ * `inputListeners`, pushed: the renderer cannot depend on input, which depends on it. The
42833
+ * renderer's `onStop` drops the registration.
41518
42834
  *
41519
42835
  * @param ctx - Kernel context of the input plugin.
41520
42836
  */
41521
42837
  function startInput(ctx) {
41522
- const canvas = ctx.require(rendererPlugin).host.canvas();
42838
+ const renderer = ctx.require(rendererPlugin);
42839
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) renderer.monitor.counts.register("inputListeners", () => countListeners(ctx.state));
42840
+ const canvas = renderer.host.canvas();
41523
42841
  ctx.state.canvas = canvas;
41524
42842
  if (canvas === void 0) return;
41525
42843
  attach(canvas, ctx);
@@ -41677,7 +42995,7 @@ function answerTrace(ctx, path) {
41677
42995
  }
41678
42996
  /**
41679
42997
  * Creates the input API: `app.input.tap`, `.press`, `.drag`, `.swipe`, `.trace`, `.onTap`,
41680
- * `.onPointer`, `.onKey`, `.pressKey`, `.cursor` and `.controls`.
42998
+ * `.onPointer`, `.onKey`, `.pressKey`, `.cursor`, `.listeners` and `.controls`.
41681
42999
  *
41682
43000
  * @param ctx - Kernel context of the input plugin.
41683
43001
  * @returns The plugin API.
@@ -41698,59 +43016,10 @@ function createInputApi(ctx) {
41698
43016
  shift: options?.shift ?? false
41699
43017
  }),
41700
43018
  cursor: () => inputCtx.state.cursor ?? inputCtx.config.cursor.idle,
43019
+ listeners: () => countListeners(inputCtx.state),
41701
43020
  controls: { add: (component) => addControl(inputCtx.state, component) }
41702
43021
  };
41703
43022
  }
41704
- //#endregion
41705
- //#region src/plugins/input/state.ts
41706
- /**
41707
- * Creates the initial input state: an empty sample queue, no active pointer and no gesture. Every
41708
- * duration starts at zero; the plugin never reads a device clock.
41709
- *
41710
- * @param _ctx - Minimal context.
41711
- * @param _ctx.config - Resolved plugin config.
41712
- * @returns A fresh state, owned by one app.
41713
- */
41714
- function createInputState(_ctx) {
41715
- return {
41716
- samples: [],
41717
- phase: "idle",
41718
- pointerId: void 0,
41719
- moved: false,
41720
- entity: void 0,
41721
- key: void 0,
41722
- start: {
41723
- x: 0,
41724
- y: 0
41725
- },
41726
- grabOffset: {
41727
- x: 0,
41728
- y: 0
41729
- },
41730
- pressedMs: 0,
41731
- hovered: void 0,
41732
- pointerOver: void 0,
41733
- parent: void 0,
41734
- restScale: void 0,
41735
- heldPose: void 0,
41736
- unmute: void 0,
41737
- carried: [],
41738
- path: [],
41739
- traceIntent: void 0,
41740
- traceClosed: false,
41741
- lastPoint: void 0,
41742
- canvas: void 0,
41743
- offFrame: void 0,
41744
- detach: void 0,
41745
- tapListeners: [],
41746
- keyListeners: [],
41747
- pointerListeners: [],
41748
- detachKeys: void 0,
41749
- wake: void 0,
41750
- cursor: void 0,
41751
- controls: []
41752
- };
41753
- }
41754
43023
  /**
41755
43024
  * Input plugin: `app.input.tap(target)`, `app.input.drag(from, to)`.
41756
43025
  *
@@ -54381,9 +55650,9 @@ function defineEffect(kind, options = {}) {
54381
55650
  * | renderer | poolLimit | 256 |
54382
55651
  * | renderer | unsupportedMessage | "This device cannot run the game." |
54383
55652
  * | renderer | loadPixi | () => import("pixi.js") |
54384
- * | renderer | debug | { nineSlice: false, verifyFrames: false, kept: true }; 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 |
55653
+ * | 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 |
54385
55654
  * | renderer | onDemand | false, every frame is drawn; true draws only a frame in which something changed |
54386
- * | renderer | monitor | { budgetMs: "rate", frames: 1024 }, the performance log of an agent build; shallow merge: a game that sets `monitor` gives both fields |
55655
+ * | renderer | monitor | { budgetMs: "rate", frames: 1024, stillFrames: 60, stillMpx: 0.25, churnPerSecond: 4, churnSeconds: 3, churnAfterTrimMs: 1000 }, the performance log of an agent build and the thresholds of its `problem` rules; shallow merge: a game that sets `monitor` gives `budgetMs` and `frames`, a threshold it leaves out keeps its default |
54387
55656
  * | renderer | keptMb | 64, MiB the kept pictures may hold: a filtered view that did not change is drawn from its picture, the least recently drawn goes first; 0 keeps none |
54388
55657
  * | renderer | warm | 1, probe draws a drawn frame that create the GPU pipelines while the game loads; 0 creates each in the first frame that needs it |
54389
55658
  * | input | tapSlopPx | 12 |
@@ -54543,4 +55812,4 @@ const screen = [
54543
55812
  uiPlugin
54544
55813
  ];
54545
55814
  //#endregion
54546
- export { parallel as $, refuseStart as $t, types_exports$12 as A, Blur as At, DebugTrigger as B, assetsPlugin as Bt, types_exports$5 as C, Traced as Ct, types_exports$9 as D, effectsPlugin as Dt, types_exports$8 as E, messageDuration as Et, types_exports$17 as F, Outline as Ft, textPlugin as G, rendererPlugin as Gt, popup as H, AnimPlayer as Ht, uiPlugin as I, defineFilter as It, defineAnimation as J, PERF_GAP_BUCKETS as Jt, platformPlugin as K, frameAt as Kt, resolve as L, debugPlugin as Lt, types_exports$14 as M, Displacement as Mt, types_exports$15 as N, Glow as Nt, types_exports$10 as O, Emitter as Ot, types_exports$16 as P, Noise as Pt, mark as Q, flowPlugin as Qt, defineMotion as R, audioPlugin as Rt, types_exports$4 as S, Traceable as St, types_exports$7 as T, messageArgument as Tt, defineTokens as U, Animation as Ut, LocalWrite as V, animPlugin as Vt, defineStyle as W, Frames as Wt, frames as X, DEMAND_REASONS as Xt, external as Y, PERF_LOG_VERSION as Yt, haptic as Z, worldPlugin as Zt, load as _, Pressable as _t, defineEffect as a, SaveUnreadableError as an, spawned as at, types_exports$2 as b, Tappable as bt, schedule as c, clockPlugin as cn, use as ct, tr as d, Draggable as dt, exit as en, play$1 as et, defineFeature as f, DropTarget as ft, defineBundles as g, PointerOver as gt, music as h, Pointer as ht, screen as i, modelPlugin as in, spawn as it, types_exports$13 as j, ColorMatrix as jt, types_exports$11 as k, Alpha as kt, projection as l, wait as lt, defineEmitter as m, Hovered as mt, createPlugin as n, to as nn, set as nt, guide as o, lifecyclePlugin as on, stagger as ot, flattenFeatures as p, Held as pt, HAPTIC_KINDS as q, PERF_CAUSES as qt, defineGame as r, type as rn, sfx as rt, hint as s, timePlugin as sn, tween as st, createApp as t, slot as tn, sequence as tt, defineScene as u, inputPlugin as ut, types_exports as v, Pressed as vt, types_exports$6 as w, i18nPlugin as wt, types_exports$3 as x, Touchable as xt, types_exports$1 as y, Swipeable as yt, Box as z, scenesPlugin as zt };
55815
+ export { parallel as $, DEMAND_REASONS as $t, types_exports$12 as A, Blur as At, DebugTrigger as B, assetsPlugin as Bt, types_exports$5 as C, Traced as Ct, types_exports$9 as D, effectsPlugin as Dt, types_exports$8 as E, messageDuration as Et, types_exports$17 as F, Outline as Ft, textPlugin as G, rendererPlugin as Gt, popup as H, AnimPlayer as Ht, uiPlugin as I, defineFilter as It, defineAnimation as J, PERF_CAUSES as Jt, platformPlugin as K, PASS_KINDS as Kt, resolve as L, debugPlugin as Lt, types_exports$14 as M, Displacement as Mt, types_exports$15 as N, Glow as Nt, types_exports$10 as O, Emitter as Ot, types_exports$16 as P, Noise as Pt, mark as Q, SHADER_WHATS as Qt, defineMotion as R, audioPlugin as Rt, types_exports$4 as S, Traceable as St, types_exports$7 as T, messageArgument as Tt, defineTokens as U, Animation as Ut, LocalWrite as V, animPlugin as Vt, defineStyle as W, Frames as Wt, frames as X, PERF_LOG_VERSION as Xt, external as Y, PERF_GAP_BUCKETS as Yt, haptic as Z, PROBLEM_RULES as Zt, load as _, Pressable as _t, defineEffect as a, to as an, spawned as at, types_exports$2 as b, Tappable as bt, schedule as c, SaveUnreadableError as cn, use as ct, tr as d, clockPlugin as dn, Draggable as dt, worldPlugin as en, play$1 as et, defineFeature as f, DropTarget as ft, defineBundles as g, PointerOver as gt, music as h, Pointer as ht, screen as i, slot as in, spawn as it, types_exports$13 as j, ColorMatrix as jt, types_exports$11 as k, Alpha as kt, projection as l, lifecyclePlugin as ln, wait as lt, defineEmitter as m, Hovered as mt, createPlugin as n, refuseStart as nn, set as nt, guide as o, type as on, stagger as ot, flattenFeatures as p, Held as pt, HAPTIC_KINDS as q, frameAt as qt, defineGame as r, exit as rn, sfx as rt, hint as s, modelPlugin as sn, tween as st, createApp as t, flowPlugin as tn, sequence as tt, defineScene as u, timePlugin as un, inputPlugin as ut, types_exports as v, Pressed as vt, types_exports$6 as w, i18nPlugin as wt, types_exports$3 as x, Touchable as xt, types_exports$1 as y, Swipeable as yt, Box as z, scenesPlugin as zt };