entangled-worlds-orbiters-shared 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled-worlds-orbiters-shared",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -158,6 +158,10 @@ export function createViewportCompositor({
158
158
  let grid = null;
159
159
  let raf = null;
160
160
  let disposed = false;
161
+ // A load meter's watch on the realm's frames (`setFrameProbe`): null when nobody is watching.
162
+ let frameProbe = null;
163
+ let frameProbeAt = 0;
164
+ let probePixel = null;
161
165
  let lastCanvasW = 0;
162
166
  let lastCanvasH = 0;
163
167
  let viewportH = 0; // per-cell cull band (CSS px), refreshed with the rects
@@ -514,7 +518,60 @@ export function createViewportCompositor({
514
518
  // at half the realm's rate. Skipping the whole frame instead leaves every cell showing the last
515
519
  // frame that was drawn — together, which is the only way a shared canvas can be consistent.
516
520
  if (performanceOn && !frameBudget.shouldRender(nowMs)) return;
517
- renderOnce();
521
+ if (!frameProbe) {
522
+ renderOnce();
523
+ return;
524
+ }
525
+ const probe = frameProbe;
526
+ probe.onFrame?.(nowMs);
527
+ const timed = nowMs - frameProbeAt >= probe.everyMs;
528
+ if (!timed) {
529
+ renderOnce();
530
+ return;
531
+ }
532
+ frameProbeAt = nowMs;
533
+ // Every cell's draw counted together: one composited frame is several renders.
534
+ const info = renderer.info;
535
+ const autoReset = info.autoReset;
536
+ info.autoReset = false;
537
+ info.reset();
538
+ try {
539
+ const started = performance.now();
540
+ renderOnce();
541
+ // A lost context draws nothing and answers nothing: no reading, not a good one.
542
+ if (waitForDraw()) {
543
+ const drawn = performance.now();
544
+ // Asked again with nothing left to draw: what the asking itself costs, taken off the reading.
545
+ waitForDraw();
546
+ probe.onDraw(Math.max(0, drawn - started - (performance.now() - drawn)), info.render.calls, info.render.triangles);
547
+ }
548
+ } finally {
549
+ // Put back as it was, even when a voice throws mid-frame.
550
+ info.autoReset = autoReset;
551
+ }
552
+ }
553
+
554
+ /** Block until the graphics chip has drawn what was sent: reading a pixel back is the one wait every
555
+ * browser honours. False when there is no context to ask. */
556
+ function waitForDraw() {
557
+ const gl = renderer.getContext?.();
558
+ if (!gl || gl.isContextLost()) return false;
559
+ probePixel ??= new Uint8Array(4);
560
+ gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, probePixel);
561
+ return true;
562
+ }
563
+
564
+ /**
565
+ * Watch the realm's frames, for a load meter: `onFrame(nowMs)` for every frame drawn, and one frame
566
+ * every `everyMs` timed to the end of its drawing (`onDraw(workMs, calls, triangles)`, every cell
567
+ * together). Only the timed frame waits for the graphics chip; every other frame is untouched.
568
+ * `null` stops it.
569
+ *
570
+ * @param {{ everyMs: number, onFrame?: (nowMs: number) => void, onDraw: (workMs: number, calls: number, triangles: number) => void } | null} probe
571
+ */
572
+ function setFrameProbe(probe) {
573
+ frameProbe = probe ?? null;
574
+ frameProbeAt = 0;
518
575
  }
519
576
 
520
577
  /**
@@ -572,6 +629,7 @@ export function createViewportCompositor({
572
629
  removeVoice,
573
630
  renderOnce,
574
631
  setPerformanceMode,
632
+ setFrameProbe,
575
633
  dispose,
576
634
  };
577
635
  // Dev handle: lets tooling force a composited frame when rAF is throttled (e.g. a hidden/background