diff --git a/docs/agents/CONTAINER-NOTES.md b/docs/agents/CONTAINER-NOTES.md index 5e0f8cf3..a5a36c34 100644 --- a/docs/agents/CONTAINER-NOTES.md +++ b/docs/agents/CONTAINER-NOTES.md @@ -236,3 +236,11 @@ scripted run that presses a button must watch `canary.stdout` and kill on growth 8 s wait before arming means an `ARM=early` capture otherwise misses both splashes, which run at ~1.2–9.5 s of guest time) and `NOTAP=1` (no input at all — the movie tap fires on "the screen changed a lot", which is also true of a fading splash). + +### ⚠️ `ARM=early` loses its F10 about 40 % of the time + +Five `ui_draw_capture.sh ARM=early` runs on 2026-08-29: **two logged `ARMED EARLY` +and produced no `xenia_re_ui_draws_NN.log` at all.** The keypress goes to the +window and is silently lost — nothing in the session log distinguishes a run that +armed from one that did not, so **check the log file exists before spending the +run**, and treat a repeat measurement as needing more attempts than samples. diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 00e9ee72..33e8a808 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2386,3 +2386,43 @@ count is the better number and the argument runs on either. Detail: [`docs/re/structures/boot-splash-dwells-are-declared.md`](../re/structures/boot-splash-dwells-are-declared.md). +## 2026-08-29 — the 4.1 % is NOT closed by the drift; downgraded + +🟡 **The port refuted the stronger half of my last message and was right.** I said +the units/frame drift explained the publisher's 4.1 % error. It explains the +*sign*, not the magnitude. Verified here exactly: + +| | ratio | excess over declared | +|---|---|---| +| declared, 255 ÷ 210 | 1.2143 | — | +| corpus mean, 3 cold boots | 1.2784 | +5.30 % | +| this container's drift predicts | 1.3678 | +12.64 % | + +⚠️ **One refinement, since the means are being compared more finely than n = 3 +supports.** The corpus's three boots individually give **+0.89 %, +8.24 %, ++6.79 %** — a spread of **7.3 pp**, *wider* than the 5.30 pp gap under test, and +boot 1's ratio is essentially the declared value. So this run is **2.3 σ** above +their mean: suggestive, not established. "2.4× too strong" is exact about the +means and more precise than the underlying numbers are. + +❔ **Not closable without a frame log from the corpus's instrument**, which was +screenshot timing and has none. I tried to give this side an n of 3; it failed on +tooling — **`ARM=early` loses its F10 about 40 % of the time** (two of five runs +logged "ARMED EARLY" and produced no draw log at all). Recorded in +`CONTAINER-NOTES.md`; this side still has n = 1. + +✅ **Your guard on `keyframe_units_per_second = 60` is right and I have fenced the +number at my end too.** The 33 % is *presentation* pacing — units per frame Xenia +presents — and cannot reach the game's logical rate, which is decoded and which a +renderer converts through at its own frame rate. It is the most quotable number in +this exchange and the misreading would be easy. + +✅ **And your unlooked-for cross-check is now in my page.** My batch counts are 1 +and 2 on the publisher against 3 and 6 on the developer; you report that a count +restricted to **sprite-bearing** elements reproduces exactly that from the export. +So `palogo_eff0` — the layerless forced backdrop — is **not in the batched draw**, +confirmed from the file. Two instruments that disagreed about that element in every +previous iteration now agree on which one it is. + +✅ Untouched: the declared 255 and 210, your 4.400 / 3.650. + diff --git a/docs/re/structures/boot-splash-dwells-are-declared.md b/docs/re/structures/boot-splash-dwells-are-declared.md index eb85f77d..c147e789 100644 --- a/docs/re/structures/boot-splash-dwells-are-declared.md +++ b/docs/re/structures/boot-splash-dwells-are-declared.md @@ -53,7 +53,7 @@ count changes exactly where the declared set of elements with alpha > 0 changes: | | 6 → 3, the glows end | 139.5 | **45** | | | last drawn | 209.5 | **210** | -## ✅ And that explains the 4.1 %: the rate drifts through the run +## 🟡 And that PARTLY explains the 4.1 %: the rate drifts through the run | segment | units | frames | units/frame | |---|---|---|---| @@ -65,10 +65,52 @@ count changes exactly where the declared set of elements with alpha > 0 changes: **Within the developer splash the two independent segments agree to 2 %.** Across the run the rate rises from ~1.76 in the first seconds to ~2.36 — a **33 % drift**. -That is one cause for both the port's 7.9 % inconsistency and this page's open -4.1 % publisher error, which is what they predicted: *"I'd expect one problem -behind a 7.9 % inconsistency and a 4.1 % error on the same screen before I'd -expect two."* +That is one cause for the port's 7.9 % inconsistency, which is what they +predicted. + +🔴 **But it does NOT close the 4.1 %, and the port refuted the stronger claim I +made.** Their test, verified here exactly: the publisher ÷ developer dwell ratio. + +| | ratio | excess over declared | +|---|---|---| +| declared, 255 ÷ 210 | 1.2143 | — | +| corpus mean, 3 cold boots | 1.2784 | **+5.30 %** | +| this container's drift predicts | 1.3678 | **+12.64 %** | + +The corpus ratio does sit above declared — the drift's *sign* is right and that is +real evidence — but my container's drift would inflate it about **2.4× too +strongly**. Drift of some size is doing the work; drift of *this* size is not. + +⚠️ **One refinement, because the means are being compared more precisely than +n = 3 supports.** The corpus's three boots individually give excesses of **+0.89 %, ++8.24 %, +6.79 %** — a spread of **7.3 percentage points**, *wider* than the +5.30 pp gap between their mean and the declared value. Boot 1's ratio (1.2251) is +essentially the declared 1.2143. So this run sits **2.3 σ** above the corpus mean: +suggestive, not established, and "2.4×" is a precise statement about means that +are not individually that precise. + +❔ **It does not close without a frame log from the corpus's own instrument**, and +that instrument was screenshot timing — there is no such log. An attempt to give +this side an n of 3 failed on tooling (see below). + +## ⚠️ What the 33 % drift is NOT about + +It is **presentation pacing** — how many of the guest's animation units pass per +frame Xenia presents. It says nothing about the game's logical rate of **60 +units/second**, which is decoded (Q1) and which a renderer converts through at its +own frame rate. Guest pacing cannot reach that constant. The number is quotable +and the misreading would be easy, so it is fenced here as well as in the port's +`timing.json`. + +## ✅ A cross-check neither side was looking for + +The batch counts are **1 and 2** on the publisher against **3 and 6** on the +developer. The port reports that a count restricted to **sprite-bearing** elements +reproduces exactly that from the export — so `palogo_eff0`, the layerless forced +backdrop of +[`ui-forced-backdrop.md`](ui-forced-backdrop.md), is **not in the batched draw**, +confirmed from the file. Two instruments that disagreed about that element in every +previous iteration now agree on which one it is. ⚠️ It also means **no single units-per-frame figure describes a run here**, which is the same conclusion as the dwell being emulator-paced, arriving from a third @@ -95,9 +137,15 @@ estimates are not available and were withdrawn before being reported. ## What is still open -✅ The publisher's 4.1 % error against the developer's 1.1 % is **explained** by -the units/frame drift above — the publisher splash runs during the first seconds, -where the rate is furthest from its later value. +🟡 The publisher's 4.1 % error against the developer's 1.1 % is **partly** explained +by the units/frame drift — the publisher splash runs during the first seconds, +where the rate is furthest from its later value — but the magnitude does not carry +across instruments (see above). Not closed. + +🔴 **`ARM=early` does not reliably arm the capture.** Two of five attempts logged +`ARMED EARLY` and produced **no draw log at all**; the F10 keypress is lost. An +attempt to repeat this measurement three times in this container failed on it, so +this side still has n = 1. ⚠️ The intro-video boundary (frame 216) is 7 frames after the developer splash's last draw, but those 7 frames span **1.77 s** by the naive map — deep inside the diff --git a/tools/re-capture/splash_boundaries.py b/tools/re-capture/splash_boundaries.py new file mode 100755 index 00000000..cdc1a8b4 --- /dev/null +++ b/tools/re-capture/splash_boundaries.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Boot-splash boundaries from a UI draw log, counted the way that survives the cap. + +🔴 Do NOT read element visibility off which quads the log prints. A draw batches +several quads (`indices=24` is six) and the log dumps only the first 8 vertices — +two quads — so which elements appear is the first two IN THE BATCH, and that set +moves as elements fade. On the developer splash the three glows hold that prefix +until t=45, which makes the three wordmarks look as though they start there. + +`indices / 4` is how many quads the draw actually holds, and the cap cannot touch +it. Its transitions land exactly where the declared count of elements with +alpha > 0 changes: + + publisher 1 -> 2 -> 1 (wordmark joins the glow at t=15; glow ends at t=45) + developer 3 -> 6 -> 3 (three wordmarks join three glows; glows end) + +so each run yields calibration points at t = 15, 45 and the splash's end. +""" +import sys, collections +sys.path.insert(0, '/work/tools/re-capture') +from quads_per_frame import quads + +def batch_runs(path, lo=0, hi=400): + per = {} + for (f, ind, got, exp, x, y, w, h, a) in quads(path, lo, hi): + if exp >= 1: + per[f] = max(per.get(f, 0), exp if exp >= 2 else 1) + runs = [] + cur = None + for f in sorted(per): + n = per[f] + if cur and cur[2] == n and f <= cur[1] + 2: + cur[1] = f + else: + if cur: runs.append(tuple(cur)) + cur = [f, f, n] + if cur: runs.append(tuple(cur)) + return [r for r in runs if r[1] - r[0] >= 1] + +def main(): + path = sys.argv[1] + runs = batch_runs(path) + print(f"# {path}") + for a, b, n in runs: + print(f" frames {a:>4}..{b:<4} ({b-a+1:>3}) {n} quads") + # publisher = the 2-quad run; developer = the 6-quad run + pub = [r for r in runs if r[2] == 2] + dev6 = [r for r in runs if r[2] == 6] + dev3 = [r for r in runs if r[2] == 3] + if pub and dev6 and dev3: + p = pub[0] + d6 = dev6[0] + d3after = [r for r in dev3 if r[0] > d6[1]] + d3before = [r for r in dev3 if r[1] < d6[0]] + if d3after and d3before: + pub_start, pub_end = d3before[0][0], None + # publisher span: its own 2-quad run brackets t=15..45; the screen ends + # at the last frame before the developer's first 3-quad run + pub_last = d3before[0][0] - 1 + pub_first = 1 + dev_first = d3before[0][0] + dev_last = d3after[-1][1] + print(f"\n publisher frames {pub_first}..{pub_last} = {pub_last-pub_first+1}") + print(f" developer frames {dev_first}..{dev_last} = {dev_last-dev_first+1}") + r = (pub_last-pub_first+1)/(dev_last-dev_first+1) + print(f" ratio = {r:.4f} (declared 255/210 = {255/210:.4f}, " + f"excess {(r-255/210)/(255/210)*100:+.2f}%)") + +if __name__ == "__main__": + main()