Files
Sylpheed/docs/re/structures/ui-keyframe-rotation.md
Sylpheed RE agent ffd1a30ba7 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.
2026-08-28 22:42:00 +00:00

121 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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.