port: P7 -- the new-game intro plays, and the two screens it skips are named out loud

S00A has been exported since P4; what P7 needed was something to play it and a
defined place to land. Both are here, and the interesting part is the gap.

The real chain is NEW GAME -> DIFFICULTY -> SELECT DATA -> (A) on a save slot ->
~4.5 s -> S00A. DIFFICULTY and SELECT DATA are MEASURED destinations that are not
GP_TITLE builds, so no screen file exists to go to. The port jumps from NEW GAME
to the one thing in that chain it has -- and the whole design is about not
letting that read as a sequence:

  * MenuFlow.accept returns a new kind, `video`, rather than folding this into
    `blocked`, because the caller has to announce the skip and a distinct kind is
    what forces it to;
  * the runtime prints the skipped screens by name on every run;
  * flow.json carries `skipped_chain` as DATA, so what is missing lives beside
    the decision instead of inside a GDScript string.

After the movie the port returns to the title. Authored, and it has to be: the
game goes into mission 1 and gameplay is out of scope. The ~4.5 s before the
movie is left EMPTY on purpose -- GP_TITLE does carry a loading screen and 4.5 s
is about the right shape for one, which is exactly why that belongs in BLOCKED.md
and not in flow.json.

`--script`'s 20 s per-step timeout would have killed every movie run at step 1.
Raising the constant would have been wrong the other way: a movie stuck at frame
0 would then hang the job, and a job that waits is worse than one that fails. The
test is now LIVENESS -- while get_stream_position() advances the deadline moves
with it, and a stalled movie still trips the same 20 s.

Found while looking: GP_TITLE's four unnamed builds (entries 0, 1, 12, 15) are
LOADING screens -- every element in all four is pgloading_*, and LOADING is one of
the three names the decoder read out of the title part's state function. NOT
renamed here: which member of each pair is which locale is an inference, and a
name stops being questioned once written. Handed over.

One of them is a second casualty of the rest.t problem, and a worse one:
pgloading_eff00.prm rests OPAQUE BLACK at t=38, so anything drawing that screen
at its declared rest paints a black rectangle over all of it. The title's case
only dimmed a frame.

REFUTATION, attempted and SURVIVED: HANDOFF says "exactly the six screen builds
carry the black .prm quad while the six overlays do not". Counting bundles with a
full-screen black primitive gives 8 and 4 -- build_12/15 carry one too. But
theirs runs black -> held -> clear where the transition quad runs black -> clear
-> black, so read strictly as "the quad whose group is the transition" the claim
holds. Recorded anyway: there are two kinds, and the naive census over-counts.

Gate: NEW GAME -> S00A plays 93.75 s against a declared 93.9 -> title, with
98.453 s recorded off the Master bus. What that does NOT show is that S00A's own
audio is in the mix -- bed and movie were not separated in this run, and the
write-up says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
This commit is contained in:
Sylpheed port agent
2026-08-29 13:12:41 +00:00
parent 1f1fe647f2
commit 7a6a027fc8
5 changed files with 255 additions and 1 deletions

View File

