docs: the timeline lands on rest -- except on six elements, where the game

agrees with the timeline

P2's gate is the buttons sliding in, and `tools/screen-strip` renders the strip
that shows it. But the useful result came out of checking where the animation
settles.

On 8 of 12 screens the settled timeline is BYTE-IDENTICAL to the declared `rest`
pose -- the port walks the keyframes with an authored time unit and arrives, to
the pixel, where the pinned decoders independently say the screen rests. On
`main_menu` the two differ in exactly one region, 400x470 at (440,108): the
bounding box of `ptframe1` and `ptframe2` and nothing else.

`rest` puts both at their first keyframe, off-position and transparent. The
capture of the running game shows them -- the bright circuit bracket around the
menu. Cropping the same region from the capture and from both renders puts the
ring and its elbow trace in the timeline render pixel-aligned with the game's,
and absent from the rest render. Geometry, so it does not depend on the
capture's gamma or on its having been taken with NEW GAME focused.

`ui_layout::rest_plateau` excludes a trailing run of identical keyframes because
it is normally the exit. On an element with NO exit animation the trailing run IS
the hold. The condition that identifies these exactly, with no false positives
here, is "the final untimed keyframe has the same pose as the last timed one" --
six elements, and `rest()` misses all six. Filed in BLOCKED.md for the RE agent:
the decoders are pinned and are not this port's to fix, and `sylpheed-cli screen
render` is missing the bracket too.

Worth saying plainly what this does to P1: the port and the reference agreed on
`main_menu` to 3/255 and BOTH were missing two elements the game draws. Two
renderers reading one field through one decoder agreeing is not evidence the
field is right. BLOCKED.md had already said that about the pivot; here it bit.

The title is NOT settled and P2 does not claim it. `rest` and the timeline
disagree there by 142-247/255, the only live title capture composites the PRESS A
plate over build 4 so it cannot be diffed against the title alone, and both of
the port's modes draw a cyan glow slab the game does not have -- a third problem,
P3's. Recorded as an open question rather than resolved by tuning.

Also reconciled against the RE agent's new work: Q8 is answered -- the SE waves
are located in `Static.slb` (move/confirm/back), which unblocks P6's audio; and
the title's transitions are a lookup by NAME, giving P3/P5 the game's own screen
vocabulary as candidate `goto` targets, marked as the name match it is.
This commit is contained in:
Sylpheed port agent
2026-08-28 19:51:57 +00:00
parent 0db45b4c66
commit 89de93b18f
5 changed files with 251 additions and 7 deletions

View File

@@ -9,7 +9,7 @@ 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-28**, at `/reborn` HEAD `bc6354d`.
Last reconciled against HANDOFF.md on **2026-08-28**, at `/reborn` HEAD `8b4965f`.
(`/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.)
@@ -17,7 +17,7 @@ mount is refreshed outside this container and HEAD is read, not fetched.)
| Milestone | Needs | HANDOFF | State |
|---|---|---|---|
| P6 audio | which cue fires on move / confirm / back | Q8 | ❔ open. The cue table is complete; the **event binding is not**. P6 cannot bind a sound to a keypress without inventing it. |
| ~~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. |
@@ -34,6 +34,18 @@ measured), Q9 (`ADVERTISE_MOVIE` → `ADV.wmv` is boot intro *and* attract; `MS0
`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:
@@ -45,9 +57,47 @@ than exported:
## Questions this port has raised
### Does a keyframe group loop, or hold its last pose?
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
**This one is a decoder bug, not a question**, and it is 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

View File

@@ -272,3 +272,122 @@ Both renderers skip them. The bundle declares them and carries **zero** RATC
children for either, so there is no texture on the disc to export — this is a
property of the disc, not a gap in the exporter, and `ScreenView` reports it as
`no sprite in the export` rather than dropping it silently.
---
## P2 — keyframe animation, 2026-08-28
### The time unit is authored, in one file, and says loudly that it is not on the disc
`authored/timing.json`. HANDOFF Q1 is answered — linear ramp, 2 units per
rendered frame, 1 unit = 1/60 s — but that conversion is **measured off the
running game**, not read from a file, which is exactly the case the
derived/authored split exists for. It is expressed as
`keyframe_units_per_second: 60` rather than seconds-per-unit so the value is
exact instead of a repeating decimal, and it carries the two independent lines
that support it. `t` stays raw everywhere in `export/`; seconds appear only
where this file is applied, which is one line of `boot.gd`.
`exit_ramp_seconds` is deliberately **null**. See below.
### The timeline stops at the last *timed* keyframe, and never plays the exit
The last keyframe of every group carries **no `t`** — the disc has no time slot
there. Across this export that final frame is an *exit* pose: for 116 of 134
elements it differs from the last timed keyframe **in alpha only** (a fade-out),
for 12 it is the loading splash's scale-and-slide exit, and for 6 it is
identical (no exit animation at all).
So the group is `pre-roll → ramp in → hold → [exit]`, and the port plays it up to
the hold and stops. Playing into the exit would mean **inventing how long the
ramp takes**, because the disc does not say. That duration is the screen
transition — HANDOFF Q7 measured it at ~0.4 s — and it belongs to P3, with its
own evidence. This is why `exit_ramp_seconds` is null rather than 0.4: P2 has no
business holding it.
### The interpolation is checked by where it lands, not by inspection
For **8 of the 12** screens the settled timeline is **byte-identical** to the
`--pose=rest` render. That is the useful assertion: the port walks the keyframes
with an authored time unit and arrives, to the pixel, at the pose the pinned
decoders independently identify as the resting one. `tools/screen-strip` reports
this per screen, so a change to the interpolation that drifts by one unit shows
up as a diff rather than as nothing.
The four that differ do so for two distinct reasons, below.
## `rest` misidentifies six elements, and the running game says so
On `main_menu`, the settled timeline and `rest` differ in exactly one region:
**400×470 at (440,108)** — the bounding box of `ptframe1` and `ptframe2`, and
nothing else on the screen.
`rest` puts both at their **first** keyframe: off-position and fully
transparent. The keyframes say they slide (620,108)→(440,108) and (403,267)→
(583,267) while fading 0x00→0xff, and then hold that pose for their last three
keyframes including the untimed one.
`/reborn/docs/re/captures/main-menu-oracle.png`, a capture of the running game,
**shows them**: the bright circuit-frame bracket around the menu, with a ring at
the bottom right. Cropping the same 250×180 region from the capture and from
both renders puts the ring and its elbow trace in the port's timeline render
**pixel-aligned with the game's**, and absent from the `rest` render. That is
geometry, not luminance, so it does not depend on the capture's gamma or on the
fact that it was taken with `NEW GAME` focused.
### Why the decoders get it wrong, precisely
`ui_layout::rest_plateau` excludes a run of identical keyframes that **ends the
group**, because that run is normally the exit — the comment cites the pause
menu, where taking the trailing run erased the word PAUSE. That exclusion is
right in general and wrong for an element with **no exit animation**, where the
trailing run *is* the hold. The rule then falls back to an earlier run, which
for a slide-in is the invisible pre-roll.
The condition that identifies the affected elements exactly, with no false
positives in this export, is:
> the final untimed keyframe has the **same pose** as the last timed keyframe
Six elements match it and `rest` misses all six: `ptframe1`/`ptframe2` on
`main_menu` and `main_menu_jp`, and `pteff02` on `title` and `title_jp`. This is
a **finding for the RE agent** about `sylpheed-formats`, not something this port
fixes: the decoders are pinned and must not be reimplemented here. The port
simply does not use `rest` — it derives the arrived pose from the keyframes,
which needs no heuristic — and `verify-screen` still asks for `--pose=rest` so
that renderer-vs-renderer diffing compares like with like.
Note what this says about P1: the port and the reference renderer **agreed** on
`main_menu` to 3/255, and both were missing two elements the game draws. Two
renderers reading the same field through the same decoder agreeing is not
evidence that the field is right. `docs/BLOCKED.md` had already said that about
the pivot; here it bit for real.
## The title is not settled, and P2 does not claim it
`title` and `title_jp` differ between the two modes by much more (max 142 and
247), and there the disagreement is **not** the six-element bug alone. `rest`
picks a mid-timeline hold for several glows (`pteff01`, `ptlogoall_eff`,
`ptlogoall_eff2`, `ptlogo_back2eff5`) where the timeline runs on to a much
brighter pose.
I could not settle which is right, and did not try to make the numbers agree:
* No element's alpha ever reverses direction anywhere in this export, so the
title's 4.48 s timeline is a slow one-way ramp, not a pulse — which removes the
obvious reason to expect a loop, but does not prove there is none.
* The only live title capture composites the **`PRESS Ⓐ` plate (build 2) over
the title (build 4)**, so it cannot be diffed against build 4 alone. Mean
luminance is oracle 64.1, `rest` 62.8, timeline 80.0 — which looks like it
favours `rest`, except that the plate *adds* brightness and `rest` is carrying
a 25 % black dim quad (`pteff02`) that is itself one of the six misidentified
elements. The comparison is confounded in both directions and settles nothing.
* **Both modes are visibly wrong anyway.** Side by side with the capture, the
port draws a washed-out cyan glow slab across the logo that the running game
does not have — in `rest` mode too. That is a third problem, independent of
this one, and it is P3's.
So: the timeline is the default because it is derived from the disc's own
keyframes with one measured constant and no heuristic, and because it is proven
right on the screen this milestone gates. On the title it is **unverified**, and
P3 should not assume P2 settled it.

View File

@@ -187,6 +187,22 @@ keyframes with an identical pose that does not end the group. Neither the first
nor the last keyframe, and not the longest-dwell frame either: a long gap after
keyframe *k* means the screen spends that time *arriving at* `k+1`.
> ⚠️ **`rest` is a heuristic over the keyframes, and it misfires.** The rule
> excludes a run that ends the group, because that run is usually the exit. On
> an element with **no exit animation** the trailing run *is* the hold, and the
> rule then falls back to an earlier run — usually the invisible pre-roll. Six
> elements in this export are affected, and the condition that identifies them
> exactly is *"the final untimed keyframe has the same pose as the last timed
> one"*: `ptframe1`/`ptframe2` on both main menus, and `pteff02` on both titles.
> A live capture of the running main menu shows `ptframe1`/`ptframe2` on screen;
> `rest` says they are invisible.
>
> A consumer that wants the pose after arrival should therefore take **the last
> timed keyframe**, not `rest`. `rest` is kept in the format because it is what
> the pinned decoders say and removing it would hide the disagreement — see
> `docs/DECISIONS.md`. The format is unchanged at **v2**: no field changed
> meaning, this is a warning about one of them.
**`paint_order`** is back-to-front, as declaration indices, and is a permutation
of them. It is the stable sort by `layer`. See `unresolved: paint_order_ties`.