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.
5.7 KiB
✅ 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
+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,
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), so the identification is carried by five independent agreements, not by elimination:
- The draw has exactly two quads; the screen declares exactly two
ptloopelements. - Both widths measure 400 against a declared sprite width of 399.
- The heights are different numbers that both land: 1076 ≈ 1080 (600 %) and 1444 ≈ 1440 (800 %). A coincidence would have to hit both.
- 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
(
ptloop01x: −639 → 1521,ptloop02x: 1721 → −839). - The drawn vertex alphas
0xC3/0xB6both fall inside the declared0x80→0xffalpha 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,
output committed at 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_degis decoded but NOT rendered.ui_layout's blitter draws axis-aligned quads only, sosylpheed-cli screen renderstill 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
.ratleaf records. Every instance located so far — the twoptlooprecords, theGP_BUNKspin — is nested, andscreen info --geometry's newr=column is empty on every build ofGP_TITLE,GP_BUNKandGP_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
RATCblob can start at an odd offset (ptloop01.ratat0xbb5966), and its blocks inherit that. Any scanner over raw bundle bytes must not assume alignment — see METHOD.md. +4and+8are not claimed as X/Y rotation. Three adjacent signed degree-valued words invite that reading; only+12has been observed to do anything.