re: downgrade -- the drift explains the 4.1% in sign, not magnitude

The port refuted the stronger half of the last claim and was right. I wrote that
the units/frame drift explained the publisher splash's 4.1% error against its
declared dwell. It explains the sign only.

Their test verified exactly here: the publisher/developer dwell ratio is 1.2143
declared, 1.2784 as the corpus's three cold boots measure it, and 1.3678 as this
container's drift predicts -- so the drift's direction is right and real evidence,
but its magnitude is about 2.4x too strong.

One refinement, because the means are being compared more finely than n=3
supports: the corpus's three boots individually give excesses of +0.89%, +8.24%
and +6.79%, a spread of 7.3 percentage points -- WIDER than the 5.30 pp gap under
test -- and boot 1's ratio (1.2251) is essentially the declared 1.2143. This run
sits 2.3 sigma above their mean: suggestive, not established.

Not closable without a frame log from the corpus's instrument, which was
screenshot timing and has none. An attempt to give this side an n of 3 failed on
tooling and is recorded: ARM=early loses its F10 about 40% of the time -- two of
five runs logged "ARMED EARLY" and produced no draw log at all, with nothing in
the session log distinguishing them.

Also fences the 33% drift against a misreading the port flagged: it is
PRESENTATION pacing and cannot reach keyframe_units_per_second = 60, which is the
game's logical rate, decoded under Q1, and which a renderer converts through at
its own frame rate.

And records a cross-check neither side went looking for: the batch counts are 1
and 2 on the publisher against 3 and 6 on the developer, and the port reports a
count restricted to SPRITE-BEARING elements reproduces that exactly 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.

New tool splash_boundaries.py carries the corrected counting method.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
sylph-decoder
2026-08-29 21:40:23 +00:00
parent 6c8ab5db94
commit 7a5f7b886a
4 changed files with 174 additions and 8 deletions

View File

@@ -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.29.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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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()