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
371 lines
34 KiB
Markdown
371 lines
34 KiB
Markdown
# Waiting on the RE agent
|
||
|
||
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
|
||
|
||
| 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. |
|
||
|
||
## 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.
|