From 33a15efa3c841c28ea165aaab97dd5e8cc453e10 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 14:05:19 +0000 Subject: [PATCH] handoff: the keyframe record layout, and the loading screens are named --- docs/port/HANDOFF.md | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 434c6928..460fb55e 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -23,6 +23,101 @@ There is no fourth kind. If a row says *measured* or *undecodable*, the port is human can see it is a human decision, so that when it is later decoded the authored version can be deleted. +## šŸ”“ 2026-08-29 — A KEYFRAME'S TIME COMES BEFORE ITS POSE. Change `pose_at`. + +**This is the one you said had a wide blast radius, and it is bigger than a +shift.** Read +[`ui-keyframe-record-layout.md`](../re/ui-keyframe-record-layout.md) before +touching `screen_view.gd`. + +A placement group is an 8-byte header `{u32 element_index, u32 frame_count}` +followed by `frame_count` records of **40 bytes**, each `{u32 time; 36-byte +pose}`. The time word **precedes** the pose it belongs to. Our parser's window +opened at the pose — four bytes into the record — and then read the word at its +`+36` as that pose's time, which is the *next* pose's. + +So neither of the two readings the corpus was arguing between was right: + +* the old default (`+36` is this pose's time) is off by one; +* `SYLPHEED_KF_TIME_SHIFT=1` had the association right but left **pose 0 + untimed**, because it never asked what the group's "lead-in word" was. It is + pose 0's time. + +`SYLPHEED_KF_TIME_SHIFT` is gone. `SYLPHEED_KF_TIME_LEGACY=1` restores the old +reading if you want to A/B. + +**Disc-wide, 33 archives, 13 991 groups, each test with a control:** + +| | corrected | control / old | +|---|---|---| +| lead-in prepended to the shifted times is non-decreasing | **13 991 / 13 991** | — | +| a non-zero lead-in is strictly below the next time (5 058 of them) | **5 058 / 5 058** | another group's lead-in: 70.9 % | +| multi-segment alpha ramp runs at a constant `dα/dt` | **857 / 1 540** | old reading: **0 / 1 042** | + +The last row is the one that cannot be argued with. Interpolation between +keyframes is linear; under the old reading **not one** multi-keyframe ramp on the +whole disc comes out at a constant rate. + +### What it changes for you, by the list you sent me + +* **`pose_at` (line ~184).** The comment *"a keyframe is the start of a ramp + toward the next"* is still true as a statement about ramps. What changes is + **which time each pose is at**: pose `k`'s time is the word before it. Every + screen's build-in timing moves. +* **The final pose now has a time.** Anything you authored to cover "the last + keyframe carries no time" — an exit ramp with no end, `exit_ramp_units = 24` — + can come out and be read instead. `exit_ramp_units` is now a decodable number, + not an authored one. +* **`settle_units` / `settle_time`.** `rest.t` still is not when a screen settles + (`5b0a6e6` stands), but its *value* moves. Re-derive it. +* **`spin_period_units`.** The focus ring's first keyframe's declared `t=120` is + now the time that pose is **reached**, not left. Check which end of the ring's + group you were reading. +* **The `PRESS ā’¶` plate.** `a=255` is at **`t=238`** — the corrected reading + agrees with the old default here, not with the old shift's 236. Your `5b0a6e6` + note is unaffected. +* **`ramp: "linear"` in `authored/timing.json` — KEEP IT.** Interpolation between + two keyframes is linear, and this work reinforces that rather than touching it. + āš ļø But 44 % of multi-segment ramps still are not constant-rate under the + corrected reading, and that is not a defect: authors shape a curve by placing + extra keyframes unevenly. Your `_lerp_pose` is right; do not add an easing + function. + +### And it costs nothing on the static composites + +This is the change the corpus previously declined to make. `SYLPHEED_KF_TIME_SHIFT=1` +moved `GP_TITLE` build 7 by 13.1 % of its pixels; with pose 0's time restored: + +* all **12** `GP_TITLE` builds render **byte-identical** PNGs under both readings; +* over **217** builds in six archives, exactly **two** elements pick a different + `rest()` pose — and both times the two candidates are equally invisible (α = 0), + so no render changes. + +So `screen render` is still the reference you diff against, unchanged. **If your +static screens move, that is your bug, not this change.** + +## āœ… 2026-08-29 — builds 0/1 and 10/11 are the LOADING SCREEN (your ask #2) + +**Decoded** — the authors' own element names, straight out of the declaration +table. Every element of all four bundles is prefixed `pgloading_`: the 7-element +pair (builds 0/1) is the plain plate, the 10-element pair (builds 10/11) adds +`pgloading_eff00.prm`, `pgloading_loop5.rat` and `pgloading_baseeff.t32` over a +circuit-line background. `DELTASABER / SYLPHEED A.I.` is the caption art on +`pgloading_str.t32`, not the screen's identity. + +āš ļø **Do not name them `LOADING` / `LOADING2` in an asset path.** The executable +does name five title-side screens — `TITLE_SCREEN`, `BUTTON`, `TITLE_MENU`, +`LOADING`, `LOADING2`, verified in the image at `sub_821C4EB0` — so there really +are two, but nothing observed says which bundle is which. 🟔 undecided. + +🟔 **Which member of each pair is English: the first half of the data segment.** +All eight pairs put exactly one member in each half of `GP_TITLE.p00`, and all +three pairs whose language is visible (4/7, 5/8, 6/9) put English in the first. +So builds **0, 2, 4, 5, 6, 10 are English**; 1, 3, 7, 8, 9, 11 Japanese. 🟔 not +āœ… — the three pairs this is *used* for are exactly the three no capture can +check, and nothing in the bundle bytes differs between those twins at all. +[`ui-title-build-map.md`](../re/ui-title-build-map.md) + ## āœ… 2026-08-29 — the disc is back in the decoder container; the red banner that stood here is withdrawn **This supersedes the "the decoder container has no disc" banner** written at