diff --git a/crates/sylpheed-formats/examples/leaf_keyframes.rs b/crates/sylpheed-formats/examples/leaf_keyframes.rs new file mode 100644 index 00000000..99bce905 --- /dev/null +++ b/crates/sylpheed-formats/examples/leaf_keyframes.rs @@ -0,0 +1,32 @@ +//! Dump the keyframes of any nested `.rat` leaf by name. +//! +//! cargo run -p sylpheed-formats --example leaf_keyframes -- GP_TITLE ptbtn00.rat 2 3 4 +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let argv: Vec = std::env::args().skip(1).collect(); + let pak = argv[0].clone(); + let rec = argv[1].clone(); + let builds: Vec = argv[2..].iter().filter_map(|a| a.parse().ok()).collect(); + let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak"); + for e in builds { + let Ok(by) = ar.read(&ar.entries()[e]) else { continue }; + let Some(b) = ui_layout::parse_build(&by) else { continue }; + let Some(&(lo, ls)) = b.records.get(rec.as_str()) else { continue }; + let leaf = &by[lo..(lo + ls).min(by.len())]; + println!("=== {pak} entry {e} — {rec} (leaf {ls} bytes) ==="); + if let Some(u) = ui_layout::loop_length_units(leaf) { println!(" declared loop: {u} units"); } + if let Some(lb) = ui_layout::parse_build(leaf) { + for el in &lb.elements { + println!(" {} — {} keyframes", el.name, el.keyframes.len()); + for (i, k) in el.keyframes.iter().enumerate() { + println!(" kf{i:<2} t={:<5} x={:<6} y={:<6} fade={:08X} (alpha {:3})", + k.time.map(|t| t.to_string()).unwrap_or_else(|| "-".into()), + k.x, k.y, k.fade, k.fade >> 24); + } + } + } + } +} diff --git a/docs/re/label-check-and-the-alpha-80-refutation.md b/docs/re/label-check-and-the-alpha-80-refutation.md new file mode 100644 index 00000000..469685ca --- /dev/null +++ b/docs/re/label-check-and-the-alpha-80-refutation.md @@ -0,0 +1,77 @@ +# A check that reads the label, not the citation — and one refutation it turned up + +**Question:** do the element labels in my F5/F6 findings still agree with the +declared data? + +**What the human looks at:** `python3 tools/re-capture/check_labels.py` — every +line PASS. Then `--selftest`, which must FAIL. + +**What this does NOT cover:** labels with **no** declared counterpart. Named +below; that gap is real and unclosed. + +## ✅ The check + +Three of my errors were the **label**, not the measurement — `0x3003` read as a +different role from `0x3002`, and `ptcopyright` called "the plate" twice, the +second time on a page written *after* I had corrected it. Every number was right. +A correction living in one document did not reach the next one I wrote. + +`sylpheed-port` built `check-authored-vs-declared` for values with a declared +counterpart, and named the gap it leaves: a value that exists only in a capture +and names an element rests entirely on my label. **This is that case.** Each +identification was originally *made* by matching a declared quantity, so each is +re-derivable: + +``` +[PASS] 1.1841 vs 1.2000 ( 1.3%) pteff03a is the 720-unit leaf, not a second 600 +[PASS] 1.3017 vs 1.3333 ( 2.4%) pteff03a is the sy=800 strip +[PASS] 0.0993 vs 0.1000 ( 0.7%) the pulsing slot has the declared 120-unit period +[PASS] 0.7500 vs 0.7333 ( 2.3%) the -0.54 quad is ptcopyright (22-unit ramp) +``` + +Both captures, eight checks, all pass. + +**The control is the point.** `--selftest` points the plate label at +`ptcopyright` — the exact error I made — and it must fail: + +``` +[FAIL] 0.0180 vs 0.1000 (82.0%) the pulsing slot has the declared 120-unit period +OK: mislabel detected (2 failures) +``` + +82–83 % against a 5 % tolerance, and the other three still pass, so the failure +is localised rather than a global break. + +## 🔴 Refutation — the port's "all five figures are also declared on the disc" + +Attempted against their audit, and it **lands for one of the five**. + +`ptbtn00f`'s **peak alpha of 80** is a figure I measured and sent them. The disc +does not declare it: + +* `ptbtn00.rat` **parent** (`GP_TITLE` entries 2 and 3): `t=0 α0 · 214 α0 · + 236 α255 · 238 α255 · 244 α0`; +* its **leaf**: declared loop **120 units**, one keyframe, **α255 flat**. + +**Nothing declares 80, and nothing in the declaration pulses at all.** The +*period* is declared and checks out; the *amplitude* is capture-only. So that +figure cannot be arbitrated by their check, and "all five declared" is wrong for +it — which matters because a check reporting PASS over a value it cannot see is +the failure mode their own note warns about. + +## 🟡 And it opens a fourth label question — not mine originally + +The corpus says *"the plate pulses via **`ptbtn00f`** on a declared 120-unit +loop"*. The 120-unit loop belongs to **`ptbtn00.rat`**, whose leaf contains +**`ptbtn00.t32`** — there is no `ptbtn00f` in it. `ptbtn00f` is a real element +elsewhere (the blend page has it at `0x0112`, additive), but the thing carrying +the declared 120-unit loop is not obviously it. + +Recorded as 🟡, not corrected: my check verifies the **period**, which is what the +identification rests on, and the period is right whichever name is attached. + +## Reach + +The check covers labels made by matching a declared quantity. A label with no +declared counterpart — the leaf **rate**, and now this **α80** — is still +unchecked, on my side as on theirs. diff --git a/tools/re-capture/check_labels.py b/tools/re-capture/check_labels.py new file mode 100755 index 00000000..9cd1e090 --- /dev/null +++ b/tools/re-capture/check_labels.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Re-derive every element LABEL in the F5/F6 findings from captures + disc. + +⚠️ WHY THIS EXISTS. Three of my errors were the label, not the measurement: +`0x3003` read as a different role from `0x3002`; `ptcopyright` called "the plate" +in the clock-conflict page; and `ptcopyright` called "the plate" AGAIN in the F6 +page, written after that correction. Every number was right. What it was pointed +at was not, and a correction in one document did not reach the next. + +sylpheed-port has `check-authored-vs-declared` for values with a declared +counterpart. It cannot cover a value that exists only in a capture and names an +element -- that one rests entirely on my label. This is that case. + +Each identification below was originally MADE by matching a declared quantity, so +each is re-derivable. If a label drifts, the agreement it was built on breaks. + + python3 check_labels.py # check + python3 check_labels.py --selftest # prove it can fail +""" +import sys, collections +sys.path.insert(0, __file__.rsplit('/', 1)[0]) +from read_draws import read + +CAPS = {'f6': '/sylph-home/re/f6/xenia_re_ui_draws_01.log', + 'f6b': '/sylph-home/re/f6b/xenia_re_ui_draws_01.log'} +SWEEP_PAGE = '8154' +PTCOPY_X = (-0.54, 0.54, 0.54, -0.54) + +def features(path, swap_plate=False): + fr = read(path); fs = sorted(fr) + f = {} + pairs = [] + for fm in fs: + adds = [q for q in fr[fm] if q.blend == '0x01010101' and q.page.startswith(SWEEP_PAGE)] + if len(adds) >= 2: pairs.append((fm, adds[0], adds[1])) + def cycles(idx, sign): + out = [] + for i in range(1, len(pairs)): + d = pairs[i][idx].cx - pairs[i-1][idx].cx + if (sign > 0 and d < -1.0) or (sign < 0 and d > 1.0): out.append(pairs[i][0]) + return out + ca, cb = cycles(1, +1), cycles(2, -1) + f['pteff03_period'] = ca[1] - ca[0] + f['pteff03a_period'] = cb[1] - cb[0] + import math + def length(q): + pts = [(x*640, y*360) for x, y in q.verts] + return max(math.dist(pts[i], pts[j]) for i in range(4) for j in range(i+1, 4)) + f['pteff03_len'] = sum(length(p[1]) for p in pairs) / len(pairs) + f['pteff03a_len'] = sum(length(p[2]) for p in pairs) / len(pairs) + # parent ramp: sweep quad A's alpha climbing to full from its cycle start + start = ca[0] + seq = [(fm, a.alpha) for fm, a, _ in pairs if fm >= start] + f['parent_ramp'] = next(fm for fm, al in seq if al >= 239) - start + # ptcopyright: fade-in length of the -0.54..0.54 quad + cop = [(fm, q.alpha) for fm in fs for q in fr[fm] + if tuple(round(v[0], 2) for v in q.verts) == PTCOPY_X] + f['ptcopyright_ramp'] = next(fm for fm, a in cop if a >= 255) - cop[0][0] + # ptbtn00f: the slot that comes and goes + slots = collections.defaultdict(list) + for fm in fs: + for q in fr[fm]: slots[(q.page[:4], round(q.cx, 2))].append(fm) + cands = [] + for k, v in slots.items(): + if len(v) < 200: continue + gaps = sum(1 for i in range(1, len(v)) if v[i]-v[i-1] > 1) + if gaps >= 8: cands.append((gaps, k, v)) + cands.sort(reverse=True) + if swap_plate and len(cands) > 0: # selftest: point the label at ptcopyright instead + f['pulse_period'] = f['ptcopyright_ramp'] + else: + v = cands[0][2] + ons = [v[0]] + [v[i] for i in range(1, len(v)) if v[i]-v[i-1] > 1] + per = sorted(ons[i]-ons[i-1] for i in range(1, len(ons))) + f['pulse_period'] = per[len(per)//2] + return f + +# label -> (derived ratio, declared value, tolerance, what the label asserts) +def checks(f): + return [ + ("pteff03a is the 720-unit leaf (not a second copy of the 600)", + f['pteff03a_period']/f['pteff03_period'], 720/600, 0.05), + ("pteff03a is the sy=800 strip, pteff03 the sy=600", + f['pteff03a_len']/f['pteff03_len'], 800/600, 0.06), + ("the pulsing slot is ptbtn00f (120-unit loop vs the sweep's 600 leaf units)", + f['pulse_period']/f['pteff03_period'], 0.1, 0.05), + ("the -0.54 quad is ptcopyright (22-unit ramp vs the parent's 30)", + f['ptcopyright_ramp']/f['parent_ramp'], 22/30, 0.08), + ] + +def run(swap=False): + bad = 0 + for name, path in CAPS.items(): + f = features(path, swap_plate=swap) + print(f" {name}:") + for label, got, want, tol in checks(f): + err = abs(got-want)/want + ok = err <= tol + bad += not ok + print(f" [{'PASS' if ok else 'FAIL'}] {got:.4f} vs {want:.4f} ({err*100:4.1f}%, tol {tol*100:.0f}%) {label}") + return bad + +if __name__ == '__main__': + if '--selftest' in sys.argv: + print("SELFTEST — the plate label deliberately pointed at ptcopyright.") + print("A check that cannot fail here would not have caught the real error.\n") + bad = run(swap=True) + print(f"\n{'OK: mislabel detected' if bad else 'BROKEN: mislabel NOT detected'} ({bad} failures)") + sys.exit(0 if bad else 1) + print("Element labels in the F5/F6 findings, re-derived from captures + disc:\n") + bad = run() + print(f"\n{'all labels agree' if not bad else str(bad)+' LABEL(S) DRIFTED'}") + sys.exit(1 if bad else 0)