# โœ… 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.