re(ui): decode keyframe +12 as screen-plane rotation in degrees

The rotated quads on the title screen come from the keyframe block after
all. The earlier negative -- "every GP_TITLE build 4 element has all three
angle words at zero" -- read the right bytes over too small a region: it
walked the top-level declaration table, and the rotated elements are the
nested leaf records ptloop01.rat / ptloop02.rat.

Confirmed against the framebuffer rather than against our own renderer.
The two records declare +12 = 30 and -45; the GPU capture submits their
quads at +30.26 and -45.28 degrees -- magnitude and sign, two different
values. Corroborated by shape in GP_BUNK 117ca14f, where +12 ramps
0 -> 360 with position, scale and alpha constant: a spin in place.

Identifying which draw it was needed edge lengths, not bounding boxes:
400x1076 and 400x1444 against pteff03/pteff03a 399x180 at the elements'
two different declared scales, 600% (1080) and 800% (1440). The same test
names three known-positives in the capture (ptlogo1, ptcopyright,
ptbtn00), so it passes its own control.

Keyframe gains rotation_deg plus unknown_4/unknown_8, carried rather than
dropped. NOT rendered -- ui_layout::blit is axis-aligned only, so the
reference renderer and the port will both draw these upright until a
rotating blit exists.

The census tool ships with the trap that broke its first version: nested
RATC blobs are not 4-byte aligned, so an aligned scan found 0/3 of its
own control blocks and missed 16 341 blocks. Disc-wide +12 is non-zero in
14.50 % of 83 862 blocks.

sylpheed-formats tests, SYLPHEED_DISC set: 131 passed, 0 failed across the
6 suites finished at commit time; the run had not yet completed.
This commit is contained in:
Sylpheed RE agent
2026-08-28 22:41:32 +00:00
parent abe7477476
commit ffd1a30ba7
10 changed files with 430 additions and 42 deletions

View File

@@ -662,12 +662,15 @@ rotation about three axes. 🟡 That reading is **not tied to an observed rotati
it is the shape of the numbers, nothing more. The doc comment is corrected either
way: "0 on every frame seen" was a sample artefact.
### 🔴 But they do not explain this screen
### ✅ They *do* explain this screen — the earlier negative was wrong about reach
**Every element of `GP_TITLE` build 4 has all three at zero** — checked
element by element. The game still submits rotated parallelograms there. So the
title's rotation comes from **outside the keyframe data**, and remains
unidentified.
~~**Every element of `GP_TITLE` build 4 has all three at zero** — checked element
by element. So the title's rotation comes from outside the keyframe data.~~
**Withdrawn (2026-08-28).** That check walked build 4's **top-level declaration
table**. The rotated quads belong to its two **nested leaf records**, and there
`+12` reads **30** and **45** — against a measured **+30.26°** and **45.28°**.
The bytes were read correctly; the *region* was too small. See
[`structures/ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md).
⚠️ **One thing I should not have stated flatly:** that the skewed draw *is* the
swoosh. It is the only skewed geometry in the capture and the swoosh is the only
@@ -688,18 +691,43 @@ These span the **full screen height and well beyond it**. The swoosh
(`ptlogo_back2`, rest `(71,126)`, 1118 × 262) is a **band** at y 126…360. Draw 2
is not it.
🟡 **A candidate, offered as one:** the two `ptloop` sweeps. `pteff03a.t32` is
399 × 180 and its element carries scale **`(100,800)`** — 399 × 1440 — while
`pteff03` carries `(100,600)`. Two long thin sprites, two quads, and the drawn
vertex alphas `0xC3`/`0xB6` sit inside the `0x80``0xff` ramp those records
declare. **Not confirmed** — no texture or position match was made.
## ✅ It is the two `ptloop` sweeps — confirmed
⚠️ **If** that is right it has a consequence worth flagging: our renderer parks
those sweeps at their final keyframe (`x = 1521` and `839`, both off-screen) and
draws nothing, while the game draws them across the screen — which would mean the
draw capture caught them **mid-sweep**, inside the `t = 150…600` window, and that
the "groups hold" reading needs re-examining for these two elements specifically.
Conditional on an unconfirmed identification, so recorded and not acted on.
The bounding box was the wrong measurement; the quads are rotated, so what
identifies them is their **edge lengths**:
| quad | size | rotation | centre |
|---|---|---|---|
| A | 400.1 × 1076.3 | **+30.26°** | (992.0, 359.1) |
| B | 400.2 × 1444.5 | **45.28°** | (467.2, 360.0) |
| quad | element | sprite × declared scale |
|---|---|---|
| A | `ptloop01.rat` | `pteff03.t32` 399×180 @ 100 %,**600 %** = 399 × **1080** |
| B | `ptloop02.rat` | `pteff03a.t32` 399×180 @ 100 %,**800 %** = 399 × **1440** |
Five independent agreements, listed in
[`structures/ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md): the
count (two quads, two `ptloop` elements), both widths (400 vs 399), **both
heights, which are different numbers that both land**, the direction each quad
moves between the capture's two frames (matching each record's own sweep
direction), and the vertex alphas falling inside the declared ramps.
The known-positives in the same capture pass the same test: draw 5 measures
915 × 115 (`ptlogo1.t32` 919×113), draw 7 measures 691 × 18 (`ptcopyright.t32`
694×20) and 512 × 50 (`ptbtn00.t32` 513×50).
### ✅ And "a keyframe group holds" survives
The consequence I flagged conditionally last iteration resolves the *other* way.
Our renderer parks these two sweeps at their final keyframe (`x = 1521` and
`839`, both off-screen); the game draws them across the screen. That is not a
contradiction — the capture caught them **mid-sweep**: the quad centres, x = 992
and x = 467, both fall inside the decoded `t = 150…600` / `150…720` travel, the
alphas are mid-ramp, and the sprites move in the decoded direction between
frames. The capture is of the **build-in**, not of the settled screen, and the
18 s stillness measurement (sd ≤ 0.01) still says the groups stop. No change to
[the holds-not-loops finding](#-a-keyframe-group-holds-at-its-last-keyframe--it-does-not-loop).
### What this costs