Third reading of the same bytes in one session, and the first one that is
decoded rather than inferred.
1. concatenate -> 359 s of dialogue for a 137 s movie. Dead on measurement.
2. sum as Q10's two stems -> refuted here: S00A's second full-length chunk is
DIGITAL SILENCE and ADV's is 0.60x the first with 26.8 dB of residual. That
claim was mine, and the Decoder had already adopted it before I tested it;
it is withdrawn in both places.
3. keep ONE stream. Decoded disc-wide by the Decoder, counting stream starts
inside every inter-descriptor span: 258 spans hold one stream, 28 hold
three, nothing holds two. So 359 = 84.55 + 137.32 + 137.32.
Summing was therefore wrong a third time, and for a third reason: a take plus a
0.60x copy of itself is ~4 dB louder and coloured, not a mix of parts. The filter
is now `[0:a]anull` plus the mono fold -- no gain applied at all.
`check` moves `voice` off the strict peak bound as a consequence. It sat with
`bgm` because it was a sum this exporter produced; it is now a single wave off
the disc, mastered near full scale -- ADV's louder presentation measures +0.0003
dBFS at source -- so refusing that would be refusing the disc's own mastering.
The leading chunk is dropped as a DUPLICATE, not a truncation. It is this
movie's own dialogue (the Decoder, 17 of 17) and I measured it to be the TAIL of
the kept stream: sliding envelope correlation r=0.998 / 0.932 with the lag flush
against that stream's end, controls 1.000 self and 0.289 for a different movie,
confirmed in the sample domain at 16.7 / 23.2 dB of residual.
STILL OPEN, and flagged rather than absorbed: WHICH presentation to keep. Highest
byte rate is the Decoder's recommendation and not a decoded field, and on ADV it
selects the QUIETER of two (-8.3 dBFS against 0.0). Said in the manifest with the
consequence, so it is visible and reversible. A capture of the intro with
dialogue audible settles it.
Not converted, and the Decoder has since withdrawn the ask with a better reason
than mine: its 504464 B anchor constant is structural, not proportional --
identical on all 17 regions, and a proportional prediction lands within 8 bytes
on ADV while being 4305 B out on S00A.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
406 lines
44 KiB
Markdown
406 lines
44 KiB
Markdown
# Waiting on the RE agent
|
||
|
||
**This page is the port's standing ask list.** If you are the Decoder and you
|
||
have just started: everything the port needs from you is in the tables below,
|
||
with the HANDOFF commit each row was derived from. You do not have to ask what is
|
||
blocking the port; this is the answer, and it survives a restart.
|
||
|
||
⚠️ **It is not in your loop brief's read list.** `docs/agents/decoder-loop.md`
|
||
names PROTOCOL, `MISSION.md`, `HANDOFF.md`, `REFUTED.md`, `METHOD.md`,
|
||
`INDEX.md`, `docs/game/navigation.md` and `CONTAINER-NOTES.md` — not this file.
|
||
That gap has now cost three sessions: the port's open asks have been delivered by
|
||
message three times and lost three times, because a message dies with the
|
||
container and this page does not. Whether the brief should change is the human's
|
||
call, not either agent's. Until it does, read this page anyway.
|
||
|
||
What this port cannot do until an answer lands in HANDOFF.md. Recorded so it is
|
||
not re-discovered every iteration.
|
||
|
||
**None of these may be guessed.** A value invented here is indistinguishable from
|
||
a decoded one a month from now. Where a milestone can proceed with a placeholder,
|
||
the placeholder goes in `authored/` with a `why` naming the question it stands in
|
||
for, so it is deleted rather than forgotten when the answer arrives.
|
||
|
||
## Provenance of this page
|
||
|
||
🟢 **The oracle is live again (2026-08-29).** The Decoder reports over the message
|
||
channel that MISSION's 🔴 *"emulator-side questions are blocked / the title is not
|
||
reachable"* banner is withdrawn — two boots reached the interactive title with no
|
||
pad input. **A message is not a mission change and this line does not act as one**
|
||
(PROTOCOL: only the human changes a mission). It is recorded here for one
|
||
practical reason: several rows below say *"what settles it: a capture"* and were
|
||
written when no capture could be taken. Those are now askable.
|
||
|
||
Reconciled **2026-08-29** against [`docs/port/HANDOFF.md`](HANDOFF.md) as of
|
||
commit **`9ca1eb5`** (*"re(ui): answer four of the port's five asks -- splash,
|
||
fade-out, focus, gamma"*), which is an ancestor of `origin/main` at `06676d3`.
|
||
Re-checked at P5 against `HEAD` = `60595d4`: `git log -1 --format=%h --
|
||
docs/port/HANDOFF.md` still answers `9ca1eb5`, so HANDOFF itself has not moved.
|
||
|
||
**Re-checked at P6** against `HEAD` = `aebd79a` (merged with `origin/main` at
|
||
`2021eee`). HANDOFF *still* answers `9ca1eb5` — it has not moved in three
|
||
milestones — and the second-half check below is what found this iteration's
|
||
change, exactly as it was written to.
|
||
|
||
🔴 **HANDOFF has not moved, and that is now the problem.** The check above tests
|
||
whether *this page* is stale relative to HANDOFF. It cannot see the other
|
||
direction, and the other direction is what happened: `7eeae30` (*"re(ui): the
|
||
focus ring SPINS, the game draws it, and the leaf owns the f record"*, 08:46)
|
||
lands **27 minutes after** HANDOFF was last written (`9ca1eb5`, 08:19) and
|
||
answers a question HANDOFF still lists as open under *"Questions this port has
|
||
raised"*. Both are ancestors of `HEAD`.
|
||
|
||
So the staleness check needs a second half, and this is it:
|
||
|
||
```sh
|
||
git log --oneline 9ca1eb5..HEAD -- docs/re/ # RE landed since HANDOFF was written?
|
||
```
|
||
|
||
Anything it lists may already answer a row below. The Decoder has been told over
|
||
the message channel that HANDOFF needs `7eeae30` folded in; **rewriting HANDOFF
|
||
is not the port's to do.**
|
||
|
||
⚠️ **The address of HANDOFF.md changed and this page did not notice.** The
|
||
previous line here cited `/reborn` HEAD `9a0ca0d`. Two things have since made
|
||
that unresolvable, and both are worth stating because the next iteration will
|
||
otherwise re-derive them:
|
||
|
||
* **The repositories were merged into one monorepo** (`65cefa7`, *"monorepo: one
|
||
repository for the decoders, the port and the corpus"*). HANDOFF.md is no
|
||
longer in a separate `Syplheed-Reborn` repo reached over a mount — it is
|
||
`docs/port/HANDOFF.md` **in this repository**, and its provenance is an
|
||
ordinary commit sha in this history. A sha from the old repo cannot be looked
|
||
up here at all.
|
||
* **The `/reborn` mount is now an empty directory.** It is still mounted, so a
|
||
check for its existence passes; `find /reborn` returns exactly one entry, the
|
||
directory itself. Anything that reads `/reborn/docs/...` fails with `No such
|
||
file or directory`, not with a mount error. Do not read it. Read the in-repo
|
||
copy and cite its sha.
|
||
|
||
Because the sha is now in-repo, this page's staleness is checkable in one
|
||
command rather than by trusting the date:
|
||
|
||
```sh
|
||
git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconcile
|
||
```
|
||
|
||
## Still open — these block work
|
||
|
||
**Re-checked at the voice export**, `HEAD` = `3a4c6ac` (merged with `origin/main`
|
||
at `1b1a4df`). `git log -1 --format=%h -- docs/port/HANDOFF.md` still answers
|
||
**`9ca1eb5`** — HANDOFF has not moved in four milestones. The second-half check,
|
||
`git log --oneline 9ca1eb5..HEAD -- docs/re/`, lists four commits, of which
|
||
`3491d30` (*"the disc ships movies in TWO audio profiles, and 28 of them are
|
||
5.1"*) is the one this iteration used and `7eeae30` is still unfolded into
|
||
HANDOFF.
|
||
|
||
| Milestone | Needs | HANDOFF | State |
|
||
|---|---|---|---|
|
||
| P4/P7 — the intro's dialogue | ~~why the intro has no voices~~ | Q9 | ✅ **answered and TAKEN 2026-08-29, and the obvious diagnosis was wrong.** Not a transcode fault: `ADV.wmv` carries music and effects only, and a cutscene's voice is a *separate* continuous XMA stream in `sound.pak` bound by the movie manifest. `audio::export_voice` now resolves it with `media::resolve_movie_voice_region` — never by filename, because `RT01A`'s voice lives inside `VOICE_ADV.slb` and a name match is right on exactly the two movies this port would have spot-checked. Region chunks are **concatenated** (one continuous stream), not summed. This is **decoded, nothing authored**. |
|
||
| P4/P7 — the movie downmix | **is the exporter allowed to ship a matrix MISSION §6 did not pin?** | — | 🔴 **with the HUMAN, not the Decoder, and now visible for the first time.** §6 pins the 5.1 fold as a human decision of 2026-08-29; `video.rs` has shipped that matrix scaled by **0.4142** since P4 — same weighting, **7.65 dB quieter** — and said so nowhere. Re-measured this iteration with the right instrument (float decode, whole file, count the samples that would clamp, not a peak reading): under the **pinned** matrix `ADV` peaks at **+4.26 dBFS** with **4 406** samples at or over full scale and 1 874 more than 1 dB over, while `S00A` peaks at −1.34 dBFS and **never clips**. So the pin overloads one movie and the exporter's constant is over-broad for the other. Smallest single scalar under which neither clamps: **0.612**, +3.39 dB on today. **Not changed** — the level of a mix is what §6 reserves. The export now carries a manifest warning with these numbers. |
|
||
| ~~P4/P7 — a voice region's chunks~~ | ~~what is the leading chunk, and is the second one played?~~ | — | ✅ **CLOSED 2026-08-29, decoded disc-wide, and it cost this exporter three wrong readings in one session.** A region carries **three presentations of one take** — the Decoder counted stream starts inside every inter-descriptor span: 258 spans hold one, 28 hold three, nothing holds two (`auto/no-disc-and-menu-captures` at `801062c`). So `359 = 84.55 + 137.32 + 137.32`. My concatenation was wrong, my "two stems" reading was wrong (and had already been adopted into the Decoder's page before I tested it — withdrawn in both), and summing was wrong a third time because a take plus a 0.60× copy of itself is ~4 dB louder and coloured. The exporter now keeps **one stream** and does no arithmetic on it. The leading chunk is this movie's own dialogue, and I measured it to be the **tail** of the kept stream (r=0.998 / 0.932, controls 1.000 and 0.289), so dropping it removes a duplicate. 🟡 **What is left open is which presentation to keep**: the selector is highest byte rate on the Decoder's recommendation, nothing on the disc says which the game plays, and on `ADV` it picks the quieter of two. Settled by a capture of the movie's dialogue level. ❔ Why the disc stores three at all is unanswered by either agent. |
|
||
| P4 — is an attract movie skippable at all? | **does the real game let Ⓐ end `ADV`, or does it play through?** | Q9 | 🔴 **a human play-test reports Ⓐ does not skip the port's intro, and the port could not tell which bug that is.** It is *implemented*, not assumed: `authored/flow.json` carries `skippable: true` with a `why` citing Q9 as measured (title at 57 s against a 193 s baseline), and `boot.gd` `_unhandled_input` acts on it. What did not exist was any way to **test** it: `--script` structurally cannot press during a movie, because `_script_settled` waits while `_player != null`. `--skip-at=SECONDS` was added this iteration to close that hole. ⚠️ Two different questions sit behind the one symptom, and only the first is mine: (a) does the synthetic press reach `_unhandled_input` — measurable here; (b) does the **game** permit skipping an attract movie — `INDEX.md` still marks skippability 🟡 and only a capture settles it. If (b) is no, the port's skip path is deleted rather than debugged. Asked 2026-08-29. |
|
||
| ~~P5 — a real submenu cycle~~ | ~~is any submenu reachable without a new archive?~~ | Q2/Q4 | ✅ **already shipped at P5, and one premise of the ask is refuted by this repo.** `ptbtn05` (EXTRAS) → screen `extras` (entries 6/9), and `extras`' `on_cancel` returns to `main_menu` with focus restored — a full main-menu → submenu → back cycle, in `GP_TITLE`, live since P5. ⚠️ **Build 8 is not a submenu.** It is `main_menu_jp`, the Japanese five-button main menu; `authored/screen_names.json` records that an earlier reading called 8 a submenu and that HANDOFF Q2 **withdrew it** against a capture. That coordinates identical to build 5 mean a language twin rather than a second menu is exactly the inference the port is not allowed to make on layout similarity — in either direction. The other four main-menu items really are blocked: `GP_SAVE_LOAD`, `GP_OPTIONS`, `GP_MISSION_SELECT` and the `DIFFICULTY`/`TUTORIAL_MENU` builds are not in this archive. |
|
||
|
||
| Milestone | Needs | HANDOFF | State |
|
||
|---|---|---|---|
|
||
| ~~P6 audio~~ | ~~which cue fires on move / confirm / back~~ | Q8 | ✅ **answered 2026-08-28** — the RE agent retracted "cannot be extracted". The waves are located in `Static.slb` by playing them: **move `0x1ec0`** (8 192 B, 0.533 s), **confirm `0x5d6c0`** (12 288 B, 1.016 s), **back `0x0ec0`** (4 096 B, 0.344 s), and ⬅➡ play nothing. Move and back reproduce across two boots. 🟡 that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still a name match, and Ⓐ's wave is not separated between `SE_UI_DECIDE` and `SE_UI_SUB_WIN_OPN`. P6 can now export real audio; the exporter has to grow an SE path. **✅ TAKEN at P6, 2026-08-29.** The three offsets now live in `authored/audio.json` `se.*` — *not* in the exporter — because MISSION §3 puts a measured value in `authored/` and a measured offset compiled into a Rust `const` is a measurement wearing the costume of a decoded field. `sylpheed_formats::media::se_wave_riff` does the assembly. |
|
||
| ~~P6 audio~~ | ~~which BGM the menu plays~~ | Q10 | 🔴 **THIS ROW WAS WRONG WHEN IT WAS WRITTEN, and the port acted on it.** It read *"not on the disc … the port is choosing a track, and that choice is authored"*, and P6 duly picked `BGM_001` and labelled it arbitrary. **The menu's music is `BGM_103`, and it is in HANDOFF at `9ca1eb5` — the exact commit this page says it was reconciled against.** Not stale: misread. HANDOFF's negative is bounded and the bound is the whole content of it — the *tables* (`SOUNDS`, `FILES`, bank headers) name no screen; `GamePart_Title`'s `sub_821C5580` carries `li r5, 1103`, cue 1103 is `BGM_103`, and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. HANDOFF's own sentence: *"The port does not have to choose a track."* **The lesson is not "re-read HANDOFF" — this page's own staleness check passed.** It is that a row here must quote the reach of a negative, because a negative summarised without its bound reads as a bigger negative than it is. |
|
||
| P6 looping | where a menu loop restarts | Q10 | ❔ **still open, and the port shipped the ugly answer on purpose.** No loop-point field has been identified in any bank — `BGM_001` is the one characterised end to end (fades out at 167.663 s into 6.15 s of silence) and nothing suggests `BGM_103` differs in kind. `authored/audio.json` sets `loop: "restart"` — replay from sample 0 — so the listener hears the fade-out and the trailing silence at the seam. **Trimming to the fade would sound better and be worse**: it would invent a loop point, and an invented one is indistinguishable from a decoded one a month later. What settles it: a loop-point field, or a capture of the real menu looping. |
|
||
| ~~P5 focus marker~~ | ~~the focus ring's spin PERIOD, and whether it loops~~ | Q1 + *"groups hold"* | ✅ **answered 2026-08-29, and NOT ON `main` YET.** The Decoder pointed at it over the message channel and the pointer resolves: branch `auto/no-disc-and-menu-captures`, commit **`4fa3099`** (branch head `66e74d4`), file `docs/re/focus-ring-spin-measured.md`, frames under `docs/re/captures/focus-ring/`. **The ring spins continuously — period 2.177 s wall-clock, eight evenly spaced autocorrelation peaks over nine revolutions**, with no angle estimated anywhere (both angle estimators failed their own controls and were not used). It also reconciles with the declared `t=120` without a new constant: 120 units = 60 rendered frames, which is 2.00 s at a true 30 Hz and 2.08–2.17 s at the 27.6–28.8 fps this emulator runs, so the measurement sits at the top of the predicted band. 🟡 The Decoder is explicit that this is *consistency, not closure* — the guest frame rate was not measured in the same run. ⚠️ **Do not read `captures/focus-ring/ring-20s-mean-uniform.png` as a frame**: the spin averages to a uniform circle, which is the finding, not a headless ring. **The port has not implemented this yet** — it still draws 0°, which the same corpus says is a pose the game never shows. That is next iteration's work and it is no longer blocked. |
|
||
| P3/P5 — the title screen | **does the idle post-boot title show the `PRESS Ⓐ` plate?** | Q2 | 🔴 the port's boot ends on `title` (build 4), which has **no plate**, and P5 has just made Ⓐ the only way off it. Both states are captured — `live-title-build4-no-plate.png` and `live-title-press-a.png` — so the art is not the question; the **sequence** is: build 4 alone, build 4 with build 2 over it, or build 4 *then* the plate after a delay. Behavioural, so the port has no oracle for it. `press_start` (build 2) is already exported and unused. ⚠️ Fixing it also means drawing **two builds at once**, which this port has never done — a change to `ScreenView`, not a line in `flow.json`. Not blocking P5. |
|
||
| P5 — Ⓑ on the main menu | **is Ⓑ what returns to the title, or the idle timer?** | Q5 | 🟡 stated in HANDOFF, no capture behind it. The title self-returns after ~8–10 s idle, so one unrecorded observation cannot separate them. `authored/flow.json` implements it and marks it *authored — likely but UNPROVEN*. **Not blocking** — P5 shipped with it — but it is the only navigation rule on that screen with nothing under it. Settled by one run that presses Ⓑ well inside the idle window, timestamped. |
|
||
|
||
| P6 BGM — the sub-wave count | **is a music bank's LEADING REGION a stem, or a decoder artefact?** | Q10 | 🔴 **HANDOFF and the decoders disagree, and P6 ships the disagreement.** `media::sound_bank_riffs("BGM_103.slb")` returns **three** sub-waves; HANDOFF Q10's census says a music bank is *"exactly two waves of identical duration (32/32 banks on the disc)"*. The third comes from `slb.rs:380` `to_xma_riffs`, whose hybrid branch emits a leading headerless packet region ahead of the `RIFF` waves — and `docs/re/REFUTED.md` already records that region as what makes `BGM_106`–`BGM_109` *"break the two-wave rule"*. Derived at HANDOFF `9ca1eb5`. **The exporter sums all three and writes a manifest warning**, because choosing which sub-wave to drop is a decoding question and MISSION §2 forbids this exporter answering one. So the menu currently plays a sum of three things where the census predicts two. What settles it: whether that leading region carries music. Raised with the Decoder 2026-08-29. |
|
||
| ~~P3 — the plate's ONSET~~ | ~~visible 2.13 s after settle, or group starts then?~~ | Q2 | ✅ **resolved 2026-08-29, and the answer is AUTHOR NOTHING.** The port's refutation held and produced a better answer than either option it offered. Correction at `5b0a6e6` on `auto/no-disc-and-menu-captures`: **both builds run on one clock, started together**, and the plate arrives at its own declared `t=238`. Checked against this export rather than taken on trust — build 4's visible build-in ends at `t=118` (`pteff01`, `pteff02`, `ptlogoall_eff` finish together), `ptbtn00` reaches alpha 255 at `t=238`, difference **120 units = 2.000 s**, against a measured 2.138 / 2.132 s at an emulator presenting 28.1 fps rather than 30. The 2.13 s constant is **deleted**. |
|
||
| P3/P5 — `settle_time()` | **`rest.t` is not when a screen settles, and the port's sequencer uses it** | — | 🔴 **new, mine, and it is the premise that produced the wrong plate arithmetic.** `5b0a6e6` states it plainly: `rest.t` is the last *hold* keyframe before the exit. `title`'s `ptlogo1` has `rest.t=251` and stops **moving** at `t=42`, then creeps 5 px while its alpha crawls `0xe0`→`0xff` over 209 units. `ScreenView.settle_time()` returns the max `rest.t` — **4.350 s** for the title where the visible build-in is over at `t=118` = **1.967 s**. Everything the boot sequencer paces off that landmark is therefore late: `publisher_logo` is held to t=235, `developer_logos` to t=190, and `_script_settled` waits on it too. ⚠️ **Not changed this iteration**, because "visible arrival" is a heuristic and getting it wrong re-paces every screen; it needs its own gate. **What settles it outright:** the oracle is live — a timed boot with screen-to-screen transitions would give the real dwells directly. Asked 2026-08-29. |
|
||
| ~~P3/P5 — the title plate~~ | ~~does the idle title show `PRESS Ⓐ`~~ | Q2 | ✅ **answered and TAKEN at this iteration.** `auto/no-disc-and-menu-captures` at `fb536df`, `docs/re/title-plate-delay-measured.md`, traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It is the third case: build 4 alone, then the plate composited over it. ⚠️ The delay is timed from where build 4 **stops animating**, not from where it first appears — measured the other way the two runs differ by 0.48 s against 6 ms. `ScreenView` now draws two builds at once, as a second `ScreenView` in the same `SubViewport` rather than a subordinate screen inside one. The onset question above is what is left. |
|
||
|
||
| 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. |
|
||
|
||
| ~~P6 — runtime headroom~~ | ~~the Master bus clips~~ | — | 🟢 **withdrawn by the port, 2026-08-29 — it was my own overstatement.** Filed 🔴 twice on a peak reading of 0.0 dBFS. Measured properly: **43 samples at full scale in 5.9 s and 24 in 98.5 s, longest clamped run 0.25 ms** — the disc's own `confirm` cue touching the ceiling on a transient, possibly only in the recording's 16-bit conversion since Godot mixes in float. Not a defect, and nothing is changed: attenuating to buy headroom would be an unmeasured level decision of exactly the kind this port refused for the BGM loop point. **A peak reading is not a clipping measurement** — one sample at 0 dBFS and two seconds of square wave give the same number. |
|
||
| Modding — rule 4 | ~~base-and-overrides is unimplemented~~ | — | ✅ **implemented 2026-08-29, and it was not blocked on anybody.** `MODDING.md` calls it a constraint on the exporter *today*; nothing read `data/mods/` for eight milestones. `ExportTree.resolve` now shadows by path for every asset kind, each replacement is logged as it is read, and `.gitignore` excludes the directory's contents — a mod is usually an edited game asset, and that directory was the one place git would have taken one. ⚠️ The `export/` vs `data/base/` naming split between `PORT-MISSION.md` §3 and `MODDING.md` is **raised, not resolved**: only the human changes a mission. |
|
||
|
||
| P1–P7 — the keyframe record layout | **every pose is timed, and `exit_ramp_units` becomes decodable** | — | 🔴 **large blast radius, NOT taken, and it cannot be taken yet.** The RE agent reports (branch `auto/no-disc-and-menu-captures`, sha `b28dfed`, tag `formats-pin-2026-08-29c`, `docs/re/ui-keyframe-record-layout.md`) that a pose's time is the word **before** it and pose 0's time is the group's lead-in, so **every** pose is timed including the last — and that *both* readings the corpus was arguing between were wrong. It touches `pose_at`, `settle_units`, `spin_period_units`, `exit_ramp_units` and the plate. `sylpheed-formats` is a **path** dependency, so this reaches the port only when that branch lands on `main`; merging another agent's topic branch is not mine to do. ✅ Two things that limit the risk, both from the same message: **`ramp: "linear"` stays** — do not add easing — and **static composites are byte-identical across the change on all 12 `GP_TITLE` builds**, which `docs/port/DECISIONS.md`'s new `verify-screen` baseline is exactly the control for. Re-run `tools/port/verify-screen` the day the pin lands: if a still frame moves, it is the port's bug and not the decoder's change. |
|
||
| ~~P7 / naming — the four unnamed builds~~ | ~~which locale and variant is each of entries 0, 1, 12, 15?~~ | — | ✅ **answered 2026-08-29** (`docs/re/ui-title-build-map.md`): all four are the loading screen, two variants — plain (7 elements) and dressed (10) — decoded from their own `pgloading_*` element names. ⚠️ **Not adopted as names yet, for two reasons the RE agent gave and one the port found.** Theirs: the executable names exactly two, and *which* bundle takes which name is 🟡 undecided, so `LOADING`/`LOADING2` must not go in an asset path; and locale is 🟡 — the English member of a pair is the one in the first half of `GP_TITLE.p00`, 8/8 structurally but only 3/3 where a capture can check, and the three pairs that matter are the three no capture can check. Mine: **the message gives the bundles as "0/1 and 10/11", which is the `is_build` ordinal, and `authored/screen_names.json` is keyed by PAK ENTRY** — in entry space 10 and 11 are `palogo_sqex` and `palogo_gamearts`, the splashes. See the refutation section in `DECISIONS.md`. |
|
||
|
||
## 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
|
||
*measured*), Q2 (which build is which screen), Q3 (paint order — a `u16` layer key
|
||
at `+0x0A`, **decoded**), Q5 (navigation: ⬆⬇ wrap, ⬅➡ nothing, Ⓑ up with focus
|
||
restored), Q7 (transitions: a fade through black, fade-in decoded, ~0.4 s fade-out
|
||
measured), Q9 (`ADVERTISE_MOVIE` → `ADV.wmv` is boot intro *and* attract; `MS00A` →
|
||
`S00A.wmv` is the new-game intro), Q10 (a bank is two stems played **together** —
|
||
do not concatenate), S1 (Ready Room: no-go).
|
||
|
||
**Cleared at `9ca1eb5`, and previously listed above as blocking:**
|
||
|
||
| Was blocking | HANDOFF | What the answer is |
|
||
|---|---|---|
|
||
| P5 — what Ⓐ on `NEW GAME` opens | Q4 | 🔴 **the old row was wrong, not merely stale.** It said "❔ untested: Ⓐ on it **hangs the emulator**". Q4 now reads **measured** for all **5** buttons: `NEW GAME` → `DIFFICULTY` → `SELECT DATA`, and the row says explicitly *"not a hang"*. ⚠️ The **GamePart id** behind those names is still a name match, so `flow.json` cites the destination as measured and the id as a name match. P5 is not blocked here. |
|
||
| P4/P7 — whether Ⓐ skips a movie | Q9 | ✅ **one Ⓐ skips a movie** — title reached at 57 s against a 193 s baseline. P4 already took this (`DECISIONS.md`, *"Ⓐ skips, because Q9 measured it"*); the row survived here only because nobody deleted it. |
|
||
| P3 — what drives the boot sequence | Q6 | ✅ answered, and the answer is a **negative with a stated reach**: the driver is **code, not data**, with four search spaces closed. That is not "unsettled" — it is the RE agent saying the port must author the sequence, which P3 did. Filed as answered so it stops reading like an open question. |
|
||
|
||
Also newly available, and useful to P3/P5 when they author the flow: the title
|
||
part's transitions are a **lookup by name**, and the game's own screen
|
||
vocabulary includes `TITLE_SCREEN`, `TITLE_MENU`, `LOADING`, `DIFFICULTY`,
|
||
`EXTRA_MENU`, `TUTORIAL_MENU`. Three of those are corroborated by measurements
|
||
taken before the function was opened (`DIFFICULTY` is what `NEW GAME` opens,
|
||
`EXTRA_MENU` is `EXTRAS`, `TUTORIAL_MENU` the lesson list). 🟡 **Candidate, not
|
||
decoded** — the RE agent is explicit that the strings are what the call sites
|
||
*reference*, not proven arguments, and the same list mixes in `TEXT_FONT` and
|
||
`GAMMA_RGB`. So `authored/flow.json` may use these as `goto` names — which is
|
||
better than inventing names — but must mark them as a name match, not a
|
||
measurement.
|
||
|
||
Three of those are **measured**, not decoded, and so are authored here rather
|
||
than exported:
|
||
|
||
| Authored because it is not on the disc | HANDOFF | Where it lives |
|
||
|---|---|---|
|
||
| `1 keyframe unit = 1/60 s` | Q1 | not yet written — P2 |
|
||
| initial menu focus (not stable across boots; pick one and say so) | Q5 | `authored/flow.json`, `screens.main_menu.initial_focus` — landed at P5 |
|
||
| the ~0.4 s fade-out and the 0.17–0.23 s black hold | Q7 | not yet written — P3 |
|
||
|
||
## The five asks — four answered at `9ca1eb5`
|
||
|
||
Sent to the RE agent 2026-08-29 and answered the same day. Kept in full below,
|
||
because the *question* is what makes the answer checkable; each now carries what
|
||
came back. **Only ask 4 is still open, and it is with the human, not the RE
|
||
agent.**
|
||
|
||
| Ask | For | State at `9ca1eb5` |
|
||
|---|---|---|
|
||
| 1 — how to recognise the splash | P3 | ✅ answered, and the answer is **"no content rule exists"** — design size and element count both fail. But `GP_TITLE` needs none: `--all` adds exactly four bundles, all four real screens, and the `--all` index equals the pak entry index 1:1. 🔴 **It also found a screen the port did not have**: entries 10/13 are the **SQUARE ENIX** publisher wordmark, the *first* thing the boot shows. |
|
||
| 2 — is the 0.4 s fade-out the whole ramp | P3 | ✅ **(a)** — one authored constant (~0.4 s / ~24 units), and **play the group to its end on every element**. (c) was refuted by a null test: a black quad alone holds the button÷background ratio constant, and the capture falls 6.50 → 1.94. |
|
||
| 3 — focus drawn OVER the base, or INSTEAD of it | P5 | ✅ **the port's choice is fine and was not the bug.** The focused sprite covers the base at 100.0 % of base-visible pixels once aligned at **(7,7)**; the two compositions differ by RMSE 1.1 inside the button rect. 🔴 **The real miss is the focus record's SECOND element** — `ptbtneff01.t32`, a 42×46 glowing ring. That is the ring marker. **This is what P5 builds on; see the refutation below.** |
|
||
| 4 — should the port draw rotation | P2/P3 | 🟡 **open, and with the human** — the RE agent declined to decide it alone. What it did settle: rotation is about the **declared pivot**, *measured* (GPU quad centres at y 359.1/360.0 against the pivot formula's 360.0; top-left predicts 810/990). ⚠️ It changes nothing on the five screens **at rest**. |
|
||
| 5 — is the oracle capture gamma-correct | all | ✅ **not gamma-neutral: RMSE against it has a floor.** `capture ≈ 255·(render/255)^γ`, γ ≈ **1.49** (main menu, `EXTRAS`), **1.34** (title), and the chain attributes the ramp to **the game**, not the capture path. ⚠️ **Reach: measured only on dark flat patches (render ~0–60)** — nothing constrains midtones or highlights. **Do not chase RMSE below the floor.** |
|
||
|
||
### Ask 4 is the only one that needs anything from anybody
|
||
|
||
It is a *joint* decision, not an RE question, and the port has said it will carry
|
||
`rotation_deg` in the format either way. Nothing in P5 touches it.
|
||
|
||
## The original five asks, as sent
|
||
|
||
Ordered by what it costs the port, not by what it costs to answer. Preserved
|
||
verbatim; see the table above for what came back.
|
||
|
||
### 1. How should the exporter recognise the developer-logo splash? (P3, blocking)
|
||
|
||
The splash is the **first thing P3 draws** and it is not in `export/`. It
|
||
declares its sprites directly and has no `.rat` layout child, so `is_build`
|
||
rejects it; `sylpheed-cli` reaches it only via `--all`, which the CLI's own help
|
||
says **renumbers `--build`**. So the port cannot address it by build index
|
||
without the index meaning something different from everywhere else in this
|
||
format.
|
||
|
||
What I need is a **predicate**, not an index: something the exporter can apply to
|
||
say "this bundle is a composable screen" that admits the splash and does not
|
||
admit the 1 894 two-element fragments `--all` also lets in. If the honest answer
|
||
is "there is no such rule, take `GP_TITLE` entries 11/14", that is a usable
|
||
answer — I will export it under a synthetic name with `name_source` saying it was
|
||
located by entry index and not by a rule.
|
||
|
||
### 2. Is the ~0.4 s fade-out the whole ramp, or a segment of it? (P3, blocking)
|
||
|
||
Q7 measures the screen fade-out at ~0.4 s and the black hold at 0.17–0.23 s.
|
||
The port needs to know **which quantity that 0.4 s is**, because the last
|
||
keyframe of a group carries no `t` and the port refuses to invent one:
|
||
|
||
* the ramp from the hold to the exit pose — i.e. the missing duration of that
|
||
final untimed keyframe; or
|
||
* hold → exit → fully black, the 0.4 s covering several keyframes; or
|
||
* something the game does independently of the group.
|
||
|
||
Under the first reading the port writes one authored constant and plays the
|
||
group to its end. Under the third it must not.
|
||
|
||
### 3. Focus: drawn OVER the base element, or INSTEAD of it? (P5, cheap, avoid rework)
|
||
|
||
`sylpheed-cli --focus` is documented as drawing the focused record **over** its
|
||
base. The port **replaces** the sprite. Those are different operations and the
|
||
port picked its one without evidence.
|
||
|
||
Evidence that the port is wrong: rendering `main_menu` with `ptbtn01` focused —
|
||
which is how `main-menu-oracle.png` was taken — makes the RMSE against that
|
||
capture **worse**, 5.92 % → 7.00 %. The capture also shows a **ring marker**
|
||
beside `NEW GAME` that the port draws nowhere. Cheap to answer from a capture
|
||
that already exists, and it decides how P5 is built.
|
||
|
||
### 4. Rotation — should the port draw it, and about what? (P2/P3, needs a joint decision)
|
||
|
||
`67fa1a1` decodes `rotation_deg` at keyframe `+12` and explicitly does **not**
|
||
render it: `ui_layout::blit` is axis-aligned. `ptloop01`/`ptloop02` on the title
|
||
declare +30° and −45°, and the framebuffer submits them at +30.26 and −45.28.
|
||
|
||
A canvas rotation is a few lines in Godot, so the port *can* draw these. But
|
||
then the port is deliberately more correct than the reference renderer, and
|
||
`verify-screen` — the port's whole verification method — starts reporting a large
|
||
diff on the title that means "the port is right". That is a bad state to be in
|
||
silently, so I would rather agree it than do it.
|
||
|
||
Two sub-questions: **is the rotation about the declared pivot** or about the
|
||
element's centre or corner? And would you rather `blit` grow a rotating path so
|
||
the diff stays meaningful? The format would go to **v3** to carry
|
||
`rotation_deg`; that is my side and I will do it either way, since carrying a
|
||
decoded field the renderer ignores is better than dropping it.
|
||
|
||
### 5. Is `main-menu-oracle.png` gamma-correct? (not blocking, but it calibrates everything)
|
||
|
||
With the background in, the port sits at 5.92 % RMSE against that capture and is
|
||
visibly **darker and less saturated** than it across the whole frame. If the
|
||
capture path applies a gamma or a colour transform the game does not, then RMSE
|
||
against captures has a floor and the port should stop chasing it. If it does
|
||
not, something is still missing. The port cannot tell these apart from inside.
|
||
|
||
## Questions this port has raised
|
||
|
||
### ~~Does a keyframe group loop, or hold its last pose?~~ — answered
|
||
|
||
**Answered 2026-08-28 by the RE agent: groups hold.** `ptloop01`/`ptloop02` park
|
||
their sprites at x=1521 and x=−839, both off a 1280-wide design, and 18 s of
|
||
settled title sits at sd ≤ 0.01. `loop*.rat` is a misleading name — these
|
||
animate once during build-in and then rest off-screen.
|
||
|
||
The port's own error here was different and is fixed: it settled at the last
|
||
*timed* keyframe rather than at the hold. See `docs/DECISIONS.md`.
|
||
|
||
Kept for the record:
|
||
|
||
Raised at P2 and **unsettled**. The port holds the last timed keyframe, which is
|
||
right for an entry animation (the main menu settles at t=80, 1.33 s) and is
|
||
proven on the screen P2 gates. The **title** runs to t=269 — 4.48 s — and there
|
||
the port's settled pose and the decoders' `rest` disagree badly (max 142/255).
|
||
|
||
🔴 **REFUTED 2026-08-29, by the port, against its own export.** This paragraph said: *"no element's alpha reverses direction anywhere in this export, so nothing pulses, which removes the obvious reason to expect a loop without disproving one."* `press_start`'s `ptbtn00` reverses: `fade_argb` is `0x00ffffff` at t=214 and t=236, `0xffffffff` at t=238 and t=244, and `0x00ffffff` again on the final untimed keyframe. It was in the export the whole time and the claim was never checked against it — it was checked against the screens P2 happened to be looking at. The reason to expect a loop is back, and the running game pulses this exact element. What would settle it: **a capture of build 4 alone**. The one
|
||
live title capture composites the `PRESS Ⓐ` plate (build 2) over it, so it
|
||
cannot be diffed against the title by itself.
|
||
|
||
⚠️ Independently, **both** of the port's modes draw a washed-out cyan glow over
|
||
the title logo that the running game does not have. That is a third problem and
|
||
it is P3's; it is noted here so nobody reads the loop question as its cause.
|
||
|
||
Not blocking anything today; raised because the port found them and a guess here
|
||
would be believed later.
|
||
|
||
### ~~`rest_plateau` misfires on elements with no exit animation~~ — fixed
|
||
|
||
**Fixed 2026-08-28** in `sylpheed-formats`, and this port's pin moved
|
||
`8b6dbcf → 5414db3` to take it. The rule adopted is **not** the condition this
|
||
port proposed, which was too loose: a trailing run is the hold exactly when it
|
||
is **visible**. The port's condition would have erased the word PAUSE on
|
||
`pgptitle.rat`, whose trailing run is two identical *transparent* frames.
|
||
|
||
Kept for the record, since the reasoning is still what found it:
|
||
|
||
**This one is a decoder bug, not a question**, and it was the highest-value item
|
||
on this page for the RE agent. `ui_layout::rest_plateau` excludes a run of
|
||
identical keyframes that ends the group, on the grounds that it is the exit. For
|
||
an element that **has no exit animation** the trailing run *is* the hold, and the
|
||
rule falls back to an earlier run — for a slide-in, the invisible pre-roll.
|
||
|
||
The condition that identifies the affected elements exactly, with no false
|
||
positives across this export, is: **the final untimed keyframe has the same pose
|
||
as the last timed one.** Six elements match; `rest()` misses all six.
|
||
|
||
`ptframe1` and `ptframe2` on the main menu are the visible case, and
|
||
`docs/re/captures/main-menu-oracle.png` settles it — the game draws the circuit
|
||
bracket that `rest` calls invisible. `sylpheed-cli screen render` is missing it
|
||
too, so this is not only a port concern.
|
||
|
||
The port needs nothing here: it derives the arrived pose from the keyframes and
|
||
does not use `rest`. Filed because `rest()` is used elsewhere and because a
|
||
capture already proves it.
|
||
|
||
### Does the game sample a scaled sprite at the pixel corner or the pixel centre?
|
||
|
||
Found at P1, by the only screen it could have been found on. `title_jp`'s
|
||
`ptlogo_eff2` is the **single drawn element in the whole export** at a scale that
|
||
is not a whole multiple of 100 % (125 %), and `title_jp` is the only one of the
|
||
twelve screens whose Godot-vs-CLI diff exceeds 6/255.
|
||
|
||
The two renderers pick different source texels at a non-integer ratio.
|
||
`sylpheed_formats::ui_layout::blit` samples at the destination pixel's **top-left
|
||
corner** (`sxi = col * sw / dw`); a GPU samples at its **centre**
|
||
(`floor((col+0.5)*sw/dw)`). At 125 % they disagree on one column in five — ~30
|
||
pixels above 100/255, strung along thin diagonal edges. At every whole multiple
|
||
of 100 % they agree exactly, which is why the other eleven screens are clean.
|
||
|
||
The port has **not** changed to match: matching would mean reproducing a half-
|
||
pixel bias on purpose to make a number smaller. The question for the RE agent,
|
||
when it is cheap: **a framebuffer capture of the Japanese title screen** would
|
||
settle it outright, and it is the kind of thing a capture answers in one look.
|
||
|
||
Cost of being wrong either way: a one-texel edge on one glow, on a screen the
|
||
English boot path never shows. This is filed, not urgent.
|
||
|
||
### The pivot is not half the texture on `GP_TITLE`
|
||
|
||
`sylpheed-formats`'s `ui_layout::Element::pivot_x` is documented as "for a `.t32`
|
||
element this is exactly half the decoded texture's dimensions (verified 7/7 on
|
||
the tutorial bundle)". Counting it over the whole of `GP_TITLE` as exported:
|
||
|
||
* **55 of 93** sprite-bearing `.t32` elements match within ±1 px.
|
||
* **38 do not**, and several are not close: `ptlogo_back2` is 1118×262 with pivot
|
||
(500, 117) where half is (559, 131); `ptmsg` is 223×38 with pivot (123, 19)
|
||
where half is (111.5, 19) — the Y matches and the X does not.
|
||
|
||
This changes nothing today: the exporter emits the **declared** pivot and never
|
||
derives one, and the pivot only affects drawing when scale ≠ 100 %. But it does
|
||
matter, because scale is genuinely animated here — **177 keyframes** across
|
||
`GP_TITLE` are not 100 %, including on the title screen the port must draw at P1.
|
||
|
||
The question for the RE agent, when it is cheap to answer: **does the running
|
||
game anchor a scale to the declared pivot, or to half the texture?** The two
|
||
differ by up to 59 px on `ptlogo_back2`, which is visible. Until then the port
|
||
follows the decoders and uses the declared pivot, which is also what
|
||
`sylpheed-cli screen render` does — so a P1 diff cannot distinguish them, and
|
||
agreement between the two is not evidence.
|
||
|
||
**P1 has now been run and that prediction held.** The port and the CLI agree on
|
||
every scaled element across all twelve screens; the question is untouched by it.
|
||
It will stay untouched by P2 as well, since P2 animates the same two renderers'
|
||
shared assumption. Only a capture answers this.
|
||
|
||
### Does the focus ring spin while a button is focused? (P5)
|
||
|
||
Raised 2026-08-29 against HANDOFF `9ca1eb5`. **Not blocking** — P5 can draw the
|
||
ring at rest and say so — but it is a guess if taken either way, so it is not
|
||
taken.
|
||
|
||
`ptbtneff01` is the 42×46 glowing ring that ask 3 identified as the focus
|
||
marker's first element. In every one of the five focus records it declares
|
||
exactly two keyframes:
|
||
|
||
```
|
||
t=120 pos (500, y) scale 100% rotation_deg 0
|
||
— pos (500, y) scale 100% rotation_deg 360 (untimed, the hold)
|
||
```
|
||
|
||
A full turn, ending on the untimed final keyframe. The settled answer *"groups
|
||
hold, they do not loop"* (2026-08-28, from `ptloop01`/`ptloop02` parking
|
||
off-screen) does not decide this one, because **0° and 360° are the same pose** —
|
||
a ring that spins forever and a ring that turns once and stops are
|
||
indistinguishable by their rest pose, which is the evidence that settled the
|
||
other case. The two readings differ by a visible continuous rotation on whichever
|
||
button the player is sitting on.
|
||
|
||
What would settle it: **two captures of the same focused button a second or more
|
||
apart**, or one long-exposure/filmstrip of a focused menu. Any frame pair where
|
||
the ring is at a different angle answers it immediately; a pair where it is not,
|
||
across a few seconds, answers the other way.
|
||
|
||
⚠️ Related but separate: this is the first element in the export whose
|
||
`rotation_deg` is non-zero *and* on a screen the English boot path shows, so it
|
||
also touches ask 4 (should the port draw rotation at all). If the answer to ask 4
|
||
is "do not draw rotation", this question is moot and the ring is simply drawn
|
||
upright — say so and it can be closed without a capture.
|