docs/re: the title-screen oracle — what it settled, and what it refuted

`BACKLOG.md`'s "the declaration table is not a paint order on every screen" had
been reasoned about but never measured. It is now measured, and the entry says
so both ways.

Confirmed: a framebuffer capture of Canary on the title screen shows the
`PROJECT SYLPHEED` wordmarks over the full-screen background, so declaration
order is not the paint order there.

Refuted, and recorded rather than quietly dropped:

* the `0x10`-adjacency first step this entry proposed. The background *is* next
  to a `kind = 0x10` `.prm` on both screens that have one — but on opposite
  sides (`GP_TITLE` puts the background after it, `GP_MISSION_SELECT` before),
  so no rule keyed on the `.prm` orders both. `GP_READY_ROOM` and `GP_OPTIONS`
  cannot serve as the third and fourth witnesses: neither carries a `.prm` or a
  full-screen background at all.
* the placement-region group order and the RATC child order, as above.
* reverse declaration order, by the same capture — it would draw the background
  over the copyright line, which is visible.

And separated out: part of what the bad render showed was not ordering at all
but the pivot/scale defect fixed two commits back. `ui-rat-layout.md` gains that
rule, the correlation evidence, the unmeasured pivot-vs-centre gap, and the
disc-wide demotion of "pivot is exactly half the texture" (49 % agree, 37 % are
off by more than 16 px — `ptcopyright` is 694 px wide in the framebuffer and
`pivot*2` says 618).

Captures: `title-screen-oracle.png` (the game surface cropped out of the
emulator window at (1,45), 1:1, bottom 45 rows clipped by the display) and
`title-screen-composite-2026-08-18.png` beside it.
This commit is contained in:
Sylpheed RE agent
2026-08-18 16:32:52 +00:00
parent 705d9a37ba
commit 2ac833bf75
2 changed files with 136 additions and 3 deletions

View File

