This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/ui-title-build-map.md
Sylpheed RE agent 67fa1a1b0b 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

36 KiB
Raw Blame History

Which GP_TITLE build is which screen — measured against the running game

Status: CONFIRMED for the four screens the boot path actually shows (title art, PRESS Ⓐ BUTTON, main menu, EXTRAS); 🟡 PROBABLE for their Japanese twins; open for the two DELTASABER plates.

Answers MISSION Q2. The previous statement — "build 4 title, 5 main menu, 6/8/9 submenus" — is partly wrong and is withdrawn: build 8 is the Japanese main menu, not a submenu, and GP_TITLE holds exactly one submenu (EXTRAS), in two languages.

The archive is eight screens, each shipped twice

GP_TITLE.pak has 16 entries; sylpheed-cli screen list calls 12 of them composable builds. The 16 fall into eight pairs, and each pair's two members have near-identical sizes and name hashes that differ by a constant — one character of the name apart:

entry hash bytes build idx pair
0 01a2db9c 483 958 0 A
1 0ff0b8a8 483 958 1 A
2 285d8849 267 014 2 B
3 369773cb 267 014 3 B
4 a60fcb85 12 278 666 4 C
7 b483e6e6 13 363 328 7 C
5 a715f485 6 977 437 5 D
8 b58a0fe6 6 931 653 8 D
6 a81c1d85 6 549 126 6 E
9 b69038e6 6 548 438 9 E
10 cdba806e 426 473 F
13 db2ea1f8 423 333 F
11 cec0a96e 999 643 G
14 dc34caf8 999 643 G
12 cf2a8ccd 1 774 639 10 H
15 dd56d99b 1 774 639 11 H

For the three pairs whose members differ visibly — C, D, E — the difference is English versus Japanese: build 7 is the title art with the katakana subtitle プロジェクト シルフィード, build 8 the main menu reading 新規 / ロード / チュートリアル / オプション / エクストラ, build 9 the EXTRAS submenu as エクストラ. That is the whole of the language split we can see. In pairs A, B and H the two members render byte-identical PNGs — the archive still carries two copies, but the artwork does not change with the language.

Pairs F and G are the four entries screen list does not classify as builds by default. They are the developer splash, and they render — see below.

The map

Contact sheet of every render: captures/title-builds/title-build-contact-sheet.png.

build what it is confirmed how
0, 1 a DELTASABER / SYLPHEED A.I. plate low-left on black, no background not observed running. Never seen in the boot path, the main menu, EXTRAS or MISSION SELECT
2, 3 the PRESS Ⓐ BUTTON plate — an overlay build of its own, not a state of build 4 seen composited over build 4 on the live title, at the same rect our render puts it
4 title art, English (PROJECT SYLPHEED, ™, (C)2006,2007 SQUARE ENIX) live-title-press-a.png
7 the same, Japanese 🟡 renders as the JP twin of build 4; the container runs an English locale, so it was not seen
5 main menu, English: NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS, footer Ⓐ : OK live-main-menu.png — element for element
8 the same, Japanese 🟡 as above
6 the EXTRAS submenu, English: MISSION SELECT / MOVIE THEATER / BACK, footer Ⓐ : OK Ⓑ : Back live-extras.png
9 the same, Japanese 🟡 as above
10, 11 the same DELTASABER plate as build 0, over a dark circuit-line background not observed running

The splash — the four "non-build" entries, rendered

screen list --all widens the enumeration to every composable bundle and shows all 16 entries. The four that the default listing drops are the two halves of the developer splash, each shipped twice:

entry elements / sprites what it is
10, 13 3 / 2 the white SQUARE ENIX publisher logo
11, 14 7 / 6 GAME ARTS / SETA / studio anima — the developer logos

splash-entries-rendered.png. Rendered with screen render --all --primitives --black. Entry 11's seven elements are the three logos, their three _eff glows and the palogo_eff0.prm black backdrop — exactly the composition structures/ui-paint-order-key.md measured for the splash.

Confirmed against the running game — 2026-08-28

The 🟡 above ("no framebuffer capture to diff against") is closed. Recording the boot from the moment the window appears, at 2 fps, catches the splash before the movie: live-splash-publisher.png, live-splash-developer.png.

Edge-correlated against the renders, with the other half as a negative control:

