The Decoder overturned the elimination I was most confident about. I ruled out
the ptloop sweeps because "399x180 at (441,270), keyframes hold position
constant". That is the PARENT's record. The geometry is in the leaf.
ptloop01 parent: scale (100,100) rot 0, fixed at (441,270)
LEAF: scale (100,600) rot +30, x sweeping -639 -> -39 -> 1521
ptloop02 parent: scale (100,100) rot 0, fixed at (441,270)
LEAF: scale (100,800) rot -45, x sweeping 1721 -> 1111 -> -839
Two ~1080 and ~1440 px quads leaning opposite ways and sweeping across the frame,
against two 400 px sprites drawn upright and static in the middle. That is
exactly the signature I measured -- darker centre-left, brighter right, nearly
cancelling -- and the GPU capture puts their centres at x ~ 467 and 992, the two
cells where my signed difference peaked.
`ui_layout`'s own doc comment said it: "the rotated quads come from its two
nested .rat leaf records, which the census never opened". Neither did this
exporter -- it opened a leaf in exactly one place, `highlight_name`, for focus
records.
IT IS NOT TWO ELEMENTS, IT IS 45: every button on every menu (the benign case,
where screen.rs already knew the leaf duplicates the parent and the parent wins),
the four loading screens' pgloading_loop*, and title_jp's ptlogo_eff2 -- which is
the element DECISIONS has recorded since P1 as the largest render disagreement in
the export, and which has a TWO-element leaf. A lead, not a conclusion.
EMITTED, DELIBERATELY NOT DRAWN. One `read_leaf` closure serves both the new path
and the focus path, because a second copy is how this would go missing again.
ScreenView ignores the data: parent and leaf each carry their own alpha ramp over
a different span (parent 0->255 over t=70..238, leaf 255->0x80->255 over
t=150..600), so how they compose is a decoding question, and drawing on a guess
would replace a visible 1.82% gap with an invisible wrong one. verify-screen
confirms nothing moved.
Additive blending is refuted -- the Decoder tested T8aD +0x04 bit 0x02 and "every
measure worsens", and the export carries no blend field because none has been
found (no RB_BLENDCONTROL in the per-draw capture). My hypothesis from last
iteration is dead.
This makes the port's biggest oracle gap the same item as the rotation question
already standing with the human: sylpheed-cli deliberately does not rotate, which
is why both renderers show it, and MISSION's "Needs a human decision -- rotation"
now has a number: 1.82% of the title's pixels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
50 lines
2.5 KiB
Rust
50 lines
2.5 KiB
Rust
//! Probe: does a `.rat` leaf record carry geometry the parent element does not?
|
||
//!
|
||
//! The GPU capture says the title submits `ptloop01`/`ptloop02` scaled 600 %/800 %
|
||
//! and rotated +30.26°/−45.28°, while the export writes scale 100 % and rotation
|
||
//! 0 for both. `ui_layout`'s own note says the rotated quads come from the
|
||
//! **nested `.rat` leaf records**, which is where `export_screen` already looks
|
||
//! for focus records and nowhere else.
|
||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||
|
||
fn main() {
|
||
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||
let ar = PakArchive::open(format!("{disc}/dat/GP_TITLE.pak")).expect("open");
|
||
let e = &ar.entries()[4]; // entry 4 = the English title
|
||
let bundle = ar.read(e).expect("read");
|
||
let b = ui_layout::parse_build(&bundle).expect("parse");
|
||
println!("build has {} elements, {} records", b.elements.len(), b.records.len());
|
||
let mut names: Vec<&String> = b.records.keys().collect();
|
||
names.sort();
|
||
println!("records: {names:?}");
|
||
for el in &b.elements {
|
||
if !el.name.starts_with("ptloop") { continue; }
|
||
let r = el.rest();
|
||
println!("\nPARENT {} -> rest scale {:?} rot {:?}", el.name,
|
||
r.map(|r| (r.scale_x, r.scale_y)), r.map(|r| r.rotation_deg));
|
||
if let Some(&(off, size)) = b.records.get(&el.name) {
|
||
match ui_layout::parse_build(&bundle[off..off + size]) {
|
||
Some(leaf) => {
|
||
println!(" LEAF {} parses: {} element(s)", el.name, leaf.elements.len());
|
||
for le in &leaf.elements {
|
||
let lr = le.rest();
|
||
println!(" {:<20} rest scale {:?} rot {:?} pos {:?}",
|
||
le.name,
|
||
lr.map(|r| (r.scale_x, r.scale_y)),
|
||
lr.map(|r| r.rotation_deg),
|
||
lr.map(|r| (r.x, r.y)));
|
||
for k in &le.keyframes {
|
||
println!(" t={:?} scale=({},{}) rot={} pos=({},{}) fade={:#010x} u4={} u8={}",
|
||
k.time, k.scale_x, k.scale_y, k.rotation_deg, k.x, k.y,
|
||
k.fade, k.unknown_4, k.unknown_8);
|
||
}
|
||
}
|
||
}
|
||
None => println!(" LEAF {} does NOT parse as a build", el.name),
|
||
}
|
||
} else {
|
||
println!(" no record named {}", el.name);
|
||
}
|
||
}
|
||
}
|