diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 4739232..a7fbd18 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -86,6 +86,18 @@ authored version can be deleted. affect this render**: `blit` sizes a sprite from its **texture**, and applies the pivot only as `kf.x − pivot·(scale−100)/100` — and **all seven swoosh elements are scale `(100,100)` at every keyframe**, so the term is zero. + ✅ **That formula is now MEASURED, not just implemented (2026-08-28).** The + title's two `ptloop` sweeps scale **600 %** and **800 %** vertically, where the + pivot term is worth 450 and 630 px, and the GPU capture puts both quad centres + at y **359.1** and **360.0** — against the formula's **360.0** for both. + Top-left anchoring predicts 810 and 990; treating the position as the centre + predicts 270. Horizontally the same term makes the group's `t` solved from + **position** agree with `t` solved from **vertex alpha** to **0.33 / 0.65 + units**, versus ~8 units without it. So: **anchor scaling on the pivot, not on + the top-left corner.** + 🟡 It does *not* prove interpolation is linear — both fields were inverted + through the same linear map, so a shared easing curve would cancel. It does + show position and alpha ride **one shared parameter**. ⚠️ It *would* bite `ptlogo1`/`ptlogo2`, which scale 100 → 150 during the build-in. Not at rest, and not on the swoosh. 🔴 **Ruled out for the COLOUR**: every swoosh keyframe's fade is `0x??ffffff` @@ -137,12 +149,18 @@ authored version can be deleted. constant — a spin in place. Disc-wide `+12` is non-zero in **14.50 %** of 83 862 keyframe blocks. ⚠️ **Two things the port must know about it.** - (1) 🟡 **Rotation appears to live only in NESTED `.rat` leaf records.** Every - instance found so far is nested, and the top-level table is empty of it across - `GP_TITLE`, `GP_BUNK` and `GP_CHALLENGE` — a pattern over three archives, not a - finished disc-wide sweep. Either way it is the actionable half: a composer that - reads only the declaration table will see zero rotation everywhere and be wrong - on exactly the elements that move. + (1) **Rotation lives in BOTH the top-level table and nested `.rat` leaf + records.** ⚠️ I told you one iteration ago that it looked nested-only; that was + three archives' worth of pattern and **it is refuted** — `GP_DIALOG` and + `GP_DEBRIEFING_PILOTLOG` rotate top-level elements. The actionable half stands: + the *title's* rotations are nested, so a composer reading only the declaration + table gets zero rotation on exactly the elements that move there. + The clearest examples are top-level and show up in + `screen info --build 0 --geometry dat/GP_DIALOG.pak`: `pceff03.t32` and + `pceff04.t32` ramp `r=` **90 → 30 → 10 → 3 → 0** while their alpha ramps + 0 → 255 and they slide into place — a swing-in that settles upright; and build + 6's `pzeff02.t32` ramps **43 → 61 → 75 → 90** while scaling 112 % → 200 % and + fading to 0 — a spin-out burst. (2) **`sylpheed-cli screen render` still does not rotate.** The field is decoded, 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 diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 29b9e7f..78c54f5 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -344,3 +344,17 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the `a=alpha r=rot°` matched the ` r=` pattern. A uniform count across heterogeneous inputs is the tell. Make the pattern require the *value* (` r=-?[0-9]+`), and sanity-check that a known-negative build reports zero. +* **A pattern over the archives you happened to check is not a negative.** I + wrote "rotation appears to live only in nested leaf records", correctly scoped + to the three paks I had swept — and the sweep refuted it on the fourth, within + the hour, because the alphabet had not reached `GP_DIALOG` yet. Scoping the + claim was what made it cheap to withdraw, but the better move is to let the + sweep finish before writing the sentence at all: a negative that is still + running is not a finding, it is a prediction. +* **A field that is implemented is not a field that is measured.** The + pivot-anchored scale term had been in `blit` for months and reasoned about in + the handoff, and no capture had ever exercised it, because every element anyone + had looked at was at 100 % scale where the term is identically zero. Look for + the case that makes a term *large* — here 600 % and 800 % scale, worth 450 px — + and check it there. A term you cannot distinguish from zero has not been + verified by any amount of agreement. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 1bab80b..0e49974 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -59,6 +59,17 @@ neighbourhood, not just the line. * "a keyframe-block scanner may assume 4-byte alignment" → refuted; it found **0/3** of its own control blocks and under-counted the corpus by 16 341 blocks. Nested `RATC` blobs start at odd offsets (`0xbb5966`). +* "keyframe rotation lives only in **nested** `.rat` leaf records" → **mine, and + refuted within the hour by my own sweep.** It held for the three archives I had + checked (`GP_TITLE`, `GP_BUNK`, `GP_CHALLENGE`) and failed on the next: + `GP_DIALOG` and `GP_DEBRIEFING_PILOTLOG` rotate **top-level** elements, and + those are the clearest examples on the disc. + [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) +* "the pivot-anchored scale formula `kf.x − pivot·(scale−100)/100` is our + renderer's reading, not a measurement" → **now measured.** At the `ptloop` + pair's 600 %/800 % scale it predicts both quad centres at y = 360.0 against a + captured 359.1/360.0, where top-left anchoring predicts 810/990. + [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md) * "the game passes a pink per-vertex colour for the title swoosh" → **refuted by draw capture.** Every vertex colour in the capture is `FFFFFF`, white RGB. * "the swoosh discrepancy is undecodable" → **solved**: the game submits it as two diff --git a/docs/re/data/rotation-top-level-examples.txt b/docs/re/data/rotation-top-level-examples.txt new file mode 100644 index 0000000..ea6d195 --- /dev/null +++ b/docs/re/data/rotation-top-level-examples.txt @@ -0,0 +1,7 @@ +# `sylpheed-cli screen info --geometry`, rows with a non-zero rotation (r=). +# GP_DIALOG builds 0 and 6; GP_DEBRIEFING_PILOTLOG build 10. 2026-08-28 + +4 pceff03.t32 247x281 38x38 NO 8: 630,348 100%,100% a=0 r=90 12: 550,273 100%,100% a=128 r=30 14: 510,235 100%,100% a=192 r=10 16: 490,217 100%,100% a=224 r=3 52: 470,198 100%,100% a=255 58: 470,198 100%,100% a=255 -: 470,198 100%,100% a=0 +5 pceff04.t32 257x311 478x586 NO 8: 413,77 100%,100% a=0 r=90 12: 483,167 100%,100% a=128 r=30 14: 518,212 100%,100% a=192 r=10 16: 536,234 100%,100% a=224 r=3 52: 553,247 100%,100% a=255 58: 553,247 100%,100% a=255 -: 553,247 100%,100% a=0 +1 pzeff02.t32 529x519 528x524 NO 197: 376,58 0%,0% a=0 217: 376,58 112%,112% a=255 r=43 232: 376,58 145%,145% a=64 r=61 237: 376,58 173%,173% a=32 r=75 -: 376,58 200%,200% a=0 r=90 +5 pjeff24a.t32 382x140 610x622 NO -: 335,49 210%,210% a=53 r=90 diff --git a/docs/re/structures/ui-keyframe-rotation.md b/docs/re/structures/ui-keyframe-rotation.md index 3838447..041b6b5 100644 --- a/docs/re/structures/ui-keyframe-rotation.md +++ b/docs/re/structures/ui-keyframe-rotation.md @@ -78,6 +78,65 @@ 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. +## ✅ The strongest evidence is `GP_DIALOG`, and it is visible in the CLI + +`sylpheed-cli screen info --build 0 --geometry dat/GP_DIALOG.pak`. These are +**top-level** elements, so no hex dump is needed to see them: + +| element | `+12` across the group | what else moves | +|---|---|---| +| `pceff03.t32` | **90 → 30 → 10 → 3 → 0** | alpha 0 → 128 → 192 → 224 → 255; position slides (630,348) → (470,198) | +| `pceff04.t32` | **90 → 30 → 10 → 3 → 0** | the same ramp, mirrored path (413,77) → (553,247) | +| `pzeff02.t32` (build 6) | **43 → 61 → 75 → 90** | scale 112 % → 200 %, alpha 255 → 0 | + +The first two are a swing-in that *decays to zero exactly as the element +settles*; the third is a burst that spins as it expands and fades. A monotone +decay to 0 synchronised with a fade-in, and a spin-out synchronised with an +expanding fade-out, are what a rotation field does and are not what any other +reading of the word would produce. + +`GP_DEBRIEFING_PILOTLOG` build 10 holds the static case: `pjeff24a.t32`, one +keyframe, `r=90` at 210 % scale. + +These four rows are committed verbatim at +[`data/rotation-top-level-examples.txt`](../data/rotation-top-level-examples.txt). +⚠️ The sweep that found them — every build of every `GP_*.pak`, grepped for a +non-zero `r=` — **had not finished** at the time of writing; it was in +`GP_DIALOG` build 37 with two archives hit so far. Treat the *count* as +incomplete and the *existence* as settled. + +## ✅ Bonus, from the same two quads: the pivot-anchored scale is measured-correct + +`ui_layout::blit` places a scaled element as +`ox = kf.x − pivot·(scale−100)/100`, keeping the **pivot** fixed rather than the +top-left. That was implemented and reasoned about, never measured — and until now +there was nothing to measure it against, because the title's other elements are +all at 100 % scale where the term vanishes. The `ptloop` pair scale **600 %** and +**800 %** vertically, so the term is worth 450 and 630 px: + +| element | measured centre Y | pivot-anchored | top-left | position-as-centre | +|---|---|---|---|---| +| `ptloop01` (600 %) | **359.1** | **360.0** ✅ | 810.0 | 270.0 | +| `ptloop02` (800 %) | **360.0** | **360.0** ✅ | 990.0 | 270.0 | + +Two different scales, both landing on 360.0. Horizontally the same term is worth +199.5 px, and there is an independent cross-check available: solve the group's +`t` from the quad's **position**, then from its **vertex alpha**, and see whether +the two agree. + +| element | | t from position | t from alpha | Δ | +|---|---|---|---|---| +| `ptloop01` | with the pivot term | 571.98 | 571.65 | **0.33** | +| | without | 579.65 | 571.65 | 8.00 | +| `ptloop02` | with the pivot term | 668.92 | 668.27 | **0.65** | +| | without | 659.71 | 668.27 | 8.55 | + +🟡 **What this does *not* show is that interpolation is linear.** Both fields +were inverted through the same linear map, so a shared easing curve would cancel +out and still agree. What it does show is that position and alpha are driven off +**one shared parameter** — which is worth knowing on its own, and is why the +agreement is a real check on the placement formula rather than a tautology. + ## Disc-wide census [`tools/re-capture/kf_rotation_census.py`](../../../tools/re-capture/kf_rotation_census.py), @@ -102,15 +161,15 @@ expressed in degrees rather than of a free angle. 🟡 That is a hypothesis abou 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. +* 🔴 ~~"Rotation appears to live only in nested `.rat` leaf records."~~ + **Refuted by my own sweep, within the hour.** It held for `GP_TITLE`, + `GP_BUNK` and `GP_CHALLENGE` — the three archives I had checked when I wrote + it — and fails on the very next one: `GP_DIALOG` and + `GP_DEBRIEFING_PILOTLOG` carry rotation on **top-level** elements, and those + are the best evidence on the disc (below). Rotation lives in both places. + The half that survives is the one that matters to a consumer: a composer that + reads only the declaration table will miss the *title's* rotations, because + those particular ones are nested. * **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