capture vs entry 10 vs entry 11
publisher frame (t ≈ 2.0 s) 0.9146 @ (0,0) 0.027 ✗
developer frame (t ≈ 6.0 s) 0.033 ✗ 0.9792 @ (0,0)

Both halves match their own render at zero shift and are firmly rejected by the other. The 13/14 twins score 0.876 / 0.966 — near-identical artwork, so this test cannot tell a pair apart, only a screen from a different screen.

The splash's timing is DECODED, and the capture confirms it

Re-recorded at 10 fps (the 2 fps pass below was ±0.5 s and could not see ramps at all). The splash fades, both ways — it does not cut — and the bundle's own keyframes say so:

$ sylpheed-cli screen info --all --build 10 GP_TITLE.pak
1  palogo_sqex.t32       7 kf  [15  30  235 239 251 255  -]
2  palogo_sqex_eff.t32   4 kf  [15  30  45  -]              (the glow, child of 1)

$ ... --build 11
1  palogo_gamearts.t32   7 kf  [15  30  190 194 206 210  -]   (seta, anima identical)
2  palogo_gamearts_eff   4 kf  [15  30  45  -]

Under Q1's 1 unit = 1/60 s:

declared measured at 10 fps
SQUARE ENIX ramp in 15 → 30 = 0.25 s rise 0.4 → 0.8 s
SQUARE ENIX hold 30 → 235 = 3.42 s ≈ 3.5 s (0.8 → 4.3 s)
SQUARE ENIX fade out 235 → 255 = 0.33 s ≈ 0.3 s (4.4 → 4.7 s)
developer hold 30 → 190 = 2.67 s ≈ 2.4 s (5.6 → 8.0 s)
developer fade out 190 → 210 = 0.33 s ≈ 0.3 s (8.1 → 8.4 s)

The offset between declared and measured start is ≈ 0.35 s, which is simply that the recording's t = 0 is when the window appears, not when the guest starts drawing. Everything downstream of that lines up.

The overshoot is the glow. The measured rise peaks (6.21) at 0.8 s and settles back (5.39) by 1.1 s, which looks like a bloom. It is the _eff child element: its keyframes are 15 → 30 → 45, so it ramps in after the logo and then back down, while the logo itself holds. Decoded, not a rendering artifact.

So the port can read the splash's timing off the disc rather than author it — the first screen's animation is not a measurement it has to trust.

The wall-clock sequence, for orientation

SQUARE ENIX on screen ≈ 0.5 → 4.0 s after the window appears
black ≈ 4.5 s
GAME ARTS / SETA / studio anima ≈ 5.0 → 7.5 s
black ≈ 9.0 s
ADV.wmv begins ≈ 9.5 s

⚠️ The frames at ≈ 9.512.5 s show SQUARE ENIX again in cyan. That is not a third splash — it is the intro movie's own opening, which the milestone-2 notes describe as "white SQUARE ENIX + cyan glow + red diamonds". A capture-only reading would have recorded a third logo screen that does not exist.

So the first of the five screens now has a reference composite, a framebuffer capture, and its on-screen durations.

Reach of the two negatives. Builds 0/1 and 10/11 were looked for in: the whole boot sequence (a 5 s-cadence filmstrip from launch to the title, ~190 s), the title, the main menu, the EXTRAS submenu, the LOAD GAME slot list, the transition into MISSION SELECT (a 40-frame burst), and the attract cycle. They appear in none of them. The obvious remaining candidate is a long load — a mission launch — which is out of this objective's scope; they are most likely a loading/AI-chatter plate. That is a hypothesis, not a result.

Three things the captures settle beyond the map

The live title is two builds composited. Build 4 draws the art; build 2 draws PRESS Ⓐ BUTTON on top, and it fades in a beat later — a screenshot taken 2.5 s after arriving at the title has the art and no plate, one taken ~1 s later has both. The port must treat the plate as its own timed element.

The attract-loop title carries the plate too. After ~810 s idle the title fades to black, a full-motion video plays for ~85 s, and the title comes back — with PRESS Ⓐ BUTTON (live-attract-title-press-a-band.png). This is consistent with, and adds nothing to, the draw-quad comparison in canary-scripted-input-traps.md: the plate is not the tell that distinguishes the boot title from the attract title.

