Files
Sylpheed/crates/sylpheed-formats/examples/ptloop_leaf_sweep_at.rs
sylph-decoder 59f3ac6846 re: ask #2 -- t=357.7 came from a GPU draw capture, and a sweep cannot date a frame
The port asked whether the refined sweep fit t=357.7 was measured against
live-title-build4-no-plate.png, because if so one of us is 42 units out.

It was not. 357.7 was solved against title-draw-capture-vertex-colours.log, a
GPU per-draw capture of the submitted vertex buffer -- four observables at once,
two quad centres and two vertex alphas. No framebuffer, no PNG.

The gap is not a fitting error either. Posing the leaves directly, t=400 misses
the captured quads by +169.0 and -172.2 px. Probe control: it reproduces the
page's published t=355 centres, 981 and 478, exactly.

Refutation attempted and FAILED: I expected the port's fit to be minimised by
the quad leaving the screen -- 'best fit' meaning 'draws least', the same shape
as the .tbm control that could not fail. At t=400 quad B is fully on screen and
quad A is 319 of 400 px. Their number is fitting something present and it
survives.

The real reason the two must differ is better than 'different frames'. The
sweeps are nested records on a free-running loop and their cycles differ -- 600
and 720, read from the record header +0x08 -- while the top-level clock stops at
settle. So two captures of the same settled title share a screen time and not a
sweep phase, by construction.

Consequence for the port: a sweep position does not date a frame; it gives a
phase on a 600- or 720-unit loop. And 357.7 is a joint fit over both leaves
while the port's ~400 poses one, so the two are not comparable in kind -- the
phases coincide only every LCM 3 600 units.

Discriminator handed to the port rather than taken: if its ~400 is pteff03 and
the frame is in the first cycle, pteff03a must be at centre 295 in that same
frame. The fit is against its renderer, so it owns the check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 07:15:45 +00:00

92 lines
4.0 KiB
Rust

//! Where are the title's two light-sweep quads at a given time — and is a
//! best-fit against a framebuffer PNG even measuring their position?
//!
//! `ui-leaf-vs-parent-alpha.md` solves the sweep instant as **t = 357.7** from a
//! GPU per-draw capture: quad centre x measured off the submitted vertex buffer,
//! which is a position measurement at 4 px/unit. The port agent separately
//! best-fits the same leaf against `live-title-build4-no-plate.png` and gets
//! **~400 units**, and asked whether the two used the same capture.
//!
//! Before comparing the numbers, check whether the second method can see what it
//! claims to measure. A fit that is minimised by the quad being OFF-SCREEN is
//! minimised by absence, and would return "best" at whatever time draws least —
//! the same shape as the `.tbm` control that could not fail.
//!
//! So: print the leaves' own x, alpha, and on-screen overlap across the window.
//!
//! cargo run -p sylpheed-formats --example ptloop_leaf_sweep_at
use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, ui_layout};
const SCREEN_W: i64 = 1280;
fn main() {
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
let by = ar.read(&ar.entries()[4]).expect("entry 4");
let b = ui_layout::parse_build(&by).expect("parse");
for parent in ["ptloop01", "ptloop02"] {
let Some(el) = b.elements.iter().find(|e| e.name.starts_with(parent)) else {
eprintln!("no element {parent}");
continue;
};
let Some(&(off, size)) = b.records.get(&el.name) else {
eprintln!("{}: no nested record", el.name);
continue;
};
// `ui-record-loop-length.md`: a nested record's header `+0x08` is its
// CYCLE LENGTH, and its keyframes need not fill it. This is why two
// captures of the same settled title do not share a sweep phase.
let loop_len = u32::from_be_bytes(by[off + 8..off + 12].try_into().unwrap());
println!("\n-- {} nested record: loop length (+0x08) = {loop_len}", el.name);
let Some(lb) = ui_layout::parse_build(&by[off..off + size]) else {
eprintln!("{}: leaf will not parse", el.name);
continue;
};
for le in &lb.elements {
let w = (le.pivot_x * 2) as i64;
println!(
"\n== {} -> leaf {} (sprite {:?}, pivot {}x{}, {} keyframes, last t={:?})",
el.name,
le.name,
le.sprite,
le.pivot_x,
le.pivot_y,
le.keyframes.len(),
le.keyframes.last().map(|k| k.time)
);
// ⚠️ A keyframe's `x` is the quad's LEFT edge, not its centre. The
// draw-capture fit is quoted in CENTRES, so compare `centre`, which is
// `x + pivot_x`. Printing `x` under a "centre" heading is how a
// 200-px offset gets into a comparison unnoticed.
println!(" t | x | centre | a | on-screen px of a {w}px-wide quad");
for t in [
340u32, 350, 355, 357, 358, 360, 370, 380, 390, 395, 400, 405, 410, 420, 440, 480,
540,
] {
let Some(k) = le.pose_at(t) else {
println!(" {t:>4} | (no pose)");
continue;
};
let x = k.x as i64;
let a = k.fade >> 24;
let l = x;
let r = l + w;
let vis = (r.min(SCREEN_W) - l.max(0)).max(0);
println!(
" {t:>4} | {x:>4} | {:>6} | {a:>3} | {vis:>5} px {}",
x + le.pivot_x as i64,
if vis == 0 {
"*** ENTIRELY OFF SCREEN ***"
} else {
""
}
);
}
}
}
}