tools: the sweep leaf's declared ramp, and the slope measured off the GPU

sweep_leaf_ramp dumps the nested ptloop01/ptloop02 leaf keyframes -- position,
alpha, rotation, scale and time -- which is where the ramp the port asked for
actually lives.

sweep_positions now also pools alpha against position per strip and prints the
slope, with the quantisation stated: NDC prints to two decimals, so one frame's
dx is 6.4 px and alpha is one level, and at three or four frames the two
declared slopes (+0.0814 and -0.0651) are inside that noise. It is a direction
and magnitude check, not a discrimination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-31 07:10:35 +00:00
parent 035375f413
commit 86d76b3a26
2 changed files with 75 additions and 1 deletions

View File

@@ -0,0 +1,48 @@
//! The sweep strips' pose over their loop, straight off the disc — position and
//! alpha per keyframe of the nested `ptloop01`/`ptloop02` leaf records.
//!
//! `sylpheed-port` asks for "the sweep strips' vertex alpha as a function of
//! sweep position". The oracle gives that as scattered samples: three sessions,
//! ten frames, each one (x, alpha) at whatever phase the capture caught. If the
//! ramp is on the DISC, those samples become a check on a decode instead of the
//! whole answer.
//!
//! cargo run -p sylpheed-formats --example sweep_leaf_ramp -- GP_TITLE 5 6 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<String> = std::env::args().skip(1).collect();
let pak = argv.iter().find(|a| a.parse::<usize>().is_err())
.cloned().unwrap_or_else(|| "GP_TITLE".to_string());
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
let builds: Vec<usize> = argv.iter().filter_map(|a| a.parse().ok()).collect();
for e in if builds.is_empty() { vec![5usize, 6, 4] } else { builds } {
let Ok(by) = ar.read(&ar.entries()[e]) else { continue };
let Some(b) = ui_layout::parse_build(&by) else { continue };
for name in ["ptloop01.rat", "ptloop02.rat"] {
let Some(&(lo, ls)) = b.records.get(name) else { continue };
let leaf_bytes = &by[lo..(lo + ls).min(by.len())];
println!("=== {pak} entry {e}{name} (leaf {ls} bytes) ===");
if let Some(loop_units) = ui_layout::loop_length_units(leaf_bytes) {
println!(" declared loop length: {loop_units} units");
}
match ui_layout::parse_build(leaf_bytes) {
Some(lb) => {
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} sx={:<4} sy={:<4} rot={:<5} fade={:08X} (alpha {:3})",
k.time.map(|t| t.to_string()).unwrap_or_else(|| "-".into()),
k.x, k.y, k.scale_x, k.scale_y, k.rotation_deg,
k.fade, k.fade >> 24);
}
}
}
None => println!(" (leaf did not parse as a build)"),
}
println!();
}
}
}