P5's gate is "a human clicks through it". The artifact is a scripted walk that
proves the wiring rather than the intent -- up (wraps 01->05), five down, (A)
into EXTRAS, down, (B) back, landing on the main menu with focus RESTORED to
EXTRAS, ten PNGs one per settled step:
xvfb-run -a godot --path port -- --menu \
--script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5
--script posts InputEventAction through Input.parse_input_event so the presses
arrive at _unhandled_input exactly as a d-pad's would. Calling MenuFlow directly
would have been shorter and would have proved nothing: the wiring between a
press and the cursor is the part most likely to be broken, and a direct call is
exactly the part that skips it.
Derived vs authored, which P5 is the easiest place to blur:
* DERIVED -- the ORDER of the items, from each screen file's `buttons`, which
the exporter already fills from button-role elements sorted by resting Y.
* AUTHORED -- destinations, initial focus, what (B) does, and left/right being
a no-op. All measured off the running game (HANDOFF Q4/Q5) or chosen, none
on the disc, all in authored/flow.json with a why.
Four of five main-menu destinations are `goto: null` with a `blocked` note. That
is a MILESTONE BOUNDARY, not an unknown -- DIFFICULTY, the save list, the lesson
list and OPTIONS were all measured and live in archives this export does not
carry. `blocked` and `none` are kept apart so nobody later "discovers" the gap.
--headless CANNOT DRAW, and the port hung instead of saying so.
Measured, not assumed: under --headless Godot's dummy renderer never emits
RenderingServer.frame_post_draw, so every capture path awaited it forever --
--capture since P1, --film since P3, --shots as of now. With stdout block-
buffered the observable behaviour was SILENCE, FOREVER, which in a loop reads as
a job still working. Isolated by `--quit` (prints, exits 0) vs `--capture` (zero
bytes, killed at 40 s). Now those three flags refuse at STARTUP naming the
xvfb-run line that works, and --script no longer waits for a frame it is not
going to photograph -- so headless walks the menus in 4.5 s as a cheap
regression check needing no X server.
REFUTATION ATTEMPT, against the Decoder's 7eeae30 point 2 ("the oracle confirms
the game renders the ring's rotation"). Aimed there because PROTOCOL says to aim
at a claim the port is about to build on that rests on an estimator whose own
control the Decoder reported as +/-19.8 deg. IT SURVIVES, more strongly than
claimed.
Both captures draw the SAME sprite (ptbtneff01) 240 px apart, so "is it drawn
rotated" becomes "are these two crops one image at a different angle" -- no crop
offset needed and no reference to our own renderer. 360-bin angular luminance
profile over the annulus, circularly cross-correlated. Two controls first: known
rotations 0/30/90/150/210/270/330 recovered with 0 deg error, and a ring-free
patch of the same capture peaks at 0.369, so the estimator does not manufacture
matches. Then: A vs B 134 deg (corr 0.968), sprite vs A 76 deg, sprite vs B
210 deg -- and 210-76 = 134, which nothing in the method forced.
So 0 deg is NOT A POSE THE GAME SHOWS, and screen_view.gd draws the ring at
0 deg. That is now stated in the code as known-wrong rather than suspected. The
port did NOT start spinning it: the period has two unknowns and both are the
Decoder's -- the second keyframe is untimed, and "groups hold" predicts a stop
at 360 = 0 which contradicts both captures. Two frames of one focused button a
known time apart settle it. Filed in BLOCKED.md and asked over the channel.
BLOCKED.md's staleness check was half a check. It tested whether that page is
stale relative to HANDOFF; it cannot see the other direction, and the other
direction is what happened -- 7eeae30 lands 27 minutes AFTER HANDOFF was last
written and answers a question HANDOFF still lists as open. Added the missing
half: `git log --oneline 9ca1eb5..HEAD -- docs/re/`.
Also recorded, since the two were nearly confused: the ring's annulus centroid
lands within ~0.4 px of its design position under a ZERO crop offset, which
corroborates ORACLE-CAPTURES' "1279x675, top-left aligned" on a feature nobody
chose for the purpose. The earlier "text bands at design y + 23" is an offset
WITHIN the button sprite, not a crop offset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtmUw5N5LJaMW1Njb8Ziey
347 lines
22 KiB
Markdown
347 lines
22 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
|
||
|
||
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.
|
||
|
||
🔴 **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. |
|
||
| 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. |
|
||
| P5 focus marker | **the focus ring's spin PERIOD, and whether it loops** | Q1 + the 2026-08-28 *"groups hold"* answer | ❔ open, and the port is drawing a pose it knows is wrong. Derived at HANDOFF `9ca1eb5` **plus** `7eeae30`, which HANDOFF does not yet carry. `ptbtneff01` declares `t=120, rot 0` then an **untimed** `rot 360`. The port measured the two oracle captures at **~76°** and **~210°** — 134° apart, peak corr 0.968, null control 0.369 (`DECISIONS.md`) — so **0° is not a pose the game shows**, and the port draws 0° because a spin rate would be invented. Two unknowns, both the Decoder's: (a) under Q1's *replicated* reading `t=120` is when the **next** pose is reached, giving one revolution in 2.0 s, but this port's `pose_at` implements the other reading and switching it changes every screen's animation timing; (b) *"groups hold"* predicts a stop at 360 = 0, which contradicts both captures. **What settles it: two frames of one focused button a known time apart.** |
|
||
| 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. |
|
||
|
||
## 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).
|
||
|
||
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.
|
||
|
||
### 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.
|