@@ -58,8 +58,9 @@ out-of-range value anywhere in any bundle), and the tutorial bundle, which has n
The table lists **both** sprites and `.rat` records — it is the screen's element list.
`pgp_ttrl` declares 11: six `eff*`, `msg`, and four `.rat`s (`title`, `btn10..12`).
**Verified:** for all 7 `.t32` entries the declared pivot is *exactly* half the decoded
texture's dimensions — `eff10` 408×120 → 204,60; `eff21` 428×360 → 214,180; `msg` 381×38 →
✅ **Verified — but see the disc-wide correction at the end of this document, which
demotes this to a property of *this bundle*:** for all 7 `.t32` entries the declared
pivot is *exactly* half the decoded texture's dimensions — `eff10` 408×120 → 204,60; `eff21` 428×360 → 214,180; `msg` 381×38 →
190,19; and so on, 7/7 with no mismatch (`tools/re-capture/ratc_decls.py`).
`GP_PAUSE_MENU.pak`'s six bundles are `{in-mission, tutorial} × {English, Japanese}`, with
@@ -191,7 +192,11 @@ recording them because a static-only reading would have shipped them:
screen's draw list lives. **It is the bundle's own declaration table** (above): that
table is not the child listing — the children are grouped by type (every `.t32`, then
every `.rat`), while the declaration table names *elements*, in a plausible back-to-front
order, and it is exactly the missing set that appears there. For the in-mission pause
order, and it is exactly the missing set that appears there. **"Plausible" is
as far as that goes** — a framebuffer capture of the title screen refutes it on
that screen (its background is element 13 and it is drawn behind elements 05),
and no other ordering in the bundle replaces it; see
[`BACKLOG.md`](../BACKLOG.md). For the in-mission pause
menu it lists 23: `eff11/12/10`, `eff02(+a)`, `eff03(+a)`, `eff01`, `title.rat`,
`btn00/01/04/05.rat`, `msg`, `deli1…4`, `eff30…33`, `loop1.rat`. Note what it does
**not** list: the focused button variants, which are reached through each base record's
@@ -338,3 +343,82 @@ and that sprite exists **nowhere** — not as a pak member, not as a child of th
bundles. It is the second such dangling asset found, after
[`SUBTITLE_S12B.tbl`](../movie-subtitle-link.md); a reimplementation should skip
a missing sprite rather than treat it as a decode failure.
## The pivot is the scale centre — measured against the framebuffer (2026-08-18)
**Status:** ✅ `CONFIRMED` that a keyframe's scale grows the element **about its
declared pivot** rather than about the keyframe's own corner. 🟡 the pivot and
the sprite centre are *not* told apart by this measurement — see the scope below.
The compositor had been reading a keyframe as `top-left = (X,Y)` and
`size = decoded · scale`, ignoring the pivot entirely. That is right for every
element at 100 %, which is why the pause menu and the ARSENAL rulers above never
caught it, and wrong for every element that is scaled.
### The measurement
`GP_TITLE.pak` build 7, element 13:
```
ptbase2.t32 decoded 640x360 pivot (320,180) one keyframe: (320,180) 200%,200%
```
Composited from the corner that is a 1280×720 rect at 320..1600 × 180..900 — a
quarter-screen slab hanging off the bottom-right, with the top-left quadrant
bare. Anchored at the pivot it is `(320,180) (320,180)·(21) = (0,0)`, i.e.
exactly the screen.
The oracle is a framebuffer capture of Canary sitting on the title screen
([`captures/title-screen-oracle.png`](../captures/title-screen-oracle.png), the
game surface cropped out of the window at offset (1,45), 1:1, the bottom 45 rows
clipped by the display):
- the background art reaches all four edges, so the corner reading is refuted
outright;
- normalised cross-correlation of the composite's background against the capture,
searched over ±40 px in both axes, peaks at **(0,0)** — 0.90 on the planet limb,
0.72 on the lower-left ship. Not "close": the correct shift is the argmax.
Calibration for that crop is `ptcopyright.t32`, which is unscaled: 694×20 placed
at (293,655), and the capture's glyph run is x 295…986, y 700…718 — inside that
rect to the pixel once the 45 px of window chrome is taken off. So at 100 % the
keyframe really is the top-left, as this document already said.
Residual, stated rather than glossed: the composite is uniformly brighter than
the capture (about +14 in the dark starfield) and less blue in the planet. That
is **not** placement — it is the two full-screen `PRMD` elements (`pteff00.prm`,
`pteff02.prm` at fade-alpha ≈ 204) and `pteff04.t32` that the compositor does not
draw at all. Their geometry is known (a colour and four corners); their blend is
not.
### Scope, and what this does *not* settle
Swept over every screen build on the disc (`tests/ui_paint_order_disc.rs`):
| | count | of 5 130 resting placements with a decoded sprite |
|---|---|---|
| scaled ≠ 100 % — i.e. moved by this fix | **865** | 17 % |
| of those, where "about the pivot" and "about the sprite centre" differ by ≥2 px | **213** | 4 % |
`ptbase2`'s pivot **is** half its size, so the capture proves only that scaling is
not about the keyframe corner. Pivot-anchoring is chosen because that is the field
the format carries and this document already called it "a rotation/scale centre";
the 213 placements that could tell the two apart are **unmeasured**, and a capture
of any one of them mid-animation would settle it.
### Correction: "pivot = half the texture" does not generalise
The ✅ above — *"for all 7 `.t32` entries the declared pivot is exactly half the
decoded texture's dimensions, 7/7 with no mismatch"* — is a property of the
tutorial pause bundle, not of the disc. Across all 5 130 placements:
- **2 521 (49 %)** have `pivot·2` equal to the decoded size within 1 px;
- **1 884 (37 %)** are off by more than 16 px.
Some of that is already explained here — the `.rat` layer is inherited from the
Japanese master, so its baked pivot belongs to another language's sprite. But it
also happens on `.t32` declarations (`pgpeff02.t32` in the in-mission pause build:
decoded 265×198, declared pivot 19,18), so the rule is not merely a
language-inheritance artefact. **Do not derive a texture size from a pivot**; the
capture backs the decoded size, not the pivot — `ptcopyright` is 694 px wide in
the framebuffer, and `pivot·2` says 618.