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:
120
docs/re/structures/ui-keyframe-rotation.md
Normal file
120
docs/re/structures/ui-keyframe-rotation.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# ✅ The keyframe block's `+12` is a screen-plane ROTATION, in degrees
|
||||
|
||||
**Status:** ✅ `DECODED` — field, disc-wide census, and confirmed against the
|
||||
**framebuffer** (a GPU draw capture), not against our own renderer.
|
||||
🟡 `+4` and `+8` remain unexplained.
|
||||
|
||||
This closes the ⚠️ that stood on the `Keyframe` type: *"they do not explain the
|
||||
title screen — every element of `GP_TITLE` build 4 has all three at zero, while
|
||||
the game demonstrably submits rotated quads there."* That note was wrong about
|
||||
**reach**, not about the bytes it read.
|
||||
|
||||
## The block
|
||||
|
||||
```text
|
||||
+0 u32 ARGB fade colour
|
||||
+4 i32 🟡 unexplained
|
||||
+8 i32 🟡 unexplained
|
||||
+12 i32 ✅ rotation, DEGREES, clockwise-positive in screen space (Y down)
|
||||
+16 u32 scale X, percent
|
||||
+20 u32 scale Y, percent
|
||||
+24 u32 tint
|
||||
+28 i32 X ← signed
|
||||
+32 i32 Y ← signed
|
||||
+36 u32 time (the last block of a group does not own this word)
|
||||
```
|
||||
|
||||
## How it was confirmed: two quads, two different angles, measured
|
||||
|
||||
The live title submits one draw of **two rotated quads** that our renderer does
|
||||
not reproduce ([`title-draw-capture-vertex-colours.log`](../captures/title-builds/title-draw-capture-vertex-colours.log),
|
||||
draw 2). Converting its NDC vertices to screen space — `X = (x+1)·640`,
|
||||
`Y = (1−y)·360` — and taking the quads' **edge lengths** rather than their
|
||||
bounding boxes:
|
||||
|
||||
| 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) |
|
||||
|
||||
Both are true rectangles (adjacent edges perpendicular to < 0.6 %).
|
||||
|
||||
The two elements they belong to are the title's nested leaf records
|
||||
`ptloop01.rat` → `pteff03.t32` and `ptloop02.rat` → `pteff03a.t32`. Their
|
||||
keyframe blocks, at `0xbb5966+0x68` and `0xbb5a82+0x68` in the decompressed
|
||||
build 4:
|
||||
|
||||
| | `ptloop01` | `ptloop02` |
|
||||
|---|---|---|
|
||||
| sprite | `pteff03.t32` 399×180 | `pteff03a.t32` 399×180 |
|
||||
| scale | 100 %, **600 %** → 399 × **1080** | 100 %, **800 %** → 399 × **1440** |
|
||||
| `+12` | **30** | **−45** |
|
||||
| measured | 400.1 × 1076.3 at **+30.26°** | 400.2 × 1444.5 at **−45.28°** |
|
||||
|
||||
Magnitude *and* sign, on **two different values**, from the GPU.
|
||||
|
||||
### Why this is an identification and not a coincidence
|
||||
|
||||
The same capture was first attributed to a different element entirely
|
||||
([REFUTED.md](../REFUTED.md)), so the identification is carried by five
|
||||
independent agreements, not by elimination:
|
||||
|
||||
1. The draw has exactly **two** quads; the screen declares exactly **two**
|
||||
`ptloop` elements.
|
||||
2. Both widths measure **400** against a declared sprite width of **399**.
|
||||
3. The heights are **different numbers that both land**: 1076 ≈ 1080 (600 %) and
|
||||
1444 ≈ 1440 (800 %). A coincidence would have to hit both.
|
||||
4. Between the capture's two frames quad A moves **+19 px right** and quad B
|
||||
moves left — matching each record's own decoded sweep direction
|
||||
(`ptloop01` x: −639 → 1521, `ptloop02` x: 1721 → −839).
|
||||
5. The drawn vertex alphas `0xC3` / `0xB6` both fall inside the declared
|
||||
`0x80` → `0xff` alpha ramps.
|
||||
|
||||
### A second corroboration, by ramp shape rather than by angle
|
||||
|
||||
`GP_BUNK` entry `117ca14f` at `0x7395f3` holds a two-keyframe group in which
|
||||
`+12` ramps **0 → 360** while position, scale and alpha all stay constant. A
|
||||
full-turn ramp that changes nothing else is a spin in place; no non-rotation
|
||||
reading of the field explains it. Disc-wide, `360` occurs 1 173 times and `−360`
|
||||
180 times.
|
||||
|
||||
## Disc-wide census
|
||||
|
||||
[`tools/re-capture/kf_rotation_census.py`](../../../tools/re-capture/kf_rotation_census.py),
|
||||
output committed at [`data/kf-angle-census.txt`](../data/kf-angle-census.txt).
|
||||
Over **83 862** keyframe blocks in the `GP_*.pak` UI archives:
|
||||
|
||||
| word | non-zero | dominant values |
|
||||
|---|---|---|
|
||||
| `+4` | 3 990 (**4.76 %**) | `180` (3 880) |
|
||||
| `+8` | 3 878 (**4.62 %**) | `180` (3 102), `90` (201), `−180` (156) |
|
||||
| `+12` | 12 164 (**14.50 %**) | `90`, `−90`, `360`, `120`, `180`, and a long tail of arbitrary angles (`53`, `−58`, `−125`, `114`, `−33`, …) |
|
||||
|
||||
`+12`'s long tail of arbitrary values is itself part of the decode: `+4` and `+8`
|
||||
are almost entirely `±180`/`±90`, which is the signature of a **flip flag**
|
||||
expressed in degrees rather than of a free angle. 🟡 That is a hypothesis about
|
||||
`+4`/`+8`, not a decode — nothing observed turns on them.
|
||||
|
||||
## ⚠️ Reach and limits
|
||||
|
||||
* **`rotation_deg` is decoded but NOT rendered.** `ui_layout`'s blitter draws
|
||||
axis-aligned quads only, so `sylpheed-cli screen render` still paints a
|
||||
rotated element upright. Rotating the blit is a real change to the compositor
|
||||
and was not attempted here. **The title render's residual band is expected to
|
||||
persist until it is.**
|
||||
* 🟡 **Rotation appears to live only in nested `.rat` leaf records.** Every
|
||||
instance located so far — the two `ptloop` records, the `GP_BUNK` spin — is
|
||||
nested, and `screen info --geometry`'s new `r=` column is empty on every build
|
||||
of `GP_TITLE`, `GP_BUNK` and `GP_CHALLENGE` (the CLI lists the top-level
|
||||
declaration table only). **A full sweep of every build on the disc has not
|
||||
finished**, so this is a pattern across three archives, not a disc-wide
|
||||
negative. It is stated because it matters to a consumer either way: a composer
|
||||
that reads only the declaration table will see no rotation on the elements that
|
||||
actually rotate.
|
||||
* **The blocks are not 4-byte aligned.** A nested `RATC` blob can start at an odd
|
||||
offset (`ptloop01.rat` at `0xbb5966`), and its blocks inherit that. Any scanner
|
||||
over raw bundle bytes must not assume alignment — see
|
||||
[METHOD.md](../METHOD.md).
|
||||
* `+4` and `+8` are **not** claimed as X/Y rotation. Three adjacent signed
|
||||
degree-valued words invite that reading; only `+12` has been observed to do
|
||||
anything.
|
||||
Reference in New Issue
Block a user