Five items, written down here rather than only in a message, because a request that lives in one conversation dies with the container. Two are blocking P3 (how to recognise the splash bundle, and what the ~0.4 s fade-out actually measures), one is cheap and prevents P5 rework (focus drawn over vs instead of the base -- the port picked replace without evidence, and the oracle says it picked wrong), one needs a joint decision rather than a unilateral one (whether the port should draw the newly-decoded rotation and thereby become deliberately more correct than the renderer it verifies against), and one calibrates whether RMSE against captures has a floor at all.
240 lines
14 KiB
Markdown
240 lines
14 KiB
Markdown
# Waiting on the RE agent
|
||
|
||
What this port cannot do until an answer lands in
|
||
[`/reborn/docs/port/HANDOFF.md`](https://git.mc02.dev/fabi/Syplheed-Reborn).
|
||
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.
|
||
|
||
Last reconciled against HANDOFF.md on **2026-08-29**, at `/reborn` HEAD `9a0ca0d`.
|
||
(`/reborn` is mounted read-only, so `git -C /reborn pull` fails by design; the
|
||
mount is refreshed outside this container and HEAD is read, not fetched.)
|
||
|
||
## 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. |
|
||
| P6 audio | which BGM the menu plays | Q10 | ❔ **not on the disc.** All 32 banks are named `BGM_001`…`BGM_109` with no semantic name anywhere. The port is choosing a track, and that choice is authored. |
|
||
| P6 looping | where a menu loop restarts | Q10 | ❔ `BGM_001` fades out at 167.663 s into 6.15 s of silence, and no loop-point field has been identified. A menu loop is authored. |
|
||
| P4/P7 video | whether Ⓐ skips a movie | Q9 | 🟡 unsettled — the corpus says Ⓐ skips every time, the boot harness never taps during a movie because it breaks the title. P4 can play the movie; it cannot yet say what a button press does during one. |
|
||
| P5 `NEW GAME` | what Ⓐ on `NEW GAME` opens | Q4 | ❔ untested: Ⓐ on it **hangs the emulator**. The other four destinations are measured. |
|
||
| P3 sequencing | what code decides to advance the boot sequence | Q6 | 🟡 the order is observed and the attract cycle timed (~8–10 s idle → fade → `ADV.wmv` in full → title). The *driver* is not decoded. P3 can reproduce the observed behaviour and must say it is reproducing an observation. |
|
||
|
||
## 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).
|
||
|
||
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 | not yet written — P5 |
|
||
| the ~0.4 s fade-out and the 0.17–0.23 s black hold | Q7 | not yet written — P3 |
|
||
|
||
## What the port needs next — sent to the RE agent 2026-08-29
|
||
|
||
Ordered by what it costs the port, not by what it costs to answer.
|
||
|
||
### 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).
|
||
|
||
What is known: 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. 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.
|