EXTRAS is the only main-menu destination inside GP_TITLE. Ⓐ on EXTRAS opens build 6 — measured. The other four destinations leave the archive: Ⓐ on LOAD GAME opened a LOAD GAME slot list, and Ⓐ on MISSION SELECT inside EXTRAS opened a MISSION SELECT screen, neither of which is a GP_TITLE build. dat/ carries GP_SAVE_LOAD.pak, GP_TUTORIAL.pak, GP_OPTIONS.pak, GP_MISSION_SELECT.pak and GP_MOVIE_THEATER.pak; that those are the archives behind the other four buttons is an inference from the names, not a measurement.

How to reproduce

sylpheed-cli screen list  "$SYLPHEED_DISC/dat/GP_TITLE.pak"
sylpheed-cli screen render --build 6 "$SYLPHEED_DISC/dat/GP_TITLE.pak" /tmp/b6.png
tools/re-capture/boot_menu.sh q2      # boots to the main menu, cursor on NEW GAME
tools/re-capture/pad.py dpad down 0.35 # x4 -> EXTRAS
tools/re-capture/pad.py tap A 0.30

Mind the d-pad hold — see METHOD.md.

The title is not a still image — it loops, ≈ 2.2 s

Recording the title at 10 fps for 22 s shows the screen never settles. After the build-in it oscillates by about ±0.5 in mean luminance, continuously:

peaks at   5.6  7.8  10.0  12.7  15.2  17.1  19.2  21.3 s
intervals  2.2  2.2   2.7   2.5   1.9   2.1   2.1
                                       mean 2.24 s

measured, n = 7 intervals, spread 1.92.7 s — peak-picking a low-amplitude signal is coarse, so read it as ≈ 2.2 s ± 0.4, not a precise period.

The mechanism is already decoded: build 4 declares ptloop01.rat / ptloop02.rat, and loop*.rat is a looping sprite animation rather than a composition (INDEX.md, UI screen layout row). So the title carries a looping element by construction; what is new is that it runs at ≈ 2.2 s and never stops.

The loop records ARE decoded — and they are not what I measured

ptloop01.rat is an opt -linked leaf record: in build 4 the chunk opt (size 0x0c) carries the name, immediately followed by a RATC blob at 0xbb5966 (ptloop02.rat the same at 0xbb5a82). Its pivot fields read 200/90, matching screen info's pivot (200,90) — the right blob.

The keyframes use the ordinary 40-byte block layout, starting at +0x68:

ptloop01pteff03.t32 ptloop02pteff03a.t32
kf0 t=150 x=639 α=ff t=150 x=1721 α=00
kf1 t=540 x=39 α=80 t=630 x=1111 α=80
kf2 t=600 x=1521 α=ff t=720 x=839 α=ff
scale 100 × 600 100 × 800
span 150 → 600 = 450 units = 7.5 s 150 → 720 = 570 units = 9.5 s

Y is constant at 270 and X runs off one edge to the other, so these are horizontal light sweepsloop01 left → right, loop02 right → left.

🔴 Two things I wrote last iteration are wrong

1. "A leaf record's keyframes are not in the build's 40-byte layout." Withdrawn — they are, exactly. The scan that "found nothing" demanded 29 strictly-increasing times because I read the word at +0x004 (0x001e0000) as a keyframe count. These records hold three keyframes. A filter that hard-codes the expected count rejects the right structure; whatever the 30 is, it is not the number of keyframes here.

2. "The ≈ 2.2 s oscillation is the ptloop elements." Withdrawn — I asserted the link because build 4 declares those elements, not because anything showed it. The decoded sweeps run 7.5 s and 9.5 s. A 22 s capture would show ~3 peaks from a 7.5 s cycle; it showed 8. So the loops are not what the oscillation measured.

Identified: it is the PRESS Ⓐ BUTTON plate pulsing

A per-tile amplitude map over the capture (8 × 6 grid, 18 s after build-in) localises the 2.3 s period precisely:

region sd dominant lag
band x ≈ 318954, y ≈ 560672 7.65 2.3 s
wordmark centre 0.06
bottom-right corner 0.003

That band is the PRESS Ⓐ BUTTON plate's rest position (ptbtn00.rat, rest (383,550), pivot (256,25)). It is build 2, composited over the title — not build 4.

Decoding ptbtn00f.rat, the plate's highlight variant, gives the pulse directly:

