From 531840b417c40706c8879d1769f0fd7e80d067fe Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Fri, 28 Aug 2026 22:56:17 +0000 Subject: [PATCH] re(ui): Q1 -- the interpolation law holds, the group timeline does not Q1's gate asks whether the ramp is linear. It is, and that result stands: it rests on the splash's _eff glows, which reproduce exactly. This adds the part that does not. The test is a calibration, not a fit. Fix the clock on palogo_gamearts_eff -- declared 15-unit fade-in 0@15 -> 255@30 against captured alphas 34,68,102,136,170,204,238, a constant step of 34, giving t = 2f - 171 -- then check that against the glow's own next landmark: its declared hold ends t=45, predicted frame 108.0, observed last full-alpha frame 107. Then apply it to palogo_gamearts in the same bundle and the same frames, with no free parameter left: declared a=232 at t=206 -> frame 188.5, observed alpha 255 declared a= 32 at t=210 -> frame 190.5, observed alpha 255 The logo is still at full alpha nine frames after it should read 32; its fade-out runs ~17 frames late; its declared 80-frame fade-in is never drawn. Not culling -- the same element is submitted down to a=7 on the way out. Calibration-free version: the declared fade-out spends 12 of 16 units dropping 23/255 of the alpha, and the capture has no such plateau. Candidate, offered and NOT adopted: if +36 held the NEXT keyframe's time, the fade-out shape fits (RMS 4.05 vs 12.13, two elements) and the decoder's "last block's time is unreadable" special case disappears -- the last block would simply have no successor. Rejected for now because it explains neither the missing fade-in nor the lateness, and because the _eff elements cannot discriminate between the readings at all (with four blocks the shift only relabels the phases). Decoder unchanged. Also withdrawn, mine, within the iteration: "the _eff glows hold a constant alpha 33". They ramp 34 -> 255 in steps of 34. I printed the series minimum and read it as its range, with a "14 distinct colours" column sitting next to it saying otherwise. --- docs/port/HANDOFF.md | 17 +++++- docs/re/METHOD.md | 14 +++++ docs/re/REFUTED.md | 9 +++ docs/re/data/splash-ramp-check.txt | 20 +++++++ docs/re/ui-keyframe-time-unit.md | 79 +++++++++++++++++++++++++++ tools/re-capture/splash_ramp_check.py | 70 ++++++++++++++++++++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 docs/re/data/splash-ramp-check.txt create mode 100644 tools/re-capture/splash_ramp_check.py diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index a7fbd184..f2f0657a 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -27,7 +27,7 @@ authored version can be deleted. | | Question | State | Answer / link | |---|---|---|---| -| Q1 | keyframe time unit + ramp shape | โœ… answered | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` โ€” settled**, the idle title presents at 28.5 fps so the game is 30 Hz โ€” [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) | +| Q1 | keyframe time unit + ramp shape | โœ… answered, ๐ŸŸก one gap | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` โ€” settled**, the idle title presents at 28.5 fps so the game is 30 Hz. ๐ŸŸก **The interpolation law is settled; the group TIMELINE for multi-keyframe elements is not** โ€” `palogo_gamearts` is still at full alpha 9 frames after its declared `a=32`, and its declared 80-frame fade-in never draws โ€” [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) | | Q2 | which build is which screen state | โœ… answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS โ’ถ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates โ€” [`ui-title-build-map.md`](../re/ui-title-build-map.md) | | Q3 | paint order for the six screens | โœ… answered | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title โ€” [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md) | | Q4 | button โ†’ GamePart | โœ… answered | **measured** which screen all **5** buttons open โ€” `NEW GAME` โ†’ `DIFFICULTY` โ†’ `SELECT DATA`, not a hang. The **GamePart id is still a name match**, not a measurement โ€” [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | @@ -249,6 +249,21 @@ authored version can be deleted. declared 15-unit fade lands on `round(255ยทk/15)` for all seven of its samples, with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames. **measured**, not decoded โ€” the disc says `t=30`, it does not say what a `t` is. + ๐ŸŸก **But a multi-keyframe element's TIMELINE does not reproduce (2026-08-28).** + The linear law and the 2-units-per-frame rate rest on the splash's `_eff` + glows, and those are exact. Applying *their* calibration โ€” with no free + parameter left โ€” to `palogo_gamearts` in the same bundle and the same frames: + the logo is still at `a=255` nine frames after its declared `a=32`, its + fade-out runs ~17 frames late, and its declared 80-frame fade-in is never + drawn at all (and that is not culling โ€” the same element is submitted down to + `a=7` on the way out). Its declared fade-out also spends 12 of 16 units + dropping only 23/255 of the alpha; the capture shows no such plateau. + A candidate โ€” that `+36` holds the *next* keyframe's time โ€” fixes the shape + (RMS 4.05 vs 12.13 on two elements) and removes the decoder's "last block's + time is unreadable" special case, but does **not** explain the missing fade-in + or the lateness, and the `_eff` elements cannot discriminate between the two + readings. **Not adopted; the decoder is unchanged.** Take the interpolation law + as settled and treat a multi-keyframe group's absolute timing as unverified. โœ… **The seconds conversion is settled: `1 unit = 1/60 s`, a 30 Hz title.** The re-test this page used to name has been run โ€” 300 submitted frames timed on the **idle** title (nothing loading) came out at **28.8 and 28.3 fps**, the same diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 78c54f5b..2a151e67 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -358,3 +358,17 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the 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. +* **Printing a series' minimum and reading it as its range.** I summarised a + captured alpha series as "constant ฮฑ โ‰ˆ 33" and built a contradiction on it โ€” + the summary printed `min_alpha` and no maximum, and the series actually ramps + 34 โ†’ 255 โ†’ 33. The tell was there in the same table: the column beside it said + *14 distinct colours*, which a constant series cannot have. When a summary + statistic and a distinct-value count disagree, the summary is wrong. +* **Calibrate on one element, test on another.** Fitting a declared ramp to a + capture has two free parameters (rate and offset) and will "succeed" against + almost anything โ€” my first attempt scored RMS 128/255 and I nearly read the + numbers rather than noticing the search could not reach the ramp at all. The + version that means something: fix the clock from element A's ramp, check that + fix against A's own next landmark, then apply it to element B in the same + frames with **nothing left to tune**. That is what turned "the shapes look + different" into "still at 255 nine frames after it should read 32". diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 0e499741..4ff01ba7 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -376,3 +376,12 @@ neighbourhood, not just the line. * "every IDXD string value is ASCII" โ†’ 6 non-ASCII values of 99 328. * "`ๆ–‡ๅญ—ๅˆ—` is a dev placeholder" โ†’ they are Shift-JIS **type words**. +* "the splash `_eff` glows hold a constant ฮฑ โ‰ˆ 33, contradicting their declared + 255 plateau" โ†’ **mine, and refuted within the iteration.** They ramp 34 โ†’ 255 + in exact steps of 34. I had printed the series' minimum and read it as its + range. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) +* "the declared keyframe timeline reproduces the captured splash" โ†’ **refuted for + multi-keyframe elements.** `palogo_gamearts` is still at `a=255` nine frames + after its declared `a=32`, and its declared 80-frame fade-in is never drawn. + The `_eff` glows do reproduce, exactly โ€” so this is about the group timeline, + not about the interpolation law. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) diff --git a/docs/re/data/splash-ramp-check.txt b/docs/re/data/splash-ramp-check.txt new file mode 100644 index 00000000..2cc517a6 --- /dev/null +++ b/docs/re/data/splash-ramp-check.txt @@ -0,0 +1,20 @@ +CONTROL โ€” the eff glow's fade-in must be linear at a constant step + alphas [34, 68, 102, 136, 170, 204, 238] steps [34, 34, 34, 34, 34, 34] + => 2.000 units/frame; t(f) = 2*f - 171 + check: declared hold ends t=45 -> predicted frame 108.0; observed last full-alpha frame 107 + +LOGO โ€” palogo_gamearts, checked against that calibration + observed: first drawn frame 116 at alpha 255; full alpha through 198; fade-out 199..211 + declared a= 0 at t= 15 -> frame 93.0 observed alpha NOT DRAWN + declared a= 0 at t= 30 -> frame 100.5 observed alpha NOT DRAWN + declared a=255 at t=190 -> frame 180.5 observed alpha 255 + declared a=255 at t=194 -> frame 182.5 observed alpha 255 + declared a=232 at t=206 -> frame 188.5 observed alpha 255 + declared a= 32 at t=210 -> frame 190.5 observed alpha 255 + +SHAPE, independent of any calibration: + declared fade-out spans t=194..210 (16 units); of that, + 12/16 units drop only 23/255 of the alpha (a near-flat leg), + 4/16 units drop 200/255 (a cliff). + captured fade-out: [255, 254, 243, 237, 215, 181, 148, 131, 98, 65, 48, 23, 15, 7] + per-frame drops: [1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8] -> no near-flat leg diff --git a/docs/re/ui-keyframe-time-unit.md b/docs/re/ui-keyframe-time-unit.md index ee7e72d1..209de251 100644 --- a/docs/re/ui-keyframe-time-unit.md +++ b/docs/re/ui-keyframe-time-unit.md @@ -161,3 +161,82 @@ the competing reading is ruled out. `_sqex_eff` absence is explainable (the capture joined the SQUARE ENIX logo mid-hold); the missing studio-anima logo is not, and is left as an open observation for Q2 rather than a claim. + +--- + +## ๐ŸŸก Open: the `palogo_*` LOGO elements do not play their declared timeline + +**Added 2026-08-28.** Everything above stands โ€” it rests on the `_eff` glows, and +they reproduce exactly. What follows is a **different element in the same bundle +and the same capture**, and it does not. + +Reproduce with +[`tools/re-capture/splash_ramp_check.py`](../../tools/re-capture/splash_ramp_check.py); +output committed at [`data/splash-ramp-check.txt`](data/splash-ramp-check.txt). + +### Why this is a test and not a fit + +The clock is calibrated on `palogo_gamearts_eff` โ€” its declared 15-unit fade-in +`0@15 โ†’ 255@30` against its captured alphas 34, 68, 102, 136, 170, 204, 238, a +constant step of exactly 34. That gives `t = 2f โˆ’ 171`, and the calibration +checks itself: the glow's declared hold ends at `t=45`, predicted **frame 108.0**, +and the observed last full-alpha frame is **107**. + +That calibration is then applied to `palogo_gamearts` โ€” same bundle, same frames, +**no free parameter left**: + +| declared | predicted frame | observed alpha | +|---|---|---| +| `a=0` at `t=15` | 93.0 | not drawn | +| `a=0` at `t=30` | 100.5 | not drawn | +| `a=255` at `t=190` | 180.5 | 255 | +| `a=232` at `t=206` | 188.5 | **255** | +| `a=32` at `t=210` | 190.5 | **255** | + +The logo is still at full alpha nine frames after it should have been at `a=32`. +Its fade-out actually runs frames **199โ€“211**, some 17 frames late, and its +declared 80-frame fade-in (`t=30โ†’190`) is **not drawn at all** โ€” the element's +first appearance, frame 116, is already at 255. + +โš ๏ธ "Not drawn" is not a culling artefact: the same element is submitted all the +way down to `a=7` on the way out, so low-alpha quads plainly do reach the GPU. + +### The shape mismatch, which needs no calibration at all + +The declared fade-out spends **12 of its 16 units** dropping only **23/255** of +the alpha โ€” a near-flat leg โ€” then **200/255** in the remaining 4. The captured +per-frame drops are `1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8`. There is no +near-flat leg. + +### ๐ŸŸก A candidate, offered as one and NOT adopted + +If the word at `+36` were the time of the **next** keyframe rather than of its own +block, the logo's fade-out would read `255@190 โ†’ 232@194 โ†’ 32@206 โ†’ 0@210`: +slow, fast, slow โ€” which is the captured shape. Fitting both readings to the +captured fade-out gives RMS alpha error **4.05** shifted against **12.13** as +decoded, on two elements independently (`gamearts` and `sqex`). + +It also removes a special case. The decoder currently notes that a group's data +"stops 4 bytes short of its final block's time slot", so the last block's time is +unreadable. Under the shifted reading the last block simply *has no successor*, +so it has no time word โ€” the same bytes, no special case. + +**Not adopted, for three reasons:** + +1. It does not explain the missing fade-in, which is the larger anomaly. +2. It does not fix the 17-frame lateness of the fade-out. +3. The `_eff` elements **do not discriminate** between the two readings โ€” I + checked: with four blocks the shift merely relabels which phase is which, and + both reproduce the observed fade-in / hold / fade-out. So the entire case for + the shift rests on one element's fade-out shape. + +**The decoder is unchanged.** ๐ŸŸก What a port should take from this: the +interpolation *law* is settled (linear, 2 units per frame), and the *group +timeline* for multi-keyframe elements is not. + +### ๐Ÿ”ด One thing I got wrong in the course of this + +I first reported the `_eff` glows as holding "a constant ฮฑ โ‰ˆ 33" and read that as +contradicting the declared 255 plateau. They do no such thing โ€” they ramp 34 โ†’ +255 in exact steps of 34. I had printed only the series' **minimum** and read it +as its range. Withdrawn; the trap is in [METHOD.md](METHOD.md). diff --git a/tools/re-capture/splash_ramp_check.py b/tools/re-capture/splash_ramp_check.py new file mode 100644 index 00000000..ef16c699 --- /dev/null +++ b/tools/re-capture/splash_ramp_check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Do the splash's DECLARED keyframe ramps reproduce the captured ones? + +Calibrate the animation clock on one element, then apply that calibration to a +DIFFERENT element in the same bundle and the same frames. No free parameter is +left for the second element, so it is a real test rather than a fit. + +The `_eff` glows calibrate it (they are the basis of Q1's "linear, 2 units per +frame" result). The `palogo_*` logos are then checked against it. + + splash_ramp_check.py docs/re/captures/ui-timing/splash-build-draws.log +""" +import csv, subprocess, sys, collections, tempfile, os + +LOG = sys.argv[1] if len(sys.argv) > 1 else \ + "docs/re/captures/ui-timing/splash-build-draws.log" + +here = os.path.dirname(os.path.abspath(__file__)) +csvf = tempfile.NamedTemporaryFile(suffix=".csv", delete=False).name +subprocess.run([sys.executable, os.path.join(here, "kf_time_probe.py"), LOG, + "--csv", csvf], check=True, capture_output=True) +series = collections.defaultdict(dict) +for r in csv.DictReader(open(csvf)): + if r["col"]: + series[(r["w"], r["h"])][int(r["frame"])] = int(r["col"][:2], 16) + +EFF, LOGO = ("525", "90"), ("499", "72") # gamearts_eff, gamearts + +# --- calibration, from the eff element's declared 15-unit fade-in 0@15 -> 255@30 +eff = series[EFF] +ramp = [(f, a) for f, a in sorted(eff.items()) if a < 255][:7] +steps = [ramp[i + 1][1] - ramp[i][1] for i in range(len(ramp) - 1)] +print("CONTROL โ€” the eff glow's fade-in must be linear at a constant step") +print(f" alphas {[a for _, a in ramp]} steps {steps}") +assert len(set(steps)) == 1, "eff fade-in is not a constant step; calibration void" +k_per_frame = steps[0] / (255 / 15) # alpha step -> units per frame +f0 = ramp[0][0] +t_at = lambda f: 15 + k_per_frame * (f - f0) + k_per_frame +print(f" => {k_per_frame:.3f} units/frame; t(f) = {k_per_frame:.0f}*f - " + f"{k_per_frame * f0 - 15 - k_per_frame:.0f}") + +# the calibration's own check: the eff's declared hold ends at t=45 +end_hold = max(f for f, a in eff.items() if a == 255) +print(f" check: declared hold ends t=45 -> predicted frame " + f"{f0 + (45 - 15) / k_per_frame - 1:.1f}; observed last full-alpha frame {end_hold}") + +# --- now the logo, with no freedom left +decl = [(15, 0), (30, 0), (190, 255), (194, 255), (206, 232), (210, 32)] +logo = series[LOGO] +first, last = min(logo), max(logo) +print("\nLOGO โ€” palogo_gamearts, checked against that calibration") +print(f" observed: first drawn frame {first} at alpha {logo[first]}; " + f"full alpha through {max(f for f,a in logo.items() if a==255)}; " + f"fade-out {first if logo[first]<255 else min(f for f,a in sorted(logo.items()) if a<255)}..{last}") +for t, a in decl: + fr = f0 + (t - 15) / k_per_frame - 1 + got = logo.get(round(fr)) + print(f" declared a={a:3d} at t={t:3d} -> frame {fr:6.1f} observed alpha " + f"{got if got is not None else 'NOT DRAWN'}") + +print("\nSHAPE, independent of any calibration:") +span = decl[-1][0] - decl[3][0] +print(f" declared fade-out spans t={decl[3][0]}..{decl[-1][0]} ({span} units); of that,") +print(f" {decl[4][0]-decl[3][0]}/{span} units drop only {decl[3][1]-decl[4][1]}/255 of the alpha (a near-flat leg),") +print(f" {decl[5][0]-decl[4][0]}/{span} units drop {decl[4][1]-decl[5][1]}/255 (a cliff).") +fo = [(f, a) for f, a in sorted(logo.items()) if f >= 198] +print(f" captured fade-out: {[a for _, a in fo]}") +d = [fo[i][1] - fo[i+1][1] for i in range(len(fo)-1)] +print(f" per-frame drops: {d} -> no near-flat leg") +os.unlink(csvf)