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 d67c1da467
commit ee65631de8
4 changed files with 136 additions and 3 deletions

View File

@@ -51,6 +51,55 @@ screen, and the per-element visibility toggles isolate a suspect element.
otherwise — a screen whose background lands on top is showing you this bug, not
a decode failure.
### 2026-08-18 — measured against the running game; three orderings refuted, and half the symptom was a different bug
**The premise is confirmed by the oracle**, which this entry had not had: a
framebuffer capture of Canary on the title screen
([`captures/title-screen-oracle.png`](captures/title-screen-oracle.png)) shows
the `PROJECT SYLPHEED` wordmarks (elements 05) drawn **over** `ptbase2.t32`
(element 13), which is a full-screen background. Declaration order is therefore
not the paint order on this screen, and no reading of the element table changes
that.
**But part of what the render showed was not the paint order at all.** In the
capture `ptbase2` covers the whole screen; the compositor drew it as a
960×540-visible slab starting at (320,180), because a keyframe's `scale` was
being grown from the keyframe's corner instead of about the declared **pivot**.
Fixed, and pinned against the capture by cross-correlation (peak at (0,0)) — see
[`structures/ui-rat-layout.md`](structures/ui-rat-layout.md). That was a real
defect worth separating out: it moves **865** of the disc's 5 130 resting
placements, on every screen, independently of any ordering question.
**Three candidate orderings are now dead**, all cheaply:
- **The placement region is not a second ordering.** Its keyframe groups carry an
explicit element index, so they *could* be stored in a different order — they
are not, on **every** build on the disc (`placement_region_order_is_never_a_second_ordering`,
>500 builds, identity every time).
- **The RATC child order is not it either.** For the title build it is the
declaration order with the `.prm` elements absent — strictly less information,
and it has no place to put `ptbase2` other than where the table already puts it.
- **Reverse declaration order is refuted by the same capture**: it would draw
`ptbase2` (13) over `ptcopyright` (28), and the copyright line is visible.
**The `0x10`-adjacency first step was run, and it does not survive.** The
background *is* adjacent to a `kind = 0x10` `.prm` element on both screens that
have one — but on **opposite sides**. `GP_TITLE` build 7 is
`12 pteff00.prm (0x10)`, `13 ptbase2.t32`, `14 pteff04.t32`;
`GP_MISSION_SELECT` build 0 is `0 px_mission_base.tbm`, `1 px_mission_eff00.prm
(0x10)`. So "the `.prm` opens a layer that draws beneath what precedes it" cannot
place both, and no rule keyed on the `.prm`'s position orders the background.
`GP_READY_ROOM` and `GP_OPTIONS` turned out not to be the third and fourth
witnesses this entry hoped for: neither of their largest builds carries a `.prm`
or a full-screen background at all, so they cannot discriminate.
**What is still open, stated plainly:** nothing in the bundle has been found that
orders element 13 behind elements 05. Every ordering the file itself carries is
now either identical to the declaration table or refuted by the capture. The next
step is no longer static — it is either the guest code that walks this table, or a
per-draw capture of the title screen showing the order the game submits.
## Capital ships assemble wrong in the viewer
**Reported:** 2026-07-30, by the user. **Status:** ✅ **format-side cause found and

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

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.