re: the boot splash dwells are declared -- and wall clock is the wrong unit

The port asked for two wall-clock timestamps across the boot splashes. Measured,
and the measurement's own result is that timestamps are not the invariant.

The dwells are the bundles' own declared timelines: publisher t=0..255 = 4.250 s
at 60 units/s, developer t=0..210 = 3.500 s. The corpus's independent screenshot
timing over three cold boots gives 4.30/4.60/4.37 and 3.51/3.50/3.37 -- the
developer agreeing to 1.1%, two of its three runs to 0.3%.

A fresh no-input boot with a frame->wall-clock map puts the same two dwells at
5.10-5.61 s and 3.83-4.30 s, 15-20% longer than both the declared values and the
corpus's runs, on the same disc and the same declared timeline. So the
wall-clock dwell is an emulator-pacing artefact that varies run to run, and a
port authoring seconds is authoring one run's pacing.

Boundaries from the draw stream, read per quad: publisher glow frame 1, wordmark
6-119, three frames with NO sprite drawn, developer glows 123, wordmarks
140-209, intro video 216. The 3-frame gap replicates the earlier 4-frame
measurement within the +-1 both are quantised to.

New tool frame_clock.sh, and its limitation found by its own control: it
resolves to one BUFFER FLUSH, not one frame. The capture writes through a C++
ofstream, so tail sees the log in bursts -- 69 of 125 samples showed no advance
and the rest jumped 7-15 frames. Naive interpolation inside a burst made the
apparent rate swing between 0.0164 and 0.0316 s/frame, which is the flush and
not the guest. Frames 119 and 123 fall in one burst, so the inter-splash gap is
not separable by this clock at all. Everything is quoted as brackets and the
point estimates were withdrawn before being reported.

palogo_anima never appears in the log and is NOT reported as undrawn: the
developer bundle batches 7 elements into one draw and only the first two quads
are logged. That is the trap that produced the eff3 false negative, so it is
named rather than claimed.

Also records the port's correction: ptcopyright has 105 instants with alpha >= 1
(t=139..243) against 105.89 units of span; I had quoted the rounded span.

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:29:06 +00:00
parent 78328c5022
commit acaacc36e0
4 changed files with 176 additions and 0 deletions

View File

@@ -2341,3 +2341,48 @@ predate it.
Detail: [`docs/re/structures/ui-clock-freezes-at-settle.md`](../re/structures/ui-clock-freezes-at-settle.md). Detail: [`docs/re/structures/ui-clock-freezes-at-settle.md`](../re/structures/ui-clock-freezes-at-settle.md).
## 2026-08-29 — the splash dwells: author units, not seconds
You asked for two wall-clock timestamps. I measured them, and the measurement's
own result is that **timestamps are the wrong thing to author.**
**The dwells are declared on the disc:**
| splash | declared | at 60 units/s | corpus wall clock, 3 cold boots |
|---|---|---|---|
| publisher (entries 10, 13) | t = 0…**255** | **4.250 s** | 4.30 / 4.60 / 4.37 |
| developer (entries 11, 14) | t = 0…**210** | **3.500 s** | **3.51 / 3.50** / 3.37 |
The developer splash agrees to **1.1 %**, two of its three runs to 0.3 %.
🔴 **And my fresh boot is the argument against seconds.** With a frame→wall-clock
map it puts the same two dwells at **5.105.61 s** and 3.834.30 s — 1520 %
longer than the declared values *and* than the corpus's three runs, same disc,
same declared timeline. Three independent measurements of this container's rate
(13.1 fps, ~28 fps, this one) say the same thing. **A seconds figure is one run's
emulator pacing.** So: 255 and 210 units, and your instinct not to scale anything
by a ratio from one screen was right for the same reason.
**Boundaries from the draw stream** (frames, this boot): publisher wordmark 6119;
**3 frames with no sprite drawn**; developer glows 123, wordmarks 140209; intro
video 216. The 3-frame gap replicates the earlier 4-frame one within the ±1 both
are quantised to.
🔴 **What I could NOT measure, and why you should not read the fine numbers off
this run.** `frame_clock.sh` resolves to one **buffer flush**, not one frame: 69 of
125 samples showed no advance, the rest jumped 715 frames. Interpolating inside a
burst made the apparent rate swing 0.01640.0316 s/frame — the flush, not the
guest. **Frames 119 and 123 fall in the same burst, so the inter-splash gap is not
separable by this clock at all**; its ~9 units come from frame counting instead.
Everything above is quoted as brackets, and I withdrew the point estimates.
❔ Still open: the publisher's 4.1 % error against its declared 4.250 s, where the
developer's is 1.1 %. And the developer→intro gap is only bounded (5.706.21 s end
to end) because the movie loads inside a flush burst.
📌 Your quibble on `ptcopyright` is right: **105 instants** with alpha ≥ 1
(t=139…243), against 105.89 units of span. I quoted the rounded span; the instant
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).