kf t alpha
0 6 0x00
1 29 0x06
2 35 0x4a
3 50 0x50
4 58 0x50 (hold)
5 97 0x4a
6 105 0x06

A glow that fades in to 0x50 and back out — exactly a "press start" pulse.

There is an eighth keyframe: fade 0x00ffffff at the same position — the glow returns to fully transparent, so this is a closed cycle, not a one-shot ramp.

🟡 The cycle length is still not readable — and now that is an observation. The eighth block's time slot contains the four bytes end , the record's ASCII terminator: the record simply stops there and the value does not exist. So the corpus's "a group's last block has no time of its own" rule holds here in a second form — not the next group's index, but the chunk terminator.

Declared span is ≥ 105 units = 1.75 s; measured ≈ 2.3 s (≈ 138 units), which would need a final step of ≈ 33 units. That 33 is fitted to the measurement, not read from the file, and is recorded only so nobody re-derives it as if it were a decode.

The word at +0x004 is not a keyframe count. It reads 0x003c0000 (60) here with 8 keyframes, and 0x001e0000 (30) in the loop records with 3. Whatever it is, it is not the count, and it is not decoded.

The declared 4.08 s build-in was not tested

That was the intent of this recording and it did not work. The title was reached by skipping the movie with Ⓐ, which cuts to black and brings the title up on a path that may not be the normal one; and the visible rise (≈ 2.7 s, from t ≈ 0.8 to 3.5) is a luminance curve, which screen-transitions.md already establishes is not the fade quad's ramp. So ≈ 2.7 s neither confirms nor contradicts build 4's declared 16 → 261 (4.08 s); the two are not measuring the same thing.

Testing it properly needs the title reached without a skip, and a way to separate the quad from the elements — neither of which this recording had.

For the port: build 4 alone, and groups HOLD rather than loop

Both answers the port agent asked for, in one recording.

The capture — the title with no PRESS Ⓐ plate over it

Ⓑ from the main menu returns to the title and the plate fades in a beat later, which opens a clean window. Recorded at 20 fps from the press:

title art appears t ≈ 1.10 s
builds in 1.10 → 3.70 s (mean 41.8 → 63.2)
settled, still no plate 3.70 → 5.00 s
plate arrives t ≈ 5.10 s (band jumps 282 → 3 755 bright px)

live-title-build4-no-plate.png is t = 4.0 s — build 4, settled, unobstructed. That is the reference for the washed-out cyan glow slab the port reports drawing and the game not having.

A keyframe group HOLDS at its last keyframe — it does not loop

This follows from the decoded sweeps plus a measurement, and the two agree:

  • ptloop01.rat's final keyframe puts pteff03.t32 at x = 1521 and ptloop02.rat's puts pteff03a.t32 at x = 839 — both off-screen on a 1280-wide design. A group that holds therefore parks both sweep sprites out of view and nothing moves after the build-in.
  • Measured: over 18 s of settled title the centre tiles sit at sd ≤ 0.01 (per-tile map above). A looping group would recross the screen every 7.5 s and be unmissable.

So the name loop*.rat is misleading — in this build the records animate once during the build-in and then rest off-screen. 🟡 This is about these groups on this screen; nothing here says no group anywhere loops.

The title's settled pose is rest, and the "washed-out slab" is a MISSING dim

The port agent could not decide whether rest or its played-out timeline is the title's arrived pose (they disagree by 142247/255), and separately reported a "washed-out cyan glow slab over the title logo that the running game doesn't have". The plate-free capture settles both.

sylpheed-cli screen render --build 4 --black, edge-correlated against live-title-build4-no-plate.png — a capture of the real screen, so this is an independent oracle and not one renderer checking another:

0.9163 at shift (0,0), and per band 0.92 / 0.78 / 0.93.

Geometry is right, so rest is the arrived pose for the title.

The slab is the 25 % dim, absent — not a glow, present

rest alone is uniformly too bright, and the excess leans cyan:

render mean (render capture) R G B
--black +13.14 +12.35 +13.58 +13.48
--black --primitives +0.55 +1.30 +0.89 0.54

Drawing the .prm primitives collapses the excess to nothing. The element is pteff02.prm, the 25 % dim quad (build 4, rest at t=46, fade 0x40 = 64 = 25 %) — and --primitives is off by default.

