re(ui): rotation is not nested-only, and the pivot-anchored scale is measured

Two corrections and one new confirmation, all from finishing the sweep I
left running last iteration.

Refuted, by my own sweep, within the hour: "rotation appears to live only
in nested .rat leaf records". That held for GP_TITLE, GP_BUNK and
GP_CHALLENGE -- the three archives the sweep had reached when I wrote it
-- and fails on GP_DIALOG and GP_DEBRIEFING_PILOTLOG, which rotate
top-level elements. Scoping the claim made it cheap to withdraw, but the
sentence should have waited for the sweep.

Those top-level cases are the best evidence on the disc, and they show up
in `screen info --geometry` without a hex dump. GP_DIALOG build 0:
pceff03/pceff04 ramp r = 90 -> 30 -> 10 -> 3 -> 0 while alpha ramps
0 -> 255 and they slide into place -- a swing-in settling upright. Build
6: pzeff02 ramps 43 -> 61 -> 75 -> 90 while scaling 112% -> 200% and
fading to 0 -- a spin-out. Rows committed as reference data.

New: the pivot-anchored scale term in blit, kf.x - pivot*(scale-100)/100,
was implemented and reasoned about but never measured, because every
element previously examined sits at 100% scale where the term is exactly
zero. The ptloop pair scale 600% and 800%, where it is worth 450 and 630
px. Formula predicts centre y = 360.0 for both; capture measures 359.1
and 360.0. Top-left anchoring predicts 810 and 990. Horizontally it makes
t-from-position agree with t-from-alpha to 0.33 / 0.65 units against ~8
without it.

That agreement does NOT prove linear interpolation -- both fields were
inverted through the same linear map, so a shared easing curve cancels.
It shows position and alpha ride one shared parameter. Recorded as such.

The disc-wide sweep is still running; the count is incomplete, the
existence is settled.
This commit is contained in:
Sylpheed RE agent
2026-08-28 22:46:44 +00:00
parent ffd1a30ba7
commit f5097779e4
5 changed files with 124 additions and 15 deletions

View File

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

View File

@@ -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·(scale100)/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 `<alpha>FFFFFF`, white RGB.
* "the swoosh discrepancy is undecodable" → **solved**: the game submits it as two

View File

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

View File

@@ -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·(scale100)/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