@@ -90,6 +90,10 @@ git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconc
| P3 — the plate's PULSE | **does the plate's focus record loop, and with what period?** | Q2 | ❔ **open, and the port's earlier reading of it was wrong.** The port had looked for the pulse in `ptbtn00`'s own group; `5b0a6e6` identifies it as the plate's **focus record** `ptbtn00f` — a glow ramping alpha `0x00``0x50` and back, t=6…105. Measured on the running game at 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. 🟡 **The port has not taken it.** Looping that record needs a period, and its group is 105 timed units plus the **authored** 24-unit exit ramp = 129 units = 2.15 s — composing an authored constant with a loop assumption to land on a measured number is tuning, not measuring. Separately: the port draws no focus record on `press_start` at all, because the screen has no `buttons` and nothing is focused, so *whether the game always draws it* is its own question. |
| ~~P5 focus ring — implementation~~ | ~~the ring's spin period~~ | Q1 | ✅ **implemented 2026-08-29.** `ScreenView.spin_period_units` drives it: one turn per the element's own declared `t`, looping, from the screen clock. The period comes off the **disc**; what the RE agent supplied is that the turn repeats rather than stopping. Verified on the port's own render — the ring is bit-identical one period apart across the whole frame, differs by 3.6/255 inside its box at quarter-period steps, and conserves box luminance to **0.027 %** over eight phases, which is the same observable the RE agent used to separate rotation from a pulse. 🟡 **Direction is not measured** — the port turns 0°→+360°, which is the sign the disc declares, but the RE agent's angle estimators failed their controls and no signed angle was ever taken. 🟡 **Phase across a focus change is not measured** either: the port drives the ring off the screen clock, so it does not reset when focus moves. Settled by two frames straddling a focus change. |
| P7 / naming — the four unnamed builds | **which locale and variant is each of `GP_TITLE` entries 0, 1, 12, 15?** | — | 🟢 **found by the port, not blocking, and handed over.** All four are **loading screens**: every element in all four is named `pgloading_*` (`pgloading_processing.png`, `pgloading_circle1`, `pgloading_delta`, `pgloading_ring`), and `LOADING` is one of the three screen names the Decoder read out of `sub_821C6458`. They export today as `build_00`, `build_01`, `build_12`, `build_15`. Two variants: 0/1 carry 7 elements, 12/15 carry 10 (adding `pgloading_eff00`, `pgloading_loop5`, `pgloading_baseeff`). The archive's own pairing — adjacent for 2/3, `+3` for 4…9 and 10/13, 11/14 — suggests 0 is the twin of 1 and 12 the twin of 15, but **which member is which locale is an inference and the port has not named them on it**. Naming is cheap for the Decoder and a guess for the port. |
| P7 — what fills the 4.5 s before `S00A` | **is the LOADING screen what appears between the save slot and the new-game movie?** | Q4 + Q9 | ❔ **not observed, and the port has not assumed it.** Q9 measures `S00A.wmv` starting ~4.5 s after Ⓐ on the save slot. The run that would have shown what is on screen for those 4.5 s hit the documented `sub_823070B0` cache crash after `SELECT DATA`. `GP_TITLE` carries a loading screen (row above) and 4.5 s is about the right shape for one, and that is **exactly why it is written here and not in `flow.json`**. Settled by one run that reaches the movie without crashing. |
| P3 — a second `rest.t` casualty | **the loading screen's fade quad rests OPAQUE BLACK** | — | 🔴 **noted, not fixed.** `pgloading_eff00.prm` on entries 12/15 is a 1280×720 black quad whose group is `0xff000000` at t=38, `0xff000000` at t=48, then `0x00000000` on the untimed final — black, held, *then* clear. Its `rest.t` is **38**, where it is fully opaque. So a port that draws this screen at its `rest` draws **a black rectangle over the whole loading screen**. This is the same `settle_time()`/`rest.t` problem as the row below, in a form where it hides the entire screen rather than dimming it — and it will bite whoever first draws a loading screen. |
## Answered since this file was last written — no longer blocking
Q1 (keyframe time unit — linear ramp, 2 units per rendered frame, 1 unit = 1/60 s

View File

@@ -1924,3 +1924,174 @@ printed, and the only symptom was a frame slightly darker than the previous
run's. A gate artifact that silently drifts is the failure mode this project
keeps meeting — and it was caught only because there was a previous capture to
compare against.
## P7 — the new-game intro, 2026-08-29
`S00A.wmv` has been in `export/video/` since P4 (MISSION §6 put both movies in
scope at once). What P7 needed was for something to *play* it and for the run to
end somewhere defined.
### The port skips two measured screens, and says so on screen
The real chain is **`NEW GAME``DIFFICULTY``SELECT DATA` → Ⓐ on a save slot
→ ~4.5 s → `S00A`** (HANDOFF Q4 measured the screens, Q9 decoded the movie and
then measured its onset off the running game at 0.961.000 with a strictly
monotone playhead over 25 consecutive 0.5 s samples).
`DIFFICULTY` and `SELECT DATA` are measured destinations that are **not
`GP_TITLE` builds**, so no screen file exists to go to. The port therefore jumps
from `NEW GAME` to the one thing in that chain it has.
That is a gap, not a sequence, and the whole design here is about not letting it
read as one:
* `MenuFlow.accept` returns a **new kind**, `video`, rather than folding this
into `blocked`. The caller has to announce the skip, and a distinct kind is
what forces it to.
* The runtime prints it every time:
`(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
Neither screen is in this export.`
* `authored/flow.json` carries `skipped_chain` as **data**, so the names of what
is missing live beside the decision rather than inside a GDScript string.
A port that quietly jumped from `NEW GAME` to the intro would be showing a
sequence the game does not have, with nothing on screen saying so. That is the
exact failure this project keeps meeting from the other direction.
### What happens after the movie is authored, and had to be
The game goes into **mission 1**. Gameplay is out of scope (PORT-MISSION §7), so
"returns to a defined state" is a decision, and P7's gate says as much. The port
returns to the **title**: the boot's own end state, so a run that finishes the
intro lands somewhere a player can start again from. Nothing measured says the
game does this, and `after_video.kind` is `"authored"`.
### The 4.5 s gap is left empty on purpose
Q9 measures the movie starting ~4.5 s after Ⓐ on the save slot. What is on screen
for those 4.5 s was never observed — the run that would have shown it hit the
documented `sub_823070B0` cache crash after `SELECT DATA`.
`GP_TITLE` **does** carry a loading screen (below), and 4.5 s is about the right
shape for one. That is precisely why it is in `BLOCKED.md` and not in
`flow.json`: a plausible filler that nobody watched is the kind of thing that is
indistinguishable from a measurement a month later.
### A script timeout that would have failed every movie
`--script`'s per-step timeout is 20 s, to stop an unattended run waiting forever
on a screen that never settles. `S00A` is **93.9 s**, so the first scripted
new-game run would have been killed at step 1 and reported as "never settled".
Raising the constant would have been wrong in the other direction: a movie stuck
at frame 0 would then hang the job, and a job that waits is worse than a job that
fails, because it does not look like a failure.
So the test is **liveness, not duration**: while `get_stream_position()`
advances, the deadline moves with it; a stalled movie still trips the same 20 s.
### Found while looking: `GP_TITLE`'s four unnamed builds are LOADING screens
`build_00`, `build_01`, `build_12`, `build_15` have never had names. Every
element in all four is `pgloading_*``pgloading_processing.png`,
`pgloading_circle1`, `pgloading_delta`, `pgloading_ring` — and `LOADING` is one
of the three screen names the RE agent read out of the title part's state
function.
Two variants: 0/1 carry 7 elements, 12/15 carry 10.
**They are not renamed here.** The archive's own pairing (adjacent for 2/3, `+3`
for 4…9 and for 10/13, 11/14) suggests 0 is 1's twin and 12 is 15's, but which
member of each pair is which **locale** is an inference, and a name is exactly
the kind of thing that stops being questioned once written. Handed to the RE
agent, who can answer it from a capture in one look. `BLOCKED.md` has the row.
⚠️ **And one of them is a second casualty of the `rest.t` problem.**
`pgloading_eff00.prm` on entries 12/15 is a full-screen black quad whose group
runs `0xff000000` at t=38 → `0xff000000` at t=48 → `0x00000000` untimed: black,
held, *then* clear. Its `rest.t` is **38**, where it is fully opaque. A port that
draws that screen at its declared rest draws **a black rectangle over the entire
loading screen**. The title's case only dimmed a frame; this one hides
everything. Filed with the `settle_time()` row it belongs to.
### Refutation — attempted on the fade-quad census; it survives, with a caveat
**The claim** (HANDOFF, on transitions): *"in `GP_TITLE` exactly the six screen
builds carry it while the six overlays do not"*, where "it" is the full-screen
black `.prm` quad *whose keyframe group is the transition*.
**The test**, over the whole export: count builds carrying a full-screen
primitive with black in its keyframes.
```
16 builds exported; 12 carry one.
Of the 12 `is_build` bundles (excluding the 4 authored splashes): 8 carry, 4 do not.
carry: title, title_jp, main_menu, main_menu_jp, extras, extras_jp,
build_12, build_15
do not: press_start, press_start_jp, build_00, build_01
```
8 and 4, not 6 and 6. But the two extras are `build_12` / `build_15`, and their
quad is a **different shape**:
| | transition quad (`pteff00.prm`, title) | loading quad (`pgloading_eff00.prm`) |
|---|---|---|
| | `0xff000000` t=16 | `0xff000000` t=38 |
| | `0x00000000` t=261 | `0xff000000` t=48 |
| | `0xff000000` untimed | `0x00000000` untimed |
| shape | black → clear → **black** | black → held → **clear** |
The transition quad returns to black on exit; the loading quad does not. Read
strictly — the quad *whose group is the transition* — the claim holds.
**Verdict: survives.** The refinement is worth recording anyway, because the
naive test over-counts by two and somebody will run the naive test. There are
**two kinds** of full-screen black `.prm` in `GP_TITLE`, and only one of them is
a transition.
## P7 gate
```
godot --path port -- --menu --script=accept --audio=…/p7.wav
```
```
menu on main_menu, focus ptbtn01
script[1] accept
(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
Neither screen is in this export.
-> video S00A at 1.18 s (/work/export/video/S00A.ogv)
video ended at 94.93 s
-> title (authored: authored)
menu on title, focus (none -- this screen has no focusable item)
script complete after 99.28 s on title
recorded 98.453 s of Master bus (driver Dummy)
```
The movie ran **93.75 s** against a declared 93.9 s, the run ended on the title,
and the Master bus recorded 98.453 s: `pcm_s16le`, 44.1 kHz stereo, RMS
**22.2 dBFS**.
**What this does not show, stated because it would be easy to imply otherwise:**
the recording contains the menu bed *and* the movie together, and this run did
**not** separate them. So it establishes that the engine reached an output for
98 s of a run whose middle 94 s was a movie — not that `S00A`'s own audio track
is in the mix. Separating them wants the P6 null-control method (a paired run
that differs only in the movie), and that is not done here.
🔴 **Peak 0.0 dBFS again.** The same runtime-headroom problem P6 filed: per-file
levels are the disc's and are fine, the Master bus has no headroom, and the port
has set no bus balance because nothing measures one.
### One more file read while it was being written
`ls` reported the recording as **3 702 828 B**; `ffprobe` on the finished file
reports **17 367 084 B / 98.452608 s** — a factor of 4.7. `ffprobe` is right and
the `ls` caught it mid-flight.
`AUDIO-VERIFICATION.md` opens by naming this failure and the port has had the
temp-name-then-rename discipline since P6, which is what makes it worth writing
down rather than shrugging off: **the discipline protects a reader who opens the
path, and it does not protect a reader who stats it at the wrong moment.** Size
on disk is not a measurement of a file somebody else is still writing. Ask the
decoder, not the directory entry.