So the "washed-out cyan slab" is not something being drawn that shouldn't be. It is the dim that should be drawn and isn't: without it every pixel sits ~13 high, and because the title art is blue-dominant the shortfall reads as a cyan wash. ⚠️ Any consumer of screen render that omits --primitives on this screen gets it.

🟡 The residual: right on average, not right per pixel

With primitives the mean is essentially exact (+0.55) but pixel agreement is slightly worse — edge-correlation 0.9163 → 0.9066, and pixels differing by

20 rise 108 051 → 162 636. So the dim's average contribution is right while its application is not exactly the game's (blend mode or per-region alpha). Not diagnosed.

🔴 The real defect: the logo swoosh is drawn white and thick

With the dim in place, the residual is not uniform — it is a dark patch beside a bright one in one band:

row1 (y 112-225):  -2.0  +3.0  -8.8  -38.6  -17.6  +16.2  +33.8  +24.4

Cropping that band from capture and render (title-swoosh-capture-vs-render.png) shows it plainly: the game draws the logo's Z swoosh thin, with a pink/magenta edge; our render draws it thick and solid white. Too bright to its right, too dark where the game's thin stroke actually falls.

This — not the missing dim — is the port agent's "washed-out slab over the title logo". The dim explains a uniform +13; the slab is this.

The elements are ptlogo_back2.t32 (rest (71,126), pivot (500,117) — a 1000 × 234 diagonal), its glow ptlogo_back2eff.t32, and the five ptlogo_back2eff1…5 segments at y ≈ 117194 — exactly the band that disagrees.

🟡 A connection worth chasing, not a diagnosis. Those five segments are the group whose paint-order tie-break is the known unsolved residual (structures/ui-paint-order-key.md): they share key 0x8083, the game paints them 14,15,18,16,17, the stable sort paints 14,15,16,17,18, and the measured cost is "ptlogo_back2eff5 against eff3 (22 568 px) and against eff4 (32 395 px)". Same screen, same elements.

But a blend-order swap is a poor explanation for white instead of pink — that looks like a tint or blend-mode problem, so I would expect a second cause.

🟡 A quantified cause for the GEOMETRY: the pivot belongs to the other language

The fade and tint fields are not the culprit — every keyframe of every swoosh element carries 0x??ffffff, white RGB with only alpha varying, and no tint is anything but 0xffffffff. Nor is the texture pink: ptlogo_back2.t32 decodes blue-leaning (175, 174, 198) and its glow warm (255, 253, 234).

What is wrong is the pivot. Checking every GP_TITLE element whose texture we decode (109 of 178) against the rule pivot = texture ÷ 2:

exact 17
off by ≤ 1 px (rounding) 54
off by ≤ 8 px 14
off by > 8 px 24

and the gross ones are concentrated on the title's logo elements:

a60fcb85 ptlogo_back2.t32     pivot (500,117)  texture 1118x262 -> implies (559,131)   off 59.0
a60fcb85 ptlogo_back2eff.t32  pivot (507,126)  texture 1133x280 -> implies (566,140)   off 59.5
a60fcb85 ptlogo2.t32          pivot (449, 46)  texture  992x104 -> implies (496, 52)   off 47.0
b483e6e6 ptlogo1.t32          pivot (451, 50)  texture  822x100 -> implies (411, 50)   off 40.0

Why: build 4's ptlogo_back2 pivot (500,117) is exactly half of the Japanese texture (1000 × 234), not its own English one (1118 × 262). The layout record is authored once and shared while the .t32 sprites are swapped per language — the effect structures/ui-rat-layout.md warns about in general, here measured on the screen where the render disagrees with the game. Note it cuts both ways: the Japanese build's ptlogo1 is off by 40 px too.

A 59 px offset on a 1118 px sprite is the right order to produce "too thick and extending too far right", which is what the crop shows.

🔴 …and that candidate is refuted

Checked, as promised. ui_layout's blit takes the drawn size from the texture (img.width/img.height), and uses the pivot only for the scale anchor:

let ox = kf.x - (pivot_x as i32 * (sx_pct as i32 - 100)) / 100;

At sx_pct == 100 that term is zero. And every one of the seven swoosh elements is scale (100,100) at every keyframeptlogo_back2, ptlogo_back2eff and ptlogo_back2eff1…5 all report a single scale. So the pivot mismatch, real as it is in the data, cannot move or resize the swoosh in our render.