View File

@@ -176,3 +176,4 @@ files, which is how the same ground got covered twice.
| [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) | Where a keyless primitive paints, when the file forces it | ✅ **decoded**, partly closing `ui-prm-primitives.md`'s standing blocker: **an element covering the screen and fully opaque at some instant cannot paint above anything visible then**, and where that set is *every* other element its position is **forced first**. Disc-wide **80** instances forced, 50 constrained but not forced, 0 unconstrained. ✅ **Two controls, both measured orders from the running game**: it reproduces `palogo_eff0.prm` = FIRST (opaque 211 instants, below 6/6) — which a **name**-based rule gets wrong, since it is named like an overlay — and permits `pteff00.prm` on top (opaque 2 instants, below 3/23), which is where it is measured. ✅ Answers the port's `build_12`/`build_15` blank-screen contradiction: `pgloading_eff00.prm` is forced first, 4/4. ✅ Explains 36 builds the corpus recorded as "one colour" with no cause — `pzeff00.prm` forced first 32/32, so **our own sort wiped them**. 🔴 The rule's limit was found by its own test failing: applied to `.t32` sprites it claimed 22 must sort first against their own keys (`pneff01` 0xd850 at #8/13, `pbfriendly` 0x9230 at #17/49) — a sprite's *element* alpha says nothing about its *texture*'s coverage, so it is now restricted to untextured primitives. ⚠️ Assumes straight alpha-over; blend mode is still ❔. ⚠️ A lower bound, not an ordering. ⚠️ No new oracle run — the controls are prior measurements | | [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) | Where a keyless primitive paints, when the file forces it | ✅ **decoded**, partly closing `ui-prm-primitives.md`'s standing blocker: **an element covering the screen and fully opaque at some instant cannot paint above anything visible then**, and where that set is *every* other element its position is **forced first**. Disc-wide **80** instances forced, 50 constrained but not forced, 0 unconstrained. ✅ **Two controls, both measured orders from the running game**: it reproduces `palogo_eff0.prm` = FIRST (opaque 211 instants, below 6/6) — which a **name**-based rule gets wrong, since it is named like an overlay — and permits `pteff00.prm` on top (opaque 2 instants, below 3/23), which is where it is measured. ✅ Answers the port's `build_12`/`build_15` blank-screen contradiction: `pgloading_eff00.prm` is forced first, 4/4. ✅ Explains 36 builds the corpus recorded as "one colour" with no cause — `pzeff00.prm` forced first 32/32, so **our own sort wiped them**. 🔴 The rule's limit was found by its own test failing: applied to `.t32` sprites it claimed 22 must sort first against their own keys (`pneff01` 0xd850 at #8/13, `pbfriendly` 0x9230 at #17/49) — a sprite's *element* alpha says nothing about its *texture*'s coverage, so it is now restricted to untextured primitives. ⚠️ Assumes straight alpha-over; blend mode is still ❔. ⚠️ A lower bound, not an ordering. ⚠️ No new oracle run — the controls are prior measurements |
| [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) *(span sensitivity)* | How much of the forced-backdrop rule rests on the timeline convention | ✅ **decoded**: the span is `0..=max keyframe time over every element`, and an element **holds** its final pose — decoded, not assumed ([`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md), [`ui-record-loop-length.md`](structures/ui-record-loop-length.md)). Sensitivity over the 130 keyless full-screen primitives with an opaque interval: using the header's declared **`+0x08`** instead changes **0** verdicts (interchangeable); using the primitive's **own** last keyframe changes **72**; counting elements **gone** after their last keyframe changes **72**. 🔴 So the hold decides **55 %** of verdicts — and dropping it is **refuted by a measured order**: `palogo_eff0.prm` is a single keyframe at t=0, so without the hold it is opaque for one instant, nothing else is up, and the rule calls it *free* against a game measured painting it first. ✅ The verdicts that matter are convention-independent — `pgloading_eff00.prm` is FIRST under all four, `pteff00.prm` FREE under all four. ⚠️ The port's **256 vs 211** was a **bundle mismatch, not a definitional one**: `palogo_eff0.prm` runs to t=255 on the publisher splash (entries 10/13) and t=210 on the developer (11/14) | | [`structures/ui-forced-backdrop.md`](structures/ui-forced-backdrop.md) *(span sensitivity)* | How much of the forced-backdrop rule rests on the timeline convention | ✅ **decoded**: the span is `0..=max keyframe time over every element`, and an element **holds** its final pose — decoded, not assumed ([`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md), [`ui-record-loop-length.md`](structures/ui-record-loop-length.md)). Sensitivity over the 130 keyless full-screen primitives with an opaque interval: using the header's declared **`+0x08`** instead changes **0** verdicts (interchangeable); using the primitive's **own** last keyframe changes **72**; counting elements **gone** after their last keyframe changes **72**. 🔴 So the hold decides **55 %** of verdicts — and dropping it is **refuted by a measured order**: `palogo_eff0.prm` is a single keyframe at t=0, so without the hold it is opaque for one instant, nothing else is up, and the rule calls it *free* against a game measured painting it first. ✅ The verdicts that matter are convention-independent — `pgloading_eff00.prm` is FIRST under all four, `pteff00.prm` FREE under all four. ⚠️ The port's **256 vs 211** was a **bundle mismatch, not a definitional one**: `palogo_eff0.prm` runs to t=255 on the publisher splash (entries 10/13) and t=210 on the developer (11/14) |
| [`structures/ui-clock-freezes-at-settle.md`](structures/ui-clock-freezes-at-settle.md) | The top-level clock stops at the settle point — observed in the running game | ✅ **measured**: `GP_TITLE` build 4 declares `t = 0…269`, about 120 presented frames at this run's pacing, and the dwell lasted **~1 100**. `ptcopyright` declares alpha≥1 for **106 units** (t=138…244) and is **drawn for 1 050 frames**; `ptlogo1` declares an exit at t=264 and is drawn for 1 095. Both vanish within three frames of the dwell ending. **The clock advances through the build-in, stops inside the settle window `[160,236]`, and holds; the exit ramp plays when the screen leaves, not on a timer** — [`ui-settle-time.md`](structures/ui-settle-time.md)'s decode observed from the other side. A nested record keeps looping on its own clock throughout. 🔴 **This closes the 114-vs-120 gap, and it was my arithmetic**: 2.231 units/frame was regressed over *build-in* events (the only stretch the top-level clock advances) and applied to a period measured during the freeze — two different clocks. The declared **120** was never in doubt from the calibration-free dark-fraction test. ✅ The 51.158-frame period is now confirmed by a **second independent estimator** (autocorrelation, lag 51 with harmonics at 102/154) — ⚠️ whose first version **failed its control**, returning 48, because it indexed by sample position where the log's frame numbers have gaps. ❔ The **sweeps'** period stays unmeasured: the same validated estimator disagrees between two dwells of one screen (515 vs 452 frames). 🔴 **Blocker: a single Ⓐ on the title faults the guest** — 3 attempts, 2 register dumps of 223 MB and 519 MB, against 3 no-input runs that all completed; bounds menu-side dynamic RE here, and any scripted button press needs a `canary.stdout` size guard | | [`structures/ui-clock-freezes-at-settle.md`](structures/ui-clock-freezes-at-settle.md) | The top-level clock stops at the settle point — observed in the running game | ✅ **measured**: `GP_TITLE` build 4 declares `t = 0…269`, about 120 presented frames at this run's pacing, and the dwell lasted **~1 100**. `ptcopyright` declares alpha≥1 for **106 units** (t=138…244) and is **drawn for 1 050 frames**; `ptlogo1` declares an exit at t=264 and is drawn for 1 095. Both vanish within three frames of the dwell ending. **The clock advances through the build-in, stops inside the settle window `[160,236]`, and holds; the exit ramp plays when the screen leaves, not on a timer** — [`ui-settle-time.md`](structures/ui-settle-time.md)'s decode observed from the other side. A nested record keeps looping on its own clock throughout. 🔴 **This closes the 114-vs-120 gap, and it was my arithmetic**: 2.231 units/frame was regressed over *build-in* events (the only stretch the top-level clock advances) and applied to a period measured during the freeze — two different clocks. The declared **120** was never in doubt from the calibration-free dark-fraction test. ✅ The 51.158-frame period is now confirmed by a **second independent estimator** (autocorrelation, lag 51 with harmonics at 102/154) — ⚠️ whose first version **failed its control**, returning 48, because it indexed by sample position where the log's frame numbers have gaps. ❔ The **sweeps'** period stays unmeasured: the same validated estimator disagrees between two dwells of one screen (515 vs 452 frames). 🔴 **Blocker: a single Ⓐ on the title faults the guest** — 3 attempts, 2 register dumps of 223 MB and 519 MB, against 3 no-input runs that all completed; bounds menu-side dynamic RE here, and any scripted button press needs a `canary.stdout` size guard |
| [`structures/boot-splash-dwells-are-declared.md`](structures/boot-splash-dwells-are-declared.md) | How long each boot splash is shown | ✅ **decoded**: the dwells are the bundles' own declared timelines — publisher **t=0…255 = 4.250 s**, developer **t=0…210 = 3.500 s** at 60 units/s. The corpus's independent screenshot timing over 3 cold boots gives 4.30/4.60/4.37 and **3.51/3.50/3.37** — the developer agreeing to **1.1 %**, two of its three runs to 0.3 %. 🔴 **Wall clock is the wrong unit to author**: a fresh no-input boot measured the same two dwells at **5.105.61 s** and 3.834.30 s, 1520 % longer than both the declared values and the corpus's runs, on the same disc — so a seconds figure is one run's emulator pacing. Boundaries from the draw stream: publisher wordmark frames 6119, **3 frames with no sprite drawn**, developer glows 123, wordmarks 140209, intro video 216. 🔴 **The frame→wall-clock instrument resolves to one BUFFER FLUSH, not one frame** — 69 of 125 samples showed no advance and the rest jumped 715 frames, making the apparent rate swing 0.01640.0316 s/frame; frames 119 and 123 fall in one burst, so the inter-splash gap is **not separable** by it. Quoted as brackets; sub-flush estimates withdrawn before reporting. ⚠️ `palogo_anima` never appears — almost certainly the 8-vertex cap (7 elements batched, 2 logged), the same trap as the `eff3` false negative, so it is named not reported. ❔ the publisher's 4.1 % error vs the developer's 1.1 % is unexplained |

View File

@@ -0,0 +1,87 @@
# The boot splash dwells are declared on the disc — wall clock is the wrong unit
**Classification: decoded**, with an independent wall-clock check. The port asked
for two timestamps; the right answer is that timestamps are not the invariant.
## The answer
| splash | bundle | declared timeline | at 60 units/s | corpus wall clock, 3 cold boots | error |
|---|---|---|---|---|---|
| publisher | `GP_TITLE` entries 10, 13 | t = 0 … **255** | **4.250 s** | 4.30 / 4.60 / 4.37 | 4.1 % |
| developer | entries 11, 14 | t = 0 … **210** | **3.500 s** | 3.51 / 3.50 / 3.37 | 1.1 % |
The developer splash is the sharp one: **3.500 s declared against 3.51 and 3.50
measured** on two of three boots. **Author the units.**
## Why not seconds — this run is the argument
A fresh no-input boot with a frame→wall-clock map (below) puts the same two dwells
at **5.10 5.61 s** and **3.83 4.30 s****1520 % longer** than both the
declared values and the corpus's three runs, on the same disc and the same
declared timeline.
**So the wall-clock dwell is an emulator-pacing artefact that varies run to run.**
Three separate measurements of this container's rate — 13.1 fps, ~28 fps, and this
one — say the same thing from another direction. A port that authors seconds is
authoring one run's pacing.
## The boundaries, from the draw stream
Read with the per-quad parser, this boot:
| event | frame |
|---|---|
| `palogo_sqex_eff` — the publisher's glow | 1 |
| `palogo_sqex` — the wordmark | **6** 119 |
| no sprite drawn at all | 120 122 |
| `gamearts_eff` + `seta_eff` — the developer's glows | **123** 139 |
| `palogo_gamearts` + `palogo_seta` — the wordmarks | 140 209 |
| intro video (a 640×360 pair per frame) | **216** 563 |
The 3-frame black gap replicates the 4-frame gap measured in the earlier boot,
within the ±1 frame both are quantised to — consistent with the ~9 units in
[`boot-splash-gap-measured.md`](boot-splash-gap-measured.md).
⚠️ **`palogo_anima` never appears**, and that is almost certainly the 8-vertex cap
rather than a fact about the game: the developer bundle has 7 elements batched into
one draw, and only the first two quads are logged. This is the trap that produced
the `eff3` false negative, so it is named rather than reported.
## 🔴 The instrument's resolution is one buffer flush, not one frame
`tools/re-capture/frame_clock.sh` polls the growing draw log for its last frame
number. The capture writes through a C++ `ofstream`, so `tail` sees the file in
flush-sized bursts:
* **69 of 125 samples showed no advance at all**; the rest jumped **715 frames**
at once.
* Interpolating a frame's time *inside* a burst invents precision. Done naively it
made the apparent rate swing between **0.0164 and 0.0316 s/frame** — 61 fps to
31 fps — which is the flush, not the guest.
* **Frames 119 and 123 fall in the same burst**, so the black gap between the
splashes is **not separable by this instrument at all**. Its duration here comes
from frame counting, not from this clock.
So the table above is quoted as **brackets**: a frame's true time lies between the
last sample that had not reached it and the first that had. Sub-flush point
estimates are not available and were withdrawn before being reported.
## What is still open
❔ The publisher's 4.1 % error against its declared 4.250 s is larger than the
developer's 1.1 %, across only three corpus boots plus this one. Whether that is
run-to-run pacing or something the publisher splash does differently is not
settled.
⚠️ 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
flush artefact, and the movie is loading there. **The developer→intro gap is not
measured**, only bounded at 5.70 6.21 s end to end.
## Reproducing
```bash
GRACE=1 NOTAP=1 FRAMES=9000 ARM=early tools/re-capture/ui_draw_capture.sh 200 /tmp/cap &
tools/re-capture/frame_clock.sh /tmp/cap/xenia_re_ui_draws_01.log /tmp/cap/frameclock.tsv \
150 0.2 /tmp/cap/canary.stdout 400
```

43
tools/re-capture/frame_clock.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Sample (wall clock, last captured frame) while a UI draw capture runs.
#
# The `xenia_re_ui_draws_NN.log` carries frame NUMBERS and no timestamps, and
# Canary logs no fps, so a draw capture can say "4 frames of black" and not how
# long that is. Two runs measured 13.1 and ~28 presented fps, so a nominal rate
# cannot be assumed either.
#
# This polls the growing log and writes `epoch frame` pairs, which invert to give
# any frame's wall-clock time.
#
# 🔴 RESOLUTION IS ONE BUFFER FLUSH, NOT ONE FRAME. The capture writes through a
# C++ ofstream, so `tail` sees the log in flush-sized bursts: measured, 69 of 125
# samples showed NO advance and the rest jumped 7-15 frames at once. Interpolating
# a frame's time *inside* a burst invents precision -- it made the apparent rate
# swing between 0.016 and 0.032 s/frame, which is the flush, not the guest.
#
# Use BRACKETS: a frame's true time lies between the last sample that had not
# reached it and the first that had. Two frames inside one burst (the 3-frame
# black gap between the boot splashes) are not separable at all.
#
# It also GUARDS canary.stdout: a guest fault dumps registers without bound
# (223 MB and 519 MB observed on a filesystem at 91 %), so the run is killed if
# stdout passes the cap.
#
# frame_clock.sh <draws.log> <out.tsv> [seconds] [interval] [stdout-file] [cap-MB]
set -u
LOG="$1"; OUT="$2"; DUR="${3:-150}"; IVAL="${4:-0.25}"; SOUT="${5:-}"; CAP="${6:-400}"
: > "$OUT"
end=$(( $(date +%s) + DUR ))
while [ "$(date +%s)" -lt "$end" ]; do
f=$(tail -c 400000 "$LOG" 2>/dev/null | grep -oE '^--- frame [0-9]+' | tail -1 | awk '{print $3}')
[ -n "$f" ] && printf '%s\t%s\n' "$(date +%s.%N)" "$f" >> "$OUT"
if [ -n "$SOUT" ] && [ -f "$SOUT" ]; then
mb=$(( $(stat -c %s "$SOUT") / 1048576 ))
if [ "$mb" -gt "$CAP" ]; then
echo "STDOUT ${mb}MB > ${CAP}MB cap — guest is dumping registers, killing" >&2
pkill -9 -x xenia_canary; exit 3
fi
fi
sleep "$IVAL"
done
echo "sampled $(wc -l < "$OUT") points"