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:
48
crates/sylpheed-formats/examples/sweep_leaf_ramp.rs
Normal file
48
crates/sylpheed-formats/examples/sweep_leaf_ramp.rs
Normal 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!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Where the two rotated sweep strips actually are, per frame, from a draw log.
|
||||
"""Where the two rotated sweep strips are, per frame, and how their alpha ramps.
|
||||
|
||||
sweep_positions.py <xenia_re_ui_draws_NN.log> [...]
|
||||
|
||||
@@ -27,6 +27,7 @@ def main():
|
||||
for path in sys.argv[1:]:
|
||||
print("=== %s ===" % path)
|
||||
lines = open(path).read().splitlines()
|
||||
rows = []
|
||||
frame = 0
|
||||
print("%-6s %-5s %-6s %-18s %-18s %-8s %s"
|
||||
% ("frame", "draw", "quad", "NDC x range", "NDC y range", "col", "on screen?"))
|
||||
@@ -41,10 +42,35 @@ def main():
|
||||
for k, q in enumerate(quads(lines[i + 1])):
|
||||
xs = [p[0] for p in q]
|
||||
ys = [p[1] for p in q]
|
||||
rows.append((frame, m.group(1), k, xs, ys, q[0][2]))
|
||||
on = min(xs) < VIS_HI and max(xs) > VIS_LO and min(ys) < VIS_HI and max(ys) > VIS_LO
|
||||
print("%-6d %-5s %-6d %7.2f .. %7.2f %7.2f .. %7.2f %-8s %s"
|
||||
% (frame, m.group(1), k, min(xs), max(xs), min(ys), max(ys),
|
||||
q[0][2], "ON SCREEN" if on else "parked off screen"))
|
||||
# ── alpha vs position, pooled per strip ────────────────────────────
|
||||
# The disc declares the ramp in the ptloop01/ptloop02 LEAF keyframes:
|
||||
# pteff03 x -39 -> 1521 over t 150..540, alpha 128 -> 255
|
||||
# pteff03a x 1111 -> -839 over t 150..630, alpha 128 -> 255
|
||||
# which predict d(alpha)/dx of +0.0814 and -0.0651 per design pixel.
|
||||
# This measures the same slope off the GPU. NDC prints to two decimals,
|
||||
# so one frame's dx is quantised to 6.4 px and alpha to 1 level -- with
|
||||
# only a handful of frames the two predictions are INSIDE that noise and
|
||||
# this cannot separate them. It is a direction and a magnitude check.
|
||||
by_h = {}
|
||||
for f, d, q, xs, ys, col in rows:
|
||||
h = round((max(ys) - min(ys)) / 2 * 720)
|
||||
if h < 900:
|
||||
continue
|
||||
by_h.setdefault(h, []).append((min(xs), int(col[0:2], 16)))
|
||||
print("alpha vs position, per tall strip (design px, alpha level):")
|
||||
for h, pts in sorted(by_h.items()):
|
||||
pts.sort()
|
||||
span_x = (pts[-1][0] - pts[0][0]) * 640
|
||||
span_a = pts[-1][1] - pts[0][1]
|
||||
slope = span_a / span_x if span_x else float("nan")
|
||||
print(" h=%-5d n=%d x %.0f..%.0f px alpha %d..%d d(alpha)/dx %+.4f"
|
||||
% (h, len(pts), pts[0][0] * 640, pts[-1][0] * 640,
|
||||
pts[0][1], pts[-1][1], slope))
|
||||
print()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user