⚠️ It is not harmless everywhere: ptlogo1/ptlogo2 run scales 100 → 101 → 103 → 112 → 150 during the build-in, so there the wrong pivot does displace them — during the animation, not at rest.

🟡 So the swoosh defect is a BLEND problem, by elimination

Position and size are the texture's own and are right; fade is white-with-alpha; tint is white; the texture is blue-leaning, not pink. What remains is how the seven overlapping sprites are combined — ptlogo_back2 is only 5.4 % opaque, its glow 10.3 %, the five eff segments 1023 %, all white or warm. Stacked with plain alpha-over they saturate toward opaque white, which is what we draw and would read as "thicker" beside the game's thin coloured stroke. Not diagnosed — no blend mode has been identified in the data.

🔴 Refuted on the way

The obvious guess — our dim is applied over the whole frame instead of beneath the UI, where its layer key puts it — is wrong. If it were, the logo would render too dark; it reads +2.36 against a background of 0.74. The compositor honours the paint order here.

🔴 The swoosh is not displaced either — and the residual is restated

Two more candidates eliminated, and the residual is smaller than earlier sections implied.

Not a displacement. Shifting the render's swoosh band over ±80 px × ±8 px and re-correlating peaks sharply at (0, 0) — 0.7342, falling to 0.22 at ±24 px and 0.10 at ±48. The swoosh is where it should be.

Not additive blending. See structures/ui-paint-order-key.md: every measure worsens.

And the residual, restated with the current best render (--black --primitives, rest() fixed):

whole-frame mean diff +0.55
swoosh-band mean diff +1.83
swoosh-band edge-correlation 0.6971 (vs ≈ 0.92 frame-wide)

⚠️ Earlier sections quoted band tiles at +16 … +34. Those were measured on a render without --primitives. With the dim drawn the band's average is nearly right; what is wrong is its structure — the tiles run 38.6 then +33.8 across the band and cancel. So the defect is neither brightness, nor position, nor additive blending: it is a shape difference in one band, and it is not diagnosed. Six candidates eliminated: pivot (twice — inert at scale 100, and no measured displacement), fade, tint, texture colour, additive blend.

🔴 The capture IS settled — my own caveat, tested and withdrawn

The previous version of this section worried that the plate-free capture, at t ≈ 4.0 s, might be too early: elements have keyframes to t = 600 (10 s), and "settled" had been judged from mean luminance, which cannot see a thin sprite still moving.

The plate sits at y ≈ 550600, disjoint from the swoosh band at y 112225, so a late capture works even with the plate present. Correlating the render's band against the same band at several ages of the screen:

capture band edge-corr band mean
t = 4.0 s (plate-free) 0.7342 126.63
t = 6.0 s 0.7351 126.97
t = 12.0 s 0.7352 126.98
t = 18.0 s 0.7353 126.98
t = 21.5 s 0.7353 126.98

Identical to within 0.001 over 17.5 s. The band is settled by t = 4.0 s, the capture handed to the port agent is sound, and the caveat is withdrawn. It also corroborates that the groups hold: nothing crosses that band in 22 s.

The swoosh — SOLVED by draw capture (see below); the elimination trail kept

Seven candidates eliminated, none confirmed:

candidate verdict
element pivot (off by 59 px, authored for the other language) inert — blit sizes from the texture and applies the pivot only when scale ≠ 100; all seven elements are scale (100,100)
displacement none — shifting ±80 × ±8 px peaks sharply at (0,0), 0.734 → 0.22 at ±24 px
fade 0x??ffffff on every keyframe: white RGB, alpha only
tint 0xffffffff throughout
texture colour blue-leaning (175,174,198); the glow warm — neither pink
additive blend via T8aD +0x04 bit 0x02 refuted: every measure worsens
capture not settled refuted above

The residual is stable and modest: band mean +1.83, band edge-correlation 0.69710.735 against ≈ 0.92 frame-wide. Real, persistent, and not located in any field this project can read from the disc.

Where a next attempt should start, and it is not another field — but check what the tool actually records first, because the obvious phrasing of this is wrong.

