#!/usr/bin/env python3 """Is "the game presents at 27.6 fps" a property of the GAME or of this container? ui-keyframe-time-unit.md reads 27.6-28.8 fps off frame counts over wall-clock windows here, and the 8.5 % splash-dwell excess is the same number from the other side. The audio clock is bounded at 0.985 +- 0.015 of real time (container-audio-clock.txt), so a uniform slowdown is refuted -- but that bounds the AUDIO path, and these are FRAME numbers. 🔴 sylpheed-port's proposed instrument -- frames presented per audio sample consumed, against a quartz reference -- IS NOT AVAILABLE HERE. This container has no /dev/snd, no ALSA and no PulseAudio, so SDL's only backend is `dummy` and every clock in reach is a software clock. There is no hardware rate to measure against. So use a different axis. A rate set by the GAME does not move with host load; a rate set by STARVATION does. sylpheed-port demonstrated exactly this mechanism on their box (720p +6.7 % vs 432p -0.5 %); this asks whether it operates on mine. MEASUREMENT: presented frames = frames that DIFFER from their predecessor in an x11grab capture. The settled title free-runs two sweep leaves and a pulsing plate, so every presented frame differs -- which is what makes counting them reliable here and would not hold on a still screen. 🔴 CONTROLS, both required: * capture at 60 AND at 30 fps. If both are above the guest's rate they must agree; if the 60 Hz figure is higher, the 30 Hz capture was undersampling and neither number is a guest rate. * the settled-title gate is CALLED (wait_plate_pulse.py), not reimplemented -- a previous run sampled across the build-in because I rebuilt the gate from memory and dropped its twelve-sample hold. present_rate_vs_load.py OUTDIR [seconds] """ import os, subprocess, sys, time import numpy as np OUT = sys.argv[1] SECS = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0 W, H = 1280, 720 os.makedirs(OUT, exist_ok=True) def count_distinct(rate, seconds, thresh=24): """Frames differing from their predecessor, captured at `rate` fps.""" p = subprocess.Popen( ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", "-video_size", f"{W}x{H}", "-i", ":98", "-r", str(rate), "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE, bufsize=W * H * 3 * 4) n = W * H * 3 prev = None got = distinct = 0 t0 = time.time() while time.time() - t0 < seconds: b = p.stdout.read(n) if len(b) < n: break a = np.frombuffer(b, np.uint8).reshape(H, W, 3) got += 1 if prev is not None and int((np.abs(a.astype(np.int16) - prev).max(axis=2) > thresh).sum()) > 200: distinct += 1 prev = a.astype(np.int16) el = time.time() - t0 p.kill() return got, distinct, el def load_on(workers=4): return [subprocess.Popen([sys.executable, "-c", "\nwhile True: pass\n"]) for _ in range(workers)] print("── measuring the presented-frame rate on the settled title ──", flush=True) rows = [] for rate in (60, 30): got, dis, el = count_distinct(rate, SECS) fps = dis / el rows.append((f"capture {rate} fps, idle", got, dis, el, fps)) print(f" capture {rate:2d} fps: {got} grabbed, {dis} distinct in {el:.1f}s " f"-> {fps:.2f} presented fps", flush=True) print("── now under artificial CPU load ──", flush=True) procs = load_on(4) time.sleep(3) try: got, dis, el = count_distinct(60, SECS) fps = dis / el rows.append(("capture 60 fps, +4 busy cores", got, dis, el, fps)) print(f" capture 60 fps + load: {got} grabbed, {dis} distinct in {el:.1f}s " f"-> {fps:.2f} presented fps", flush=True) finally: for q in procs: q.kill() with open(f"{OUT}/present-rate.tsv", "w") as f: f.write("# condition\tgrabbed\tdistinct\tseconds\tpresented_fps\n") for r in rows: f.write(f"{r[0]}\t{r[1]}\t{r[2]}\t{r[3]:.2f}\t{r[4]:.3f}\n") print("\n================ RESULT ================") for r in rows: print(f" {r[0]:30} {r[4]:6.2f} fps") if len(rows) == 3: a, b, c = rows[0][4], rows[1][4], rows[2][4] print(f"\n CONTROL 60 vs 30 Hz capture: {a:.2f} vs {b:.2f} " f"({'agree' if abs(a-b) < 0.15*max(a,b) else 'DISAGREE — 30 Hz undersampled'})") print(f" idle {a:.2f} -> loaded {c:.2f} = {100*(c-a)/a:+.1f} %") print(" a rate set by the GAME does not move with host load;" " a rate set by STARVATION does") print("PRESENT RATE RUN DONE", flush=True)