diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 455933e0..00e9ee72 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2341,3 +2341,48 @@ predate it. 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.10–5.61 s** and 3.83–4.30 s β€” 15–20 % +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 6–119; +**3 frames with no sprite drawn**; developer glows 123, wordmarks 140–209; 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 7–15 frames. Interpolating inside a +burst made the apparent rate swing 0.0164–0.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.70–6.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). + diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 6add0ae8..854f0c0b 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -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) *(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/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.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 β€” so a seconds figure is one run's emulator pacing. Boundaries from the draw stream: publisher wordmark frames 6–119, **3 frames with no sprite drawn**, developer glows 123, wordmarks 140–209, 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 7–15 frames, making the apparent rate swing 0.0164–0.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 | diff --git a/docs/re/structures/boot-splash-dwells-are-declared.md b/docs/re/structures/boot-splash-dwells-are-declared.md new file mode 100644 index 00000000..51fafd84 --- /dev/null +++ b/docs/re/structures/boot-splash-dwells-are-declared.md @@ -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** β€” **15–20 % 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 **7–15 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 +``` diff --git a/tools/re-capture/frame_clock.sh b/tools/re-capture/frame_clock.sh new file mode 100755 index 00000000..b19d30f5 --- /dev/null +++ b/tools/re-capture/frame_clock.sh @@ -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 [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"