⚠️ The per-draw capture does NOT record blend state. Reading command_processor.cc, each captured draw carries: primitive type, index count, index-buffer address, vertex- and pixel-shader ucode_data_hash, the pixel shader's texture bindings (base, dimensions, format), and vertex attribute 0 of binding 0. There is no RB_BLENDCONTROL / RB_COLORCONTROL dump. An earlier version of this section claimed the capture "reads the actual blend state"; it does not.

So the route splits:

  • Testable today, no code change — whether the game passes a vertex colour for those draws. The capture dumps vertex attributes, and a pink vertex colour would explain white-versus-pink directly.
  • Needs a Canary change — the blend mode itself, which means adding an RB_BLENDCONTROL dump to the same capture path.

Either way it is instrumentation of the running guest, not another field in the file.

SOLVED — the game draws the swoosh as ROTATED QUADS, which our blit cannot

The route named in the previous section was run: --ui_draw_capture_frames=3, armed with F10 on the settled title. title-draw-capture-vertex-colours.log is the capture, 22 draws over 3 frames.

🔴 First, the vertex-colour hypothesis dies

Every vertex colour in the entire capture is <alpha>FFFFFFwhite RGB, only the alpha varying: FFFFFFFF, C5FFFFFF, C3FFFFFF, B8FFFFFF, B6FFFFFF, 31FFFFFF, 1EFFFFFF. The game passes no colour. That was the eighth candidate.

And the ninth is the answer — it is the GEOMETRY

Draw 2 submits two parallelograms, neither axis-aligned:

quad corners (NDC) edge v0→v1 axis-aligned?
A (0.70,1.58) (1.24,1.02) (0.40,1.57) (0.14,1.02) (0.54, 0.56) no
B (1.29,1.02) (0.85,1.81) (0.75,1.02) (0.31,1.81) (0.44, 0.79) no

Both verified parallelograms (opposite edges equal to 0.01), both rotated — roughly 45° and 61° — and both extending to y = ±1.81, well beyond the screen. That is the diagonal Z stroke.

Our compositor cannot draw that. ui_layout::blit walks destination rows and columns of an axis-aligned rectangle (for row in 0..dh { for col in 0..dw), sampling the source by a straight ratio. It has no rotation. So the swoosh is blitted upright where the game draws it skewed — which is exactly the observed signature: right on average (+1.83), right in position (peak at (0,0)), wrong in structure (edge-corr 0.70), dark on one side of the true stroke and bright on the other.

And a real gap in the decoded format

The keyframe carries fade, scale_x, scale_y, tint, x, y, time (ui_layout.rs) — no rotation. So where the game's rotation comes from is not decoded: either a field not yet identified, or the element is positioned by code rather than by its keyframes. That is the open question this leaves.

⚠️ The "pink versus white" reading is now suspect. It was a visual comparison of two differently-shaped renderings. Whether any colour difference survives correct geometry is untested, and should be re-checked rather than carried forward as a separate defect.

🟡 Three angle fields found — and they are NOT the title's rotation

The obvious place to look for the rotation was the keyframe block's three words at +4, +8, +12, which ui_layout.rs documented as 0.

They are not zero. Over 72 287 keyframe blocks disc-wide:

word non-zero commonest values (signed)
+4 4.81 % 180, 180, 22, 90
+8 4.56 % 180, 90, 180, 178
+12 15.82 % 90, 90, 120, 58

Values clustering on ±180, ±90, 120 read as degrees, and three of them suggests rotation about three axes. 🟡 That reading is not tied to an observed rotation — 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.

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. 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.

⚠️ 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 diagonal element on the screen, so the inference is reasonable — but it was not confirmed by matching the draw's texture or screen position to that element, and should be.

🔴 The skewed draw is NOT the swoosh — my identification was wrong

Converting draw 2's quads from NDC to screen space settles it:

quad screen corners bbox
A (1088,209) (1434,7) (896,925) (550,727) x 550…1434, y 209…925
B (186,7) (96,292) (1120,727) (838,1012) x 186…1120, y 292…1012

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.

It is the two ptloop sweeps — confirmed

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: 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.

What this costs

Five iterations of swoosh work — the additive-blend test, the pivot analysis, the vertex-colour capture — were built on "the skewed draw is the swoosh", an identification made by elimination on one screen and never checked against the draw's own coordinates. The eliminations themselves stand (they were measured against the capture, not against the identification), but the chain of reasoning that pointed at ptlogo_back2* did not.