Compare commits
115 Commits
main
...
auto/re-ra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b5afeaa3 | ||
|
|
3491d30066 | ||
|
|
7eeae3006a | ||
|
|
d110cf38c7 | ||
|
|
b21c8e4118 | ||
|
|
9ca1eb50fd | ||
|
|
9a0ca0d71f | ||
|
|
f817dd5939 | ||
|
|
56cc7acfc3 | ||
|
|
49f109deb1 | ||
|
|
7a4e4333f4 | ||
|
|
2532c056be | ||
|
|
7988f52ce7 | ||
|
|
7156591654 | ||
|
|
2a086051b3 | ||
|
|
baed44a9ea | ||
|
|
7fb6bdfad8 | ||
|
|
1f2b469f7e | ||
|
|
458585d136 | ||
|
|
4012d5b555 | ||
|
|
515456c59a | ||
|
|
93e9b185ea | ||
|
|
0265da31a1 | ||
|
|
60285a6ead | ||
|
|
fb6376a548 | ||
|
|
0efd692b4c | ||
|
|
145046f88f | ||
|
|
6d9214fd07 | ||
|
|
714a26769d | ||
|
|
489ea12759 | ||
|
|
5f701db588 | ||
|
|
c463f98164 | ||
|
|
921a3cfc44 | ||
|
|
421c61a517 | ||
|
|
c6ce000002 | ||
|
|
93b0a6b20f | ||
|
|
3490fba9e3 | ||
|
|
aaaa08b164 | ||
|
|
7373035868 | ||
|
|
f7f9b555f6 | ||
|
|
ba47bdebe8 | ||
|
|
c3cf3c2e81 | ||
|
|
46006406a4 | ||
|
|
a3946e5005 | ||
|
|
e9924ff9e8 | ||
|
|
cc4e5e04a7 | ||
|
|
ee73de50be | ||
|
|
2b4ec20f00 | ||
|
|
7d07eb945f | ||
|
|
8783adda14 | ||
|
|
f10edf1e79 | ||
|
|
0f1c0f4e14 | ||
|
|
6d246c7a97 | ||
|
|
b973d8cde3 | ||
|
|
c2c69b02be | ||
|
|
b947fa8e8e | ||
|
|
6f23f4d113 | ||
|
|
531840b417 | ||
|
|
f11fde51e5 | ||
|
|
67fa1a1b0b | ||
|
|
130ad8d1b3 | ||
|
|
5115b4894a | ||
|
|
0ecb20bdbc | ||
|
|
6930d301c1 | ||
|
|
e10a6d6f2c | ||
|
|
5d02d5cd60 | ||
|
|
e0d02dce71 | ||
|
|
c10802536f | ||
|
|
2a0afc1405 | ||
|
|
e249b2e2ff | ||
|
|
cdb5a99def | ||
|
|
d217c07db2 | ||
|
|
5414db34bf | ||
|
|
4bc970688f | ||
|
|
ed4e5c7b37 | ||
|
|
1b95001feb | ||
|
|
c176fa53bc | ||
|
|
83293c3f81 | ||
|
|
3717293fa9 | ||
|
|
00ce0f33d0 | ||
|
|
a048910497 | ||
|
|
5f3d8f86a4 | ||
|
|
113c25aac1 | ||
|
|
9f39e8ee72 | ||
|
|
b302558d26 | ||
|
|
cc72f8b5a8 | ||
|
|
4830c63402 | ||
|
|
4380305921 | ||
|
|
86e52b01d0 | ||
|
|
a98e96b0f0 | ||
|
|
52275ddd53 | ||
|
|
6e92ae4597 | ||
|
|
c8c7677f6d | ||
|
|
8b4965f0a5 | ||
|
|
2be5ba7934 | ||
|
|
2b0e66b6ca | ||
|
|
b1b2576769 | ||
|
|
0d7199bc22 | ||
|
|
aea529e7dc | ||
|
|
dfcaac221e | ||
|
|
bc6354dce0 | ||
|
|
2c4005795b | ||
|
|
f5e7426cfa | ||
|
|
c760cdc509 | ||
|
|
c8e8dc0427 | ||
|
|
935f7ecab8 | ||
|
|
1f822f8b30 | ||
|
|
e81dcad632 | ||
|
|
e64965b3f9 | ||
|
|
b3c8632006 | ||
|
|
7fef19b3a0 | ||
|
|
4e745c8177 | ||
|
|
bebdb18a02 | ||
|
|
88b3ce9af5 | ||
|
|
27e8c51978 |
@@ -523,7 +523,7 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
|||||||
println!();
|
println!();
|
||||||
println!("geometry — decoded sprite size vs the declared pivot, and every keyframe");
|
println!("geometry — decoded sprite size vs the declared pivot, and every keyframe");
|
||||||
println!(
|
println!(
|
||||||
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=fade-alpha",
|
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=alpha r=rot°",
|
||||||
"#", "sprite", "decoded", "pivot*2", "same"
|
"#", "sprite", "decoded", "pivot*2", "same"
|
||||||
);
|
);
|
||||||
for el in &b.elements {
|
for el in &b.elements {
|
||||||
@@ -543,8 +543,13 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
|||||||
.keyframes
|
.keyframes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
|
let rot = if f.rotation_deg != 0 {
|
||||||
|
format!(" r={}", f.rotation_deg)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
format!(
|
format!(
|
||||||
"{}: {},{} {}%,{}% a={}",
|
"{}: {},{} {}%,{}% a={}{rot}",
|
||||||
f.time.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
f.time.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
||||||
f.x,
|
f.x,
|
||||||
f.y,
|
f.y,
|
||||||
|
|||||||
67
crates/sylpheed-formats/examples/decl_word_probe.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
//! Does any unread declaration word POINT at the element's T8aD child?
|
||||||
|
//!
|
||||||
|
//! `pteff05.t32`/`pteff04.t32` resolve to no sprite because the `T8aD` they want
|
||||||
|
//! is registered under the name `8AX`. Elimination says `8AX` is the one they
|
||||||
|
//! mean -- one unresolved element, one unclaimed non-focus-state child, in 6 of
|
||||||
|
//! 6 title-side builds. Elimination is not a pointer, so: the 60-byte
|
||||||
|
//! declaration reads name[0..28], parent@32, kind@40, pivot@48/52. The words at
|
||||||
|
//! +28, +36, +44 and +56 are unread. If one of them indexes the RATC child
|
||||||
|
//! table, the RESOLVED elements are the control -- their child index is known,
|
||||||
|
//! so a candidate field must reproduce it for them before it may be believed for
|
||||||
|
//! the unresolved one.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example decl_word_probe -- <pak> [entry]
|
||||||
|
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||||
|
|
||||||
|
const OFFS: [usize; 4] = [28, 36, 44, 56];
|
||||||
|
|
||||||
|
fn be32(b: &[u8], o: usize) -> u32 {
|
||||||
|
if o + 4 > b.len() { return 0; }
|
||||||
|
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = std::env::args().nth(1).expect("usage: decl_word_probe <pak> [entry]");
|
||||||
|
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||||
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
// Per candidate offset, across every build: control hits / control total.
|
||||||
|
let mut hit = [0usize; 4];
|
||||||
|
let mut tot = 0usize;
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
if want.is_some_and(|w| w != i) { continue; }
|
||||||
|
let Ok(bytes) = ar.read(e) else { continue };
|
||||||
|
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||||
|
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||||
|
// Index space to test against: the T8aD children, in child order.
|
||||||
|
let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect();
|
||||||
|
if build.elements.iter().all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("== entry {i} ({} elements, {} T8aD children)", build.elements.len(), t8.len());
|
||||||
|
for (n, c) in t8.iter().enumerate() { println!(" child[{n:2}] {}", c.name); }
|
||||||
|
for el in &build.elements {
|
||||||
|
if el.kind & 0x10 != 0 { continue; }
|
||||||
|
let d = &bytes[0x20 + el.index * 60..0x20 + (el.index + 1) * 60];
|
||||||
|
let words: Vec<u32> = OFFS.iter().map(|&o| be32(d, o)).collect();
|
||||||
|
// The control: for a RESOLVED element, which T8aD child is it?
|
||||||
|
let truth = el.sprite.as_ref()
|
||||||
|
.and_then(|s| t8.iter().position(|c| &c.name == s));
|
||||||
|
if let Some(t) = truth {
|
||||||
|
tot += 1;
|
||||||
|
for (k, w) in words.iter().enumerate() {
|
||||||
|
if *w as usize == t { hit[k] += 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" [{:2}] {:26} sprite={:?} child={:?} +28={} +36={} +44={} +56={}",
|
||||||
|
el.index, el.name, el.sprite, truth,
|
||||||
|
words[0] as i32, words[1] as i32, words[2] as i32, words[3] as i32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\nCONTROL: resolved elements whose child index a word reproduces, of {tot}:");
|
||||||
|
for (k, o) in OFFS.iter().enumerate() {
|
||||||
|
println!(" +{o:<3} {:3}/{tot}", hit[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
crates/sylpheed-formats/examples/name_resolution.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//! Why does an element's sprite fail to resolve? Dump the two name spaces.
|
||||||
|
//!
|
||||||
|
//! `parse_build` resolves an element to a sprite by looking its DECLARED name up
|
||||||
|
//! in (a) the `.rat` record table, then (b) the `T8aD` child table. `pteff05.t32`
|
||||||
|
//! is in neither -- the `T8aD` it wants is registered as `8AX` -- so it resolves
|
||||||
|
//! to None and `compose` drops it without recording it as missing. This prints
|
||||||
|
//! both spaces, so the link between the two can be CHECKED rather than assumed.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example name_resolution -- <pak> <entry>
|
||||||
|
use sylpheed_formats::{pak, ui_layout};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = std::env::args().nth(1).expect("usage: name_resolution <pak> [entry]");
|
||||||
|
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||||
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
if want.is_some_and(|w| w != i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(bytes) = ar.read(e) else { continue };
|
||||||
|
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||||
|
let unresolved: Vec<&ui_layout::Element> = build
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.filter(|el| el.sprite.is_none() && el.kind & 0x10 == 0)
|
||||||
|
.collect();
|
||||||
|
let claimed: std::collections::HashSet<&str> =
|
||||||
|
build.elements.iter().filter_map(|e| e.sprite.as_deref()).collect();
|
||||||
|
let unclaimed: Vec<&String> =
|
||||||
|
build.sprites.keys().filter(|k| !claimed.contains(k.as_str())).collect();
|
||||||
|
if want.is_none() && unresolved.is_empty() && unclaimed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let un: Vec<&str> = unresolved.iter().map(|e| e.name.as_str()).collect();
|
||||||
|
let uc: Vec<String> = unclaimed
|
||||||
|
.iter()
|
||||||
|
.map(|k| format!("{k}({} B)", build.sprites[*k].1))
|
||||||
|
.collect();
|
||||||
|
println!(
|
||||||
|
"entry {i:3} {:2} elements UNRESOLVED {:?} UNCLAIMED {:?}",
|
||||||
|
build.elements.len(), un, uc
|
||||||
|
);
|
||||||
|
if want.is_some() {
|
||||||
|
for el in &build.elements {
|
||||||
|
let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 { " <-- UNRESOLVED" } else { "" };
|
||||||
|
println!(" [{:2}] kind {:#06x} {:28} -> {:?}{mark}", el.index, el.kind, el.name, el.sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
crates/sylpheed-formats/examples/paint_order_audit.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
//! Does the DERIVED paint order reproduce the ones measured from the game?
|
||||||
|
//!
|
||||||
|
//! `compose` uses a measured order for the three builds that have one and falls
|
||||||
|
//! back to `derived_paint_order` (a sort on each sprite's layer key) everywhere
|
||||||
|
//! else. The doc comment claims the derived order "reproduces both measured
|
||||||
|
//! orders up to ties" — this checks that claim against all three, and says what
|
||||||
|
//! the ties actually cost.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example paint_order_audit -- <GP_TITLE.pak>
|
||||||
|
use sylpheed_formats::{pak, ui_layout};
|
||||||
|
|
||||||
|
fn measured(names: &[&str]) -> Option<(&'static str, Vec<usize>)> {
|
||||||
|
const TITLE: [&str; 24] = [
|
||||||
|
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
|
||||||
|
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
|
||||||
|
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
|
||||||
|
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
|
||||||
|
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
|
||||||
|
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
|
||||||
|
"ptlogoall_eff2.t32",
|
||||||
|
];
|
||||||
|
const SPLASH: [&str; 7] = [
|
||||||
|
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
|
||||||
|
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
|
||||||
|
"palogo_anima_eff.t32",
|
||||||
|
];
|
||||||
|
const MENU: [&str; 16] = [
|
||||||
|
"pteff00.prm", "ptbase.t32", "pteff05.t32", "ptloop01.rat",
|
||||||
|
"ptloop02.rat", "pteff02.prm", "ptframe1.t32", "ptframe2.t32",
|
||||||
|
"pteff10.t32", "pteff12.t32", "ptbtn01.rat", "ptbtn02.rat",
|
||||||
|
"ptbtn03.rat", "ptbtn04.rat", "ptbtn05.rat", "ptmsg.t32",
|
||||||
|
];
|
||||||
|
if names == TITLE {
|
||||||
|
return Some(("title", vec![9,11,12,10,13,6,20,19,14,15,18,16,17,0,2,4,7,1,3,5,22,23,21,8]));
|
||||||
|
}
|
||||||
|
if names == SPLASH { return Some(("splash", vec![0,2,4,6,1,3,5])); }
|
||||||
|
if names == MENU {
|
||||||
|
return Some(("main menu", vec![1,3,4,2,5,8,9,6,7,15,10,11,12,13,14,0]));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = std::env::args().nth(1).expect("usage: paint_order_audit <pak>");
|
||||||
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||||
|
let mut checked = 0;
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
let Ok(bytes) = ar.read(e) else { continue };
|
||||||
|
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||||
|
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
|
||||||
|
// Every build: how exposed is it to tie-breaking? A tie between
|
||||||
|
// OVERLAPPING elements is where a derived order can go visibly wrong.
|
||||||
|
let keys_all: Vec<u32> = build.elements.iter()
|
||||||
|
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
||||||
|
.collect();
|
||||||
|
let mut tie_pairs = 0;
|
||||||
|
for a in 0..keys_all.len() {
|
||||||
|
for b in (a + 1)..keys_all.len() {
|
||||||
|
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX { tie_pairs += 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Of the tied pairs, how many OVERLAP? Only those can paint visibly
|
||||||
|
// differently under an arbitrary tie-break. Rect from the declared
|
||||||
|
// pivot (= half the sprite for a .t32) at the resting placement.
|
||||||
|
let rect = |e: &ui_layout::Element| -> Option<(i32,i32,i32,i32)> {
|
||||||
|
let kf = e.rest()?;
|
||||||
|
let (w, h) = ((e.pivot_x * 2) as i32, (e.pivot_y * 2) as i32);
|
||||||
|
if w == 0 || h == 0 { return None; }
|
||||||
|
Some((kf.x, kf.y, w, h))
|
||||||
|
};
|
||||||
|
let mut tie_overlap = 0;
|
||||||
|
for a in 0..keys_all.len() {
|
||||||
|
for b in (a + 1)..keys_all.len() {
|
||||||
|
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
||||||
|
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
||||||
|
else { continue };
|
||||||
|
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
||||||
|
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
||||||
|
if ox > 0 && oy > 0 { tie_overlap += 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some((label, want)) = measured(&names) else {
|
||||||
|
println!("entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
|
||||||
|
{tie_overlap} of them OVERLAPPING", build.elements.len());
|
||||||
|
// Name them: these are the only pairs whose order can show.
|
||||||
|
for a in 0..keys_all.len() {
|
||||||
|
for b in (a + 1)..keys_all.len() {
|
||||||
|
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
||||||
|
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
||||||
|
else { continue };
|
||||||
|
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
||||||
|
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
||||||
|
if ox > 0 && oy > 0 {
|
||||||
|
println!(" overlapping tie: [{a}] {} x [{b}] {} key {} rect {:?} / {:?} overlap {}x{}",
|
||||||
|
build.elements[a].name, build.elements[b].name, keys_all[a], ra, rb, ox, oy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
checked += 1;
|
||||||
|
let got = ui_layout::derived_paint_order(&build, &bytes);
|
||||||
|
let keys: Vec<u32> = build.elements.iter()
|
||||||
|
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
||||||
|
.collect();
|
||||||
|
let exact = got == want;
|
||||||
|
// How many adjacent pairs in the MEASURED order does derived get wrong,
|
||||||
|
// and of those, how many are between elements sharing a layer key (a
|
||||||
|
// tie the sort cannot resolve) versus a genuine key-order conflict?
|
||||||
|
let pos_got: Vec<usize> = {
|
||||||
|
let mut p = vec![0; got.len()];
|
||||||
|
for (r, &e) in got.iter().enumerate() { p[e] = r; }
|
||||||
|
p
|
||||||
|
};
|
||||||
|
let (mut inv, mut tied) = (0, 0);
|
||||||
|
for a in 0..want.len() {
|
||||||
|
for b in (a + 1)..want.len() {
|
||||||
|
let (x, y) = (want[a], want[b]);
|
||||||
|
if pos_got[x] > pos_got[y] {
|
||||||
|
inv += 1;
|
||||||
|
if keys[x] == keys[y] { tied += 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("entry {i:2} {label:10} {} elements", want.len());
|
||||||
|
println!(" derived == measured : {}", if exact { "YES" } else { "NO" });
|
||||||
|
println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})");
|
||||||
|
if !exact {
|
||||||
|
println!(" measured: {want:?}");
|
||||||
|
println!(" derived : {got:?}");
|
||||||
|
println!(" keys : {keys:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\n{checked} build(s) with a measured order were checked");
|
||||||
|
}
|
||||||
52
crates/sylpheed-formats/examples/plateauless_suppression.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//! Does DRAWING NOTHING beat guessing, for an element with no held pose?
|
||||||
|
//!
|
||||||
|
//! A keyframe group is entry → hold → exit, and the exit ends invisible (on the
|
||||||
|
//! five port screens the final keyframe is invisible for 21/24, 8/16, 12/18, 2/3
|
||||||
|
//! and 6/7 elements). So the screen "as seen" is the HOLD — which is why
|
||||||
|
//! `rest_plateau` is the primary rule. An element with **no** plateau has no
|
||||||
|
//! hold, and `rest()` currently falls back to guessing an endpoint of a movement.
|
||||||
|
//!
|
||||||
|
//! This renders each screen twice — as-is, and with every plateau-less element
|
||||||
|
//! suppressed via `compose`'s `visible` mask — and correlates both against the
|
||||||
|
//! live capture. If suppression wins, the fallback should draw nothing.
|
||||||
|
//! Writes both composites as raw RGBA (`<out>/entryNN_{asis,suppressed}.raw`,
|
||||||
|
//! 1280x720) so the correlation is done outside — this crate has no image
|
||||||
|
//! decoder and the comparison is not worth a dependency.
|
||||||
|
use sylpheed_formats::{pak, ui_layout};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let pak_path = std::env::args().nth(1).expect("usage: <GP_TITLE.pak> <outdir> <entry>...");
|
||||||
|
let ar = pak::PakArchive::open(&pak_path).expect("open");
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
let outdir = std::env::args().nth(2).expect("outdir");
|
||||||
|
std::fs::create_dir_all(&outdir).ok();
|
||||||
|
for spec in std::env::args().skip(3) {
|
||||||
|
let idx: usize = spec.parse().unwrap();
|
||||||
|
let bytes = ar.read(&entries[idx]).expect("read");
|
||||||
|
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||||
|
// plateau-less = rest() had to guess: no two adjacent keyframes share a pose
|
||||||
|
// Default: suppress plateau-less elements. With SUPPRESS_SUBSTR set,
|
||||||
|
// suppress every element whose NAME contains it instead — used to test
|
||||||
|
// the entry→hold→exit model's prediction that the splash glows are all
|
||||||
|
// finished by the moment the logos are up.
|
||||||
|
let by_name = std::env::var("SUPPRESS_SUBSTR").ok();
|
||||||
|
let mask: Vec<bool> = build.elements.iter().map(|e| {
|
||||||
|
if let Some(sub) = &by_name {
|
||||||
|
return !e.name.to_lowercase().contains(sub.as_str());
|
||||||
|
}
|
||||||
|
let k = &e.keyframes;
|
||||||
|
(0..k.len().saturating_sub(1)).any(|i| {
|
||||||
|
k[i].fade == k[i+1].fade && k[i].scale_x == k[i+1].scale_x
|
||||||
|
&& k[i].scale_y == k[i+1].scale_y && k[i].x == k[i+1].x && k[i].y == k[i+1].y
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
let suppressed = mask.iter().filter(|m| !**m).count();
|
||||||
|
let opts = ui_layout::ComposeOptions::default();
|
||||||
|
let a = ui_layout::compose(&build, &bytes, opts, None);
|
||||||
|
let b = ui_layout::compose(&build, &bytes, opts, Some(&mask));
|
||||||
|
std::fs::write(format!("{outdir}/entry{idx:02}_asis.raw"), &a.rgba).unwrap();
|
||||||
|
std::fs::write(format!("{outdir}/entry{idx:02}_suppressed.raw"), &b.rgba).unwrap();
|
||||||
|
println!("entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}",
|
||||||
|
a.width, a.height, build.elements.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
71
crates/sylpheed-formats/examples/rat_leaf_placement.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//! Does a `.rat` leaf record decode with the same reader as a whole bundle?
|
||||||
|
//!
|
||||||
|
//! The port needs the position of `ptbtneff01.t32`, the focus ring, which is
|
||||||
|
//! declared *inside* the nested `ptbtn0Nf.rat` leaf and is therefore invisible
|
||||||
|
//! to anything that walks only a bundle's top-level elements.
|
||||||
|
//!
|
||||||
|
//! The leaf's first 32 bytes have the same shape as a bundle header --
|
||||||
|
//! `"RATC"`, `0x3c` declaration-entry size at `+4`, element count at `+20`,
|
||||||
|
//! design size at `+24`/`+28` -- so the hypothesis is that `parse_build` reads
|
||||||
|
//! it unchanged. The control is the BASE record `ptbtn0N.rat`, whose single
|
||||||
|
//! element's position is already known independently: the parent screen's
|
||||||
|
//! `screen info` reports `ptbtn01.rat` resting at (542,162).
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example rat_leaf_placement -- <GP_TITLE.pak>
|
||||||
|
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = std::env::args().nth(1).expect("usage: … <pak>");
|
||||||
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
|
||||||
|
for (ei, e) in entries.iter().enumerate() {
|
||||||
|
let Ok(bytes) = ar.read(e) else { continue };
|
||||||
|
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||||
|
// Only the title-family bundles carry ptbtn records.
|
||||||
|
if !kids.iter().any(|c| c.name == "ptbtn01f.rat") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("=== pak entry {ei}");
|
||||||
|
for c in &kids {
|
||||||
|
if c.kind != "RATC" || !c.name.starts_with("ptbtn") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let leaf = &bytes[c.offset..c.offset + c.size];
|
||||||
|
match ui_layout::parse_build(leaf) {
|
||||||
|
None => println!(" {:16} …parse_build says no", c.name),
|
||||||
|
Some(b) => {
|
||||||
|
println!(
|
||||||
|
" {:16} {}x{} {} element(s), fallback={}",
|
||||||
|
c.name,
|
||||||
|
b.design_w,
|
||||||
|
b.design_h,
|
||||||
|
b.elements.len(),
|
||||||
|
b.from_fallback
|
||||||
|
);
|
||||||
|
for el in &b.elements {
|
||||||
|
let r = el.rest();
|
||||||
|
println!(
|
||||||
|
" [{}] {:18} pivot ({:4},{:4}) rest ({:5},{:5}) kf {}",
|
||||||
|
el.index,
|
||||||
|
el.name,
|
||||||
|
el.pivot_x,
|
||||||
|
el.pivot_y,
|
||||||
|
r.map(|k| k.x).unwrap_or(-1),
|
||||||
|
r.map(|k| k.y).unwrap_or(-1),
|
||||||
|
el.keyframes.len()
|
||||||
|
);
|
||||||
|
for (i, k) in el.keyframes.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
" kf{i} t={:?} pos=({},{}) scale={}%,{}% a={} rot={} tint={:#010x}",
|
||||||
|
k.time, k.x, k.y, k.scale_x, k.scale_y,
|
||||||
|
k.fade >> 24, k.rotation_deg, k.tint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
crates/sylpheed-formats/examples/ratc_child_names.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
//! Is a RATC child's name the printable run before its magic, or the `opt ` block?
|
||||||
|
//!
|
||||||
|
//! `ratc::parse` names each child by scanning backwards for the ASCII run that
|
||||||
|
//! precedes its magic. That is usually right, but it is a HEURISTIC, and the
|
||||||
|
//! real format states the name explicitly: immediately before each child sits
|
||||||
|
//!
|
||||||
|
//! "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||||||
|
//!
|
||||||
|
//! -- the same `opt ` block `ui_layout::opt_link` already decodes for a button's
|
||||||
|
//! focus link. When those 3 trailing bytes happen to be printable the heuristic
|
||||||
|
//! reads THEM as the name: the title screens' full-resolution background comes
|
||||||
|
//! out as `8AX` (bytes 38 41 58) instead of `pteff05.t32`, its element then
|
||||||
|
//! resolves to no sprite, and `compose` drops the screen's background.
|
||||||
|
//!
|
||||||
|
//! This compares the two readings for every RATC child in the paks given.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example ratc_child_names -- <pak>...
|
||||||
|
use sylpheed_formats::{pak, ratc};
|
||||||
|
|
||||||
|
/// The name stated by the `opt ` block that ends just before `at`.
|
||||||
|
fn opt_name(buf: &[u8], at: usize) -> Option<String> {
|
||||||
|
// The block is short; search back a bounded window for the tag.
|
||||||
|
let lo = at.saturating_sub(128);
|
||||||
|
let win = &buf[lo..at];
|
||||||
|
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
|
||||||
|
let len = u32::from_be_bytes(buf[pos + 4..pos + 8].try_into().ok()?) as usize;
|
||||||
|
if len == 0 || len > 64 || pos + 8 + len > at {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]).to_string();
|
||||||
|
// It must be THIS child's block: name, NUL, then a short run to the magic.
|
||||||
|
if at - (pos + 8 + len) > 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut children = 0usize;
|
||||||
|
let mut with_opt = 0usize;
|
||||||
|
let mut agree = 0usize;
|
||||||
|
let mut disagree: Vec<(String, usize, String, String)> = Vec::new();
|
||||||
|
for path in std::env::args().skip(1) {
|
||||||
|
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||||
|
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
for (i, e) in entries.iter().enumerate() {
|
||||||
|
let Ok(bytes) = ar.read(e) else { continue };
|
||||||
|
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||||
|
for c in &kids {
|
||||||
|
children += 1;
|
||||||
|
let Some(o) = opt_name(&bytes, c.offset) else { continue };
|
||||||
|
with_opt += 1;
|
||||||
|
if o == c.name {
|
||||||
|
agree += 1;
|
||||||
|
} else {
|
||||||
|
disagree.push((short.clone(), i, c.name.clone(), o));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("RATC children scanned : {children}");
|
||||||
|
println!(" with an `opt ` block: {with_opt}");
|
||||||
|
println!(" scanned name AGREES : {agree}");
|
||||||
|
println!(" scanned name DIFFERS : {}", disagree.len());
|
||||||
|
let mut by_pair: std::collections::BTreeMap<(String, String), usize> = Default::default();
|
||||||
|
for (_, _, scanned, opt) in &disagree {
|
||||||
|
*by_pair.entry((scanned.clone(), opt.clone())).or_default() += 1;
|
||||||
|
}
|
||||||
|
println!("\ndistinct disagreements (scanned -> opt), with counts:");
|
||||||
|
for ((s, o), n) in &by_pair {
|
||||||
|
println!(" {s:24} -> {o:24} x{n}");
|
||||||
|
}
|
||||||
|
println!("\nfirst 20 occurrences:");
|
||||||
|
for (p, i, s, o) in disagree.iter().take(20) {
|
||||||
|
println!(" {p} entry {i:4} {s:20} -> {o}");
|
||||||
|
}
|
||||||
|
}
|
||||||
119
crates/sylpheed-formats/examples/ratc_optless_children.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
//! The 60 RATC children that carry no `opt ` block — do they lack it, or is our
|
||||||
|
//! window too small?
|
||||||
|
//!
|
||||||
|
//! [`ratc::parse`] now prefers the name a child's own `opt ` block states, and
|
||||||
|
//! falls back to the old backwards printable-run scan when there is no block
|
||||||
|
//! within 128 bytes. That fallback fires for 60 of the disc's 18 002 children
|
||||||
|
//! (0.3 %), and the reach of the finding in `docs/re/structures/ratc-child-names.md`
|
||||||
|
//! stops there: "whether they genuinely lack the block or sit past the search
|
||||||
|
//! window is not established".
|
||||||
|
//!
|
||||||
|
//! This settles that. For every child with no accepted block it reports
|
||||||
|
//!
|
||||||
|
//! * whether an `opt ` tag exists at all further back, and how far;
|
||||||
|
//! * which guard rejected a tag that WAS in the window (length, gap, charset);
|
||||||
|
//! * the child's position in its bundle and its magic, in case the opt-less
|
||||||
|
//! ones are structurally distinct (e.g. always the first child);
|
||||||
|
//! * the raw bytes before the magic, so the fallback's answer can be judged.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example ratc_optless_children -- <pak>...
|
||||||
|
use sylpheed_formats::{pak, ratc};
|
||||||
|
|
||||||
|
/// Why a child has no accepted `opt ` name. Mirrors `ratc::opt_name`'s guards
|
||||||
|
/// one for one, so a rejection here is the same rejection the parser made.
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||||
|
enum Why {
|
||||||
|
/// No `opt ` tag in the 128-byte window, and none anywhere before it either.
|
||||||
|
NoTagAtAll,
|
||||||
|
/// No tag in the window, but one exists further back, this many bytes away.
|
||||||
|
TagBeyondWindow(usize),
|
||||||
|
/// Tag found, but its BE32 length is 0 or > 64.
|
||||||
|
BadLength(usize),
|
||||||
|
/// Tag found, name ends more than 8 bytes before the magic — a neighbour's.
|
||||||
|
GapTooBig(usize),
|
||||||
|
/// Tag found, the named bytes are not all printable ASCII.
|
||||||
|
NotGraphic,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parser's own window.
|
||||||
|
const WINDOW: usize = 128;
|
||||||
|
|
||||||
|
fn classify(buf: &[u8], at: usize) -> Option<Why> {
|
||||||
|
let lo = at.saturating_sub(WINDOW);
|
||||||
|
let pos = match buf[lo..at].windows(4).rposition(|w| w == b"opt ") {
|
||||||
|
Some(p) => lo + p,
|
||||||
|
None => {
|
||||||
|
// Widen to the whole buffer before the child: is it merely far away?
|
||||||
|
return Some(match buf[..at].windows(4).rposition(|w| w == b"opt ") {
|
||||||
|
Some(p) => Why::TagBeyondWindow(at - p),
|
||||||
|
None => Why::NoTagAtAll,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let len = u32::from_be_bytes(buf.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
|
||||||
|
if len == 0 || len > 64 || pos + 8 + len > at {
|
||||||
|
return Some(Why::BadLength(len));
|
||||||
|
}
|
||||||
|
let gap = at - (pos + 8 + len);
|
||||||
|
if gap > 8 {
|
||||||
|
return Some(Why::GapTooBig(gap));
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]);
|
||||||
|
if s.is_empty() || !s.chars().all(|c| c.is_ascii_graphic()) {
|
||||||
|
return Some(Why::NotGraphic);
|
||||||
|
}
|
||||||
|
None // accepted — this child is not one of the 60
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut total = 0usize;
|
||||||
|
let mut rows: Vec<(String, usize, usize, String, String, Why)> = Vec::new();
|
||||||
|
let mut first_child_of_bundle = 0usize;
|
||||||
|
for path in std::env::args().skip(1) {
|
||||||
|
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||||
|
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||||
|
let entries: Vec<_> = ar.entries().to_vec();
|
||||||
|
for (ei, e) in entries.iter().enumerate() {
|
||||||
|
let Ok(bytes) = ar.read(e) else { continue };
|
||||||
|
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||||
|
for (ci, c) in kids.iter().enumerate() {
|
||||||
|
total += 1;
|
||||||
|
let Some(why) = classify(&bytes, c.offset) else { continue };
|
||||||
|
if ci == 0 {
|
||||||
|
first_child_of_bundle += 1;
|
||||||
|
}
|
||||||
|
let lo = c.offset.saturating_sub(24);
|
||||||
|
let hex = bytes[lo..c.offset]
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02x}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
rows.push((short.clone(), ei, ci, c.name.clone(), hex, why));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("RATC children scanned : {total}");
|
||||||
|
println!(" with NO accepted `opt ` block: {}", rows.len());
|
||||||
|
println!(" ... of which are child #0 : {first_child_of_bundle}");
|
||||||
|
|
||||||
|
let mut by_why: std::collections::BTreeMap<String, usize> = Default::default();
|
||||||
|
for r in &rows {
|
||||||
|
let k = match &r.5 {
|
||||||
|
Why::TagBeyondWindow(_) => "TagBeyondWindow".to_string(),
|
||||||
|
Why::BadLength(_) => "BadLength".to_string(),
|
||||||
|
Why::GapTooBig(_) => "GapTooBig".to_string(),
|
||||||
|
other => format!("{other:?}"),
|
||||||
|
};
|
||||||
|
*by_why.entry(k).or_default() += 1;
|
||||||
|
}
|
||||||
|
println!("\nwhy, by cause:");
|
||||||
|
for (k, n) in &by_why {
|
||||||
|
println!(" {k:20} x{n}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nevery occurrence (name is what the FALLBACK scan returned):");
|
||||||
|
for (p, ei, ci, name, hex, why) in &rows {
|
||||||
|
println!(" {p:28} entry {ei:4} child {ci:3} {name:24} {why:?}");
|
||||||
|
println!(" 24 bytes before the magic: {hex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
27
crates/sylpheed-formats/examples/se_wave_dump.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//! Dump the menu's three SE cues as decodable `RIFF`s, to prove `se_wave_riff`
|
||||||
|
//! produces something ffmpeg actually accepts.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example se_wave_dump -- <outdir>
|
||||||
|
//! ffmpeg -i <outdir>/move.riff move.wav
|
||||||
|
use sylpheed_formats::media::{self, DirectorySource};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let out = std::env::args().nth(1).unwrap_or_else(|| "/tmp".into());
|
||||||
|
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||||
|
let src = DirectorySource::new(&disc);
|
||||||
|
for (name, off, pkts) in [("move", 0x1ec0usize, 4usize), ("back", 0x0ec0, 2), ("confirm", 0x5d6c0, 6)] {
|
||||||
|
match media::se_wave_riff(&src, "Static.slb", off, pkts, 1, 48000) {
|
||||||
|
Ok(riff) => {
|
||||||
|
let p = format!("{out}/{name}.riff");
|
||||||
|
std::fs::write(&p, &riff).unwrap();
|
||||||
|
println!("{p}: {} bytes ({pkts} packets at {off:#x})", riff.len());
|
||||||
|
}
|
||||||
|
Err(e) => println!("{name}: ERROR {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The refusal path: a packet count the bank cannot satisfy.
|
||||||
|
match media::se_wave_riff(&src, "Static.slb", 0x1ec0, 1 << 20, 1, 48000) {
|
||||||
|
Ok(_) => println!("REFUSAL PATH FAILED — returned a short stream"),
|
||||||
|
Err(e) => println!("refusal path ok: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,62 @@ pub fn sound_bank_riffs<S: DiscSource + ?Sized>(
|
|||||||
Ok(riffs_of(&bytes))
|
Ok(riffs_of(&bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One sound-effect wave out of a **delimiter-less** bank, as a decodable `RIFF`.
|
||||||
|
///
|
||||||
|
/// `Static.slb` — where the menu's cues live — has no `RIFF`, no `seek` chunk and
|
||||||
|
/// no XACT container: it is a packed run of whole 2048-byte XMA1 packets. So
|
||||||
|
/// [`sound_bank_riffs`] finds nothing to split on, and a wave is defined *only*
|
||||||
|
/// by `(offset, packet_count)`. Both come from the running game, not from the
|
||||||
|
/// file: launch Canary with `--xma_param_probe=true`, trigger the sound, and the
|
||||||
|
/// log prints the stream's packet count and first 32 bytes; searching those bytes
|
||||||
|
/// in the bank gives the offset. ⚠️ The file order is **not** cue-id order, so the
|
||||||
|
/// index cannot be counted out — see `docs/re/menu-audio-cues.md`.
|
||||||
|
///
|
||||||
|
/// The three cues a menu needs, all mono 48 kHz:
|
||||||
|
///
|
||||||
|
/// | event | offset | packets |
|
||||||
|
/// |---|---|---|
|
||||||
|
/// | d-pad move | `0x1ec0` | 4 |
|
||||||
|
/// | Ⓑ back | `0x0ec0` | 2 |
|
||||||
|
/// | Ⓐ confirm | `0x5d6c0` | 6 |
|
||||||
|
///
|
||||||
|
/// Returns an error rather than a short stream if the bank does not actually
|
||||||
|
/// hold `packet_count` whole packets at `offset` — a truncated XMA stream decodes
|
||||||
|
/// to plausible-sounding garbage, which is the failure worth refusing.
|
||||||
|
pub fn se_wave_riff<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
bank: &str,
|
||||||
|
offset: usize,
|
||||||
|
packet_count: usize,
|
||||||
|
channels: u8,
|
||||||
|
rate: u32,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let len = packet_count * crate::slb::XMA1_PACKET;
|
||||||
|
// Read only the packets asked for, not the whole bank. That is not just an
|
||||||
|
// efficiency point: `Static.slb` is 8.97 MB and is the ONE entry in
|
||||||
|
// `sound.pak` whose declared extent runs past the end of the extracted
|
||||||
|
// segments (by 616 768 B), so reading it whole fails outright on a disc
|
||||||
|
// extraction that is short at the tail. Every cue we need sits in the first
|
||||||
|
// few hundred KB. See `docs/re/menu-audio-cues.md`.
|
||||||
|
let toc = source.read_file("dat/sound.pak")?;
|
||||||
|
let entries = PakArchive::parse_toc(&toc).map_err(|e| e.to_string())?;
|
||||||
|
let hash = crate::hash::name_hash(bank);
|
||||||
|
let idx = entries
|
||||||
|
.binary_search_by_key(&hash, |e| e.name_hash)
|
||||||
|
.map_err(|_| format!("{bank}: not present in sound.pak"))?;
|
||||||
|
let e = &entries[idx];
|
||||||
|
if offset + len > e.comp_size as usize {
|
||||||
|
return Err(format!(
|
||||||
|
"{bank}: {packet_count} packets at {offset:#x} need {len} bytes, \
|
||||||
|
but the bank declares only {} bytes",
|
||||||
|
e.comp_size
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let packets =
|
||||||
|
source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?;
|
||||||
|
Ok(crate::slb::xma1_wave_riff(&packets, channels, rate))
|
||||||
|
}
|
||||||
|
|
||||||
/// The XMA `RIFF`s of a continuous byte region of the voice stream, as returned
|
/// The XMA `RIFF`s of a continuous byte region of the voice stream, as returned
|
||||||
/// by [`resolve_movie_voice_region`].
|
/// by [`resolve_movie_voice_region`].
|
||||||
pub fn voice_region_riffs<S: DiscSource + ?Sized>(
|
pub fn voice_region_riffs<S: DiscSource + ?Sized>(
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
|
|||||||
for (idx, &(off, kind)) in offs.iter().enumerate() {
|
for (idx, &(off, kind)) in offs.iter().enumerate() {
|
||||||
let next = offs.get(idx + 1).map(|&(o, _)| o).unwrap_or(bytes.len());
|
let next = offs.get(idx + 1).map(|&(o, _)| o).unwrap_or(bytes.len());
|
||||||
children.push(RatcChild {
|
children.push(RatcChild {
|
||||||
name: name_before(bytes, off),
|
name: opt_name(bytes, off).unwrap_or_else(|| name_before(bytes, off)),
|
||||||
kind: kind.to_string(),
|
kind: kind.to_string(),
|
||||||
offset: off,
|
offset: off,
|
||||||
size: next.saturating_sub(off),
|
size: next.saturating_sub(off),
|
||||||
@@ -71,9 +71,51 @@ pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
|
|||||||
Some(children)
|
Some(children)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The name a child's own `opt ` block states, if it has one.
|
||||||
|
///
|
||||||
|
/// The real format is explicit. Immediately before each child sits
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// -- the same `opt ` block `ui_layout`'s focus link already reads. Prefer it,
|
||||||
|
/// because [`name_before`] is a heuristic and those 3 trailing bytes are
|
||||||
|
/// sometimes printable, in which case the heuristic reads THEM as the name.
|
||||||
|
/// Measured disc-wide: of 18 002 RATC children, 17 942 carry an `opt ` block,
|
||||||
|
/// 17 918 of which agree with the scan and **24 do not** -- every one of the 24
|
||||||
|
/// a 3-byte tail (`8AX` x22, `'OX` x2) beating a real name. On the title screens
|
||||||
|
/// that cost the whole background: `pteff05.t32` came out as `8AX`, its element
|
||||||
|
/// then resolved to no sprite, and `compose` silently dropped it. See
|
||||||
|
/// `docs/re/structures/ratc-child-names.md`.
|
||||||
|
fn opt_name(bytes: &[u8], off: usize) -> Option<String> {
|
||||||
|
let lo = off.saturating_sub(128);
|
||||||
|
let win = &bytes[lo..off];
|
||||||
|
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
|
||||||
|
let len = u32::from_be_bytes(bytes.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
|
||||||
|
if len == 0 || len > 64 || pos + 8 + len > off {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// It must be THIS child's block: the name, its NUL and a short run to the
|
||||||
|
// magic. Anything further away is a neighbour's block, so fall back.
|
||||||
|
if off - (pos + 8 + len) > 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&bytes[pos + 8..pos + 8 + len]).to_string();
|
||||||
|
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
|
||||||
|
}
|
||||||
|
|
||||||
/// The nearest name string preceding `off`: the *last* printable run (len ≥ 3)
|
/// The nearest name string preceding `off`: the *last* printable run (len ≥ 3)
|
||||||
/// in the 96 bytes before the child magic. A few record-header bytes usually sit
|
/// in the 96 bytes before the child magic. A few record-header bytes usually sit
|
||||||
/// between the name and the magic, so an exact-adjacency scan isn't enough.
|
/// between the name and the magic, so an exact-adjacency scan isn't enough.
|
||||||
|
///
|
||||||
|
/// Fallback only -- [`opt_name`] is the stated name. 60 of the disc's 18 002
|
||||||
|
/// children have no `opt ` block and still rely on this, and all 60 are
|
||||||
|
/// accounted for: they are the ten frames of the disc's only `.tan` frame
|
||||||
|
/// sequence, in six language copies of one `GP_READY_ROOM` bundle, where a
|
||||||
|
/// single `opt ` block names the whole run. See
|
||||||
|
/// `docs/re/structures/ratc-tan-frame-sequence.md` -- and note that this means
|
||||||
|
/// `parse` OVER-reports there, listing frames as children.
|
||||||
fn name_before(bytes: &[u8], off: usize) -> String {
|
fn name_before(bytes: &[u8], off: usize) -> String {
|
||||||
let start = off.saturating_sub(96);
|
let start = off.saturating_sub(96);
|
||||||
let window = &bytes[start..off];
|
let window = &bytes[start..off];
|
||||||
@@ -101,6 +143,49 @@ fn name_before(bytes: &[u8], off: usize) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// The `opt ` block wins over a printable tail.
|
||||||
|
///
|
||||||
|
/// This is the `pteff05.t32` / `8AX` case, byte for byte: the name is stated
|
||||||
|
/// with an explicit length, then a NUL, then three payload bytes that happen
|
||||||
|
/// to spell `8AX` in ASCII. The old backwards printable-run scan returned
|
||||||
|
/// `8AX` here, which is what dropped the background from every menu screen.
|
||||||
|
#[test]
|
||||||
|
fn opt_block_beats_a_printable_tail() {
|
||||||
|
let mut b = RATC_MAGIC.to_vec();
|
||||||
|
b.extend_from_slice(&[0u8; 28]);
|
||||||
|
b.extend_from_slice(b"opt ");
|
||||||
|
b.extend_from_slice(&11u32.to_be_bytes()); // len("pteff05.t32")
|
||||||
|
b.extend_from_slice(b"pteff05.t32\0");
|
||||||
|
b.extend_from_slice(b"8AX"); // payload, printable by accident
|
||||||
|
let off = b.len();
|
||||||
|
b.extend_from_slice(b"T8aD");
|
||||||
|
b.extend_from_slice(&[0u8; 16]);
|
||||||
|
|
||||||
|
let kids = parse(&b).expect("parse");
|
||||||
|
assert_eq!(kids.len(), 1);
|
||||||
|
assert_eq!(kids[0].name, "pteff05.t32");
|
||||||
|
assert_eq!(kids[0].offset, off);
|
||||||
|
// And the heuristic on its own really would have said `8AX` -- so this
|
||||||
|
// test fails for the right reason if the preference is ever reversed.
|
||||||
|
assert_eq!(name_before(&b, off), "8AX");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No `opt ` block: 60 of the disc's 18 002 children are like this, and they
|
||||||
|
/// must keep working off the scan.
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_the_scan_without_an_opt_block() {
|
||||||
|
let mut b = RATC_MAGIC.to_vec();
|
||||||
|
b.extend_from_slice(&[0u8; 28]);
|
||||||
|
b.extend_from_slice(b"plain.t32");
|
||||||
|
b.extend_from_slice(&[0x0e, 0x10, 0xa4]);
|
||||||
|
let off = b.len();
|
||||||
|
b.extend_from_slice(b"T8aD");
|
||||||
|
b.extend_from_slice(&[0u8; 16]);
|
||||||
|
|
||||||
|
let kids = parse(&b).expect("parse");
|
||||||
|
assert_eq!(kids[0].name, "plain.t32");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lists_named_children() {
|
fn lists_named_children() {
|
||||||
let mut b = RATC_MAGIC.to_vec();
|
let mut b = RATC_MAGIC.to_vec();
|
||||||
|
|||||||
@@ -535,6 +535,28 @@ pub fn to_xma_riff_best(slb: &[u8]) -> Option<Vec<u8>> {
|
|||||||
(!data.is_empty()).then(|| build_riff(&synth_xma1_fmt(2, 2, 48000), data))
|
(!data.is_empty()).then(|| build_riff(&synth_xma1_fmt(2, 2, 48000), data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrap a run of **raw XMA1 packets** as a standalone, decodable `RIFF/WAVE`.
|
||||||
|
///
|
||||||
|
/// For a bank with no internal delimiters — `Static.slb` is a packed run of whole
|
||||||
|
/// 2048-byte packets with no `RIFF`, no `seek` and no `WAVE` — a wave is defined
|
||||||
|
/// *only* by `(offset, packet count)`, both of which come from the running game
|
||||||
|
/// (`--xma_param_probe`). There is nothing in the file to parse, so the header
|
||||||
|
/// has to be synthesized, and that is the step worth doing exactly once, here,
|
||||||
|
/// rather than in each consumer.
|
||||||
|
///
|
||||||
|
/// `packets` must be a whole number of [`XMA1_PACKET`] bytes; anything else is a
|
||||||
|
/// short read and produces a stream the decoder will run off the end of.
|
||||||
|
/// The `channel_mask` follows the same convention as the rest of this module:
|
||||||
|
/// `1` for mono, `2` for stereo.
|
||||||
|
///
|
||||||
|
/// The three menu cues in `docs/re/menu-audio-cues.md` are
|
||||||
|
/// `(0x1ec0, 4)` d-pad move, `(0x0ec0, 2)` Ⓑ back and `(0x5d6c0, 6)` Ⓐ confirm,
|
||||||
|
/// all mono 48 kHz.
|
||||||
|
pub fn xma1_wave_riff(packets: &[u8], channels: u8, rate: u32) -> Vec<u8> {
|
||||||
|
let mask = if channels == 1 { 1 } else { 2 };
|
||||||
|
build_riff(&synth_xma1_fmt(channels, mask, rate), packets)
|
||||||
|
}
|
||||||
|
|
||||||
/// A minimal `fmt ` chunk carrying an XMA1 `XMAWAVEFORMAT` (one stream).
|
/// A minimal `fmt ` chunk carrying an XMA1 `XMAWAVEFORMAT` (one stream).
|
||||||
fn synth_xma1_fmt(channels: u8, channel_mask: u16, rate: u32) -> Vec<u8> {
|
fn synth_xma1_fmt(channels: u8, channel_mask: u16, rate: u32) -> Vec<u8> {
|
||||||
let mut fmt = Vec::with_capacity(40);
|
let mut fmt = Vec::with_capacity(40);
|
||||||
|
|||||||
@@ -38,6 +38,18 @@ pub struct T8adImage {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub rgba: Vec<u8>,
|
pub rgba: Vec<u8>,
|
||||||
|
/// The header word at `+0x04`. A flag word; **bit `0x02`** is a candidate
|
||||||
|
/// blend selector — it separates the title's effect sprites from its ordinary
|
||||||
|
/// ones exactly, and disc-wide it toggles independently of the rest of the
|
||||||
|
/// word in 27.1 % of 19 216 sprites. See
|
||||||
|
/// `docs/re/structures/ui-paint-order-key.md`.
|
||||||
|
///
|
||||||
|
/// ⚠️ **Additive was tested and REFUTED.** Blending bit-`0x02` sprites
|
||||||
|
/// additively moved every measure against the title capture the wrong way:
|
||||||
|
/// whole-frame mean diff +0.55 → +1.04, swoosh-band mean +1.83 → +3.98, band
|
||||||
|
/// edge-correlation 0.6971 → 0.5578. The bit is real and independent, but it
|
||||||
|
/// does not select an additive blend. Carried, not acted on.
|
||||||
|
pub flags: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `bytes` starts with the T8aD magic.
|
/// Whether `bytes` starts with the T8aD magic.
|
||||||
@@ -108,6 +120,7 @@ pub fn parse(bytes: &[u8]) -> Option<T8adImage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(T8adImage {
|
Some(T8adImage {
|
||||||
|
flags: be32(bytes, 4),
|
||||||
width: width as u32,
|
width: width as u32,
|
||||||
height: height as u32,
|
height: height as u32,
|
||||||
rgba,
|
rgba,
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ const DESIGN_H: u32 = 720;
|
|||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// +0 u32 ARGB fade colour — alpha ramps 0x00 → 0x80 → 0xd5 … over the group
|
/// +0 u32 ARGB fade colour — alpha ramps 0x00 → 0x80 → 0xd5 … over the group
|
||||||
/// +4 u32 0
|
/// +4 i32 ⚠️ NOT always 0 — see below
|
||||||
/// +8 u32 0
|
/// +8 i32 ⚠️ NOT always 0 — see below
|
||||||
/// +12 u32 0
|
/// +12 i32 ⚠️ NOT always 0 — see below
|
||||||
/// +16 u32 scale X, percent
|
/// +16 u32 scale X, percent
|
||||||
/// +20 u32 scale Y, percent
|
/// +20 u32 scale Y, percent
|
||||||
/// +24 u32 tint (0xffff_ffff on every frame seen)
|
/// +24 u32 tint (0xffff_ffff on every frame seen)
|
||||||
@@ -69,10 +69,39 @@ const DESIGN_H: u32 = 720;
|
|||||||
/// +32 i32 Y ← signed
|
/// +32 i32 Y ← signed
|
||||||
/// +36 u32 time
|
/// +36 u32 time
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## `+12` is the screen-plane ROTATION, in degrees (2026-08-28)
|
||||||
|
///
|
||||||
|
/// ✅ **Measured against the framebuffer, not against our own renderer.** The
|
||||||
|
/// title's two light sweeps are the nested leaf records `ptloop01.rat` /
|
||||||
|
/// `ptloop02.rat`, and their keyframe blocks read `+12` = `30` and `-45`. A
|
||||||
|
/// `log_ui_draws` capture of the live title submits those two quads rotated by
|
||||||
|
/// **+30.26°** and **-45.28°** — magnitude *and* sign, on two different values.
|
||||||
|
/// Positive is clockwise in screen space (Y down).
|
||||||
|
///
|
||||||
|
/// `+4` and `+8` are 🟡 still unexplained: signed, non-zero in ~4.7 % / 4.6 %
|
||||||
|
/// of blocks disc-wide, dominated by `±180` and `±90`. Plausibly rotation about
|
||||||
|
/// the other two axes, but nothing observed turns on them.
|
||||||
|
///
|
||||||
|
/// ⚠️ **`rotation_deg` is decoded but NOT rendered.** [`crate::ui_layout`]'s
|
||||||
|
/// blitter draws axis-aligned quads only, so `screen render` still paints a
|
||||||
|
/// rotated element upright. See `docs/re/ui-title-build-map.md`.
|
||||||
|
///
|
||||||
|
/// ⚠️ The earlier note here — *"every element of `GP_TITLE` build 4 has all
|
||||||
|
/// three at zero"* — was **wrong about reach, not about the bytes**: build 4's
|
||||||
|
/// top-level elements do read zero, but the rotated quads come from its two
|
||||||
|
/// **nested** `.rat` leaf records, which the census never opened.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Keyframe {
|
pub struct Keyframe {
|
||||||
/// The fade colour, ARGB. Its alpha is what ramps an element in.
|
/// The fade colour, ARGB. Its alpha is what ramps an element in.
|
||||||
pub fade: u32,
|
pub fade: u32,
|
||||||
|
/// Screen-plane rotation in **degrees**, clockwise-positive (`+12`).
|
||||||
|
/// Confirmed against a GPU capture; see the type's docs. Not rendered.
|
||||||
|
pub rotation_deg: i32,
|
||||||
|
/// `+4` / `+8` — signed, meaning unexplained. Carried rather than dropped
|
||||||
|
/// so a consumer can see them instead of assuming they are zero.
|
||||||
|
pub unknown_4: i32,
|
||||||
|
pub unknown_8: i32,
|
||||||
/// Scale in percent (100 = 1:1).
|
/// Scale in percent (100 = 1:1).
|
||||||
pub scale_x: u32,
|
pub scale_x: u32,
|
||||||
pub scale_y: u32,
|
pub scale_y: u32,
|
||||||
@@ -154,6 +183,15 @@ impl Element {
|
|||||||
/// Falls back to the longest-dwell rule when no two adjacent keyframes
|
/// Falls back to the longest-dwell rule when no two adjacent keyframes
|
||||||
/// agree — a group that ramps through every frame and never holds.
|
/// agree — a group that ramps through every frame and never holds.
|
||||||
pub fn rest(&self) -> Option<&Keyframe> {
|
pub fn rest(&self) -> Option<&Keyframe> {
|
||||||
|
// `lastall`: the LAST keyframe for every element, bypassing the plateau
|
||||||
|
// rule entirely. This is what the shifted time reading predicts — under
|
||||||
|
// it the final pose is reached at a definite time and nothing follows,
|
||||||
|
// so "rest" needs no heuristic. Testing it against the captures is an
|
||||||
|
// independent check on that reading, from static composites rather than
|
||||||
|
// from animation timing.
|
||||||
|
if std::env::var("SYLPHEED_REST_RULE").as_deref() == Ok("lastall") {
|
||||||
|
return self.keyframes.last();
|
||||||
|
}
|
||||||
if let Some(k) = self.rest_plateau() {
|
if let Some(k) = self.rest_plateau() {
|
||||||
return Some(k);
|
return Some(k);
|
||||||
}
|
}
|
||||||
@@ -161,6 +199,26 @@ impl Element {
|
|||||||
0 => None,
|
0 => None,
|
||||||
1 => self.keyframes.first(),
|
1 => self.keyframes.first(),
|
||||||
n => {
|
n => {
|
||||||
|
// ⚠️ EXPERIMENT GATE, default off. Both published alternatives to
|
||||||
|
// the longest-dwell fallback died by argument rather than by
|
||||||
|
// measurement, and `compose` can score a rule against the live
|
||||||
|
// captures — so they are reachable here to be tested.
|
||||||
|
// SYLPHEED_REST_RULE=last -> the final keyframe
|
||||||
|
// SYLPHEED_REST_RULE=maxalpha -> the most opaque keyframe
|
||||||
|
match std::env::var("SYLPHEED_REST_RULE").as_deref() {
|
||||||
|
Ok("last") => return self.keyframes.last(),
|
||||||
|
Ok("maxalpha") => {
|
||||||
|
let mut best = (0usize, 0u32);
|
||||||
|
for (k, f) in self.keyframes.iter().enumerate() {
|
||||||
|
let a = (f.fade >> 24) & 0xff;
|
||||||
|
if a >= best.1 {
|
||||||
|
best = (k, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.keyframes.get(best.0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
let mut best = (0usize, 0u32);
|
let mut best = (0usize, 0u32);
|
||||||
for k in 0..n - 1 {
|
for k in 0..n - 1 {
|
||||||
let (Some(t0), Some(t1)) =
|
let (Some(t0), Some(t1)) =
|
||||||
@@ -227,7 +285,24 @@ impl Element {
|
|||||||
any = Some(i);
|
any = Some(i);
|
||||||
any_len = len;
|
any_len = len;
|
||||||
}
|
}
|
||||||
if j != n - 1 && len >= best_len {
|
// A trailing run is normally the EXIT and is excluded (see the
|
||||||
|
// doc comment) — but not always, and the tell is its ALPHA.
|
||||||
|
//
|
||||||
|
// An exit fades the element out, so its final keyframe is
|
||||||
|
// transparent: `pgptitle.rat`'s trailing run is `0x00ffffff`, and
|
||||||
|
// taking it erases the word PAUSE. An element with **no exit**
|
||||||
|
// ends on its hold, which is whatever it looks like on screen —
|
||||||
|
// opaque: `ptframe1`/`ptframe2` on the title main menu end on a
|
||||||
|
// run of three at `0xffffffff`, and excluding it dropped the
|
||||||
|
// bright circuit bracket the capture plainly shows.
|
||||||
|
//
|
||||||
|
// So a trailing run is the hold exactly when it is **visible**.
|
||||||
|
// (The port agent's report proposed "the final untimed keyframe
|
||||||
|
// has the same pose as the last timed one"; that is true of
|
||||||
|
// `pgptitle` too and would erase PAUSE, so it is the alpha and
|
||||||
|
// not the pose-equality that separates the two.)
|
||||||
|
let trailing_is_the_hold = j == n - 1 && (self.keyframes[i].fade >> 24) != 0;
|
||||||
|
if (j != n - 1 || trailing_is_the_hold) && len >= best_len {
|
||||||
best = Some(i);
|
best = Some(i);
|
||||||
best_len = len;
|
best_len = len;
|
||||||
}
|
}
|
||||||
@@ -247,6 +322,20 @@ pub struct UiBuild {
|
|||||||
pub elements: Vec<Element>,
|
pub elements: Vec<Element>,
|
||||||
/// Sprite name → (offset, size) of its `T8aD` child within the bundle.
|
/// Sprite name → (offset, size) of its `T8aD` child within the bundle.
|
||||||
pub sprites: HashMap<String, (usize, usize)>,
|
pub sprites: HashMap<String, (usize, usize)>,
|
||||||
|
/// Record name → (offset, size) of its nested `.rat` **leaf** within the
|
||||||
|
/// bundle, e.g. `ptbtn01f.rat`.
|
||||||
|
///
|
||||||
|
/// Exposed because a leaf is where a focused button's extra elements live —
|
||||||
|
/// `ptbtn0Nf.rat` declares the focus ring `ptbtneff01.t32` **and** the bright
|
||||||
|
/// label, and the parent bundle declares no element for the `f` record at
|
||||||
|
/// all. A consumer that walks only top-level elements cannot see either.
|
||||||
|
///
|
||||||
|
/// A leaf is itself readable by [`parse_build`]: its first 32 bytes have the
|
||||||
|
/// same shape as a bundle header, so
|
||||||
|
/// `parse_build(&bundle[off..off + size])` returns its elements with
|
||||||
|
/// absolute design-space placements. See
|
||||||
|
/// `docs/re/structures/ui-button-focus-record.md`.
|
||||||
|
pub records: HashMap<String, (usize, usize)>,
|
||||||
/// A guessed context from the sprite naming (e.g. `"tutorial"`), if any.
|
/// A guessed context from the sprite naming (e.g. `"tutorial"`), if any.
|
||||||
pub context_hint: Option<String>,
|
pub context_hint: Option<String>,
|
||||||
/// True when the declaration table was unusable and the build was recovered
|
/// True when the declaration table was unusable and the build was recovered
|
||||||
@@ -404,6 +493,8 @@ fn mark_focused_states(elements: &mut [Element]) {
|
|||||||
/// Read the placement region that follows the declaration table, filling in each
|
/// Read the placement region that follows the declaration table, filling in each
|
||||||
/// element's keyframe group.
|
/// element's keyframe group.
|
||||||
fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
||||||
|
// Experiment gate, default off; see the `time` field below.
|
||||||
|
let shift_times = std::env::var("SYLPHEED_KF_TIME_SHIFT").as_deref() == Ok("1");
|
||||||
let count = elements.len();
|
let count = elements.len();
|
||||||
let mut order = Vec::with_capacity(count);
|
let mut order = Vec::with_capacity(count);
|
||||||
let mut pos = DECL_TABLE_AT + count * DECL_ENTRY;
|
let mut pos = DECL_TABLE_AT + count * DECL_ENTRY;
|
||||||
@@ -430,13 +521,25 @@ fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
|||||||
}
|
}
|
||||||
group.push(Keyframe {
|
group.push(Keyframe {
|
||||||
fade: be32(bundle, blk),
|
fade: be32(bundle, blk),
|
||||||
|
rotation_deg: be32(bundle, blk + 12) as i32,
|
||||||
|
unknown_4: be32(bundle, blk + 4) as i32,
|
||||||
|
unknown_8: be32(bundle, blk + 8) as i32,
|
||||||
scale_x: be32(bundle, blk + 16),
|
scale_x: be32(bundle, blk + 16),
|
||||||
scale_y: be32(bundle, blk + 20),
|
scale_y: be32(bundle, blk + 20),
|
||||||
tint: be32(bundle, blk + 24),
|
tint: be32(bundle, blk + 24),
|
||||||
x: be32(bundle, blk + 28) as i32,
|
x: be32(bundle, blk + 28) as i32,
|
||||||
y: be32(bundle, blk + 32) as i32,
|
y: be32(bundle, blk + 32) as i32,
|
||||||
// Only a block wholly inside the group carries a time.
|
// Only a block wholly inside the group carries a time.
|
||||||
time: (blk + 40 <= group_end).then(|| be32(bundle, blk + 36)),
|
//
|
||||||
|
// ⚠️ Which block a time word BELONGS TO is under test — see
|
||||||
|
// `docs/re/ui-keyframe-time-unit.md`. Set `SYLPHEED_KF_TIME_SHIFT=1`
|
||||||
|
// to read `W[k-1]` as block `k`'s time ("the word is the time the
|
||||||
|
// NEXT pose is reached") instead of `W[k]`. Default is unchanged.
|
||||||
|
time: if shift_times {
|
||||||
|
(k >= 1).then(|| be32(bundle, blk - KEYFRAME + 36))
|
||||||
|
} else {
|
||||||
|
(blk + 40 <= group_end).then(|| be32(bundle, blk + 36))
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
elements[idx].keyframes = group;
|
elements[idx].keyframes = group;
|
||||||
@@ -510,6 +613,7 @@ pub fn parse_build(bundle: &[u8]) -> Option<UiBuild> {
|
|||||||
design_h,
|
design_h,
|
||||||
elements,
|
elements,
|
||||||
sprites,
|
sprites,
|
||||||
|
records,
|
||||||
context_hint,
|
context_hint,
|
||||||
from_fallback,
|
from_fallback,
|
||||||
placement_order,
|
placement_order,
|
||||||
@@ -539,6 +643,9 @@ fn fallback_elements(bundle: &[u8], records: &HashMap<String, (usize, usize)>) -
|
|||||||
pivot_y: be32(rec, 0x54),
|
pivot_y: be32(rec, 0x54),
|
||||||
keyframes: vec![Keyframe {
|
keyframes: vec![Keyframe {
|
||||||
fade: 0xffff_ffff,
|
fade: 0xffff_ffff,
|
||||||
|
rotation_deg: 0,
|
||||||
|
unknown_4: 0,
|
||||||
|
unknown_8: 0,
|
||||||
scale_x: 100,
|
scale_x: 100,
|
||||||
scale_y: 100,
|
scale_y: 100,
|
||||||
tint,
|
tint,
|
||||||
@@ -811,8 +918,16 @@ pub fn compose(
|
|||||||
// Measured paint order when one exists for this build, declaration order
|
// Measured paint order when one exists for this build, declaration order
|
||||||
// otherwise — see `measured_paint_order`.
|
// otherwise — see `measured_paint_order`.
|
||||||
// Measured order when this build is one of the two read off the running
|
// Measured order when this build is one of the two read off the running
|
||||||
// game; otherwise the order DERIVED from the sprites' layer keys, which
|
// game; otherwise the order DERIVED from the sprites' layer keys.
|
||||||
// reproduces both measured orders up to ties.
|
//
|
||||||
|
// ✅ Checked 2026-08-29 (`examples/paint_order_audit.rs`), because the
|
||||||
|
// previous wording here — "reproduces both measured orders up to ties" —
|
||||||
|
// was unmeasured and stale by one: there are THREE measured orders. The
|
||||||
|
// derived order reproduces the main menu and the developer splash EXACTLY
|
||||||
|
// (0 inverted pairs each) and differs on the title by 8 pairs, all of them
|
||||||
|
// same-layer-key ties, two being total occlusions. Of the port's five
|
||||||
|
// screens only `EXTRAS` rests on a derived order with ties: 15 tied pairs,
|
||||||
|
// 2 overlapping. See docs/re/structures/ui-paint-order-derived-check.md.
|
||||||
let order: Vec<usize> =
|
let order: Vec<usize> =
|
||||||
measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle));
|
measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle));
|
||||||
for &ei in &order {
|
for &ei in &order {
|
||||||
@@ -867,6 +982,13 @@ pub fn compose(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(sprite) = el.sprite.as_ref() else {
|
let Some(sprite) = el.sprite.as_ref() else {
|
||||||
|
// Report it, do not just skip it. This arm used to `continue`
|
||||||
|
// silently while the two arms below recorded into `missing`, so when
|
||||||
|
// a name-decoding defect left every menu screen's background
|
||||||
|
// unresolved, `screen render` still said "sprites that did not
|
||||||
|
// resolve: none". A diagnostic with a hole in it is worse than none.
|
||||||
|
// See docs/re/structures/ratc-child-names.md.
|
||||||
|
missing.push(format!("{} (element declares no resolvable sprite)", el.name));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(&(off, size)) = build.sprites.get(sprite) else {
|
let Some(&(off, size)) = build.sprites.get(sprite) else {
|
||||||
@@ -919,8 +1041,11 @@ fn fill_quad(
|
|||||||
if sw == 0 || sh == 0 {
|
if sw == 0 || sh == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
|
// `scale = 0` is COLLAPSED TO NOTHING, not "unset" — see `blit`.
|
||||||
let sy_pct = if kf.scale_y == 0 { 100 } else { kf.scale_y };
|
let (sx_pct, sy_pct) = (kf.scale_x, kf.scale_y);
|
||||||
|
if sx_pct == 0 || sy_pct == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let dw = (sw * sx_pct / 100).max(1);
|
let dw = (sw * sx_pct / 100).max(1);
|
||||||
let dh = (sh * sy_pct / 100).max(1);
|
let dh = (sh * sy_pct / 100).max(1);
|
||||||
let ox = kf.x - (pivot_x as i32 * (sx_pct as i32 - 100)) / 100;
|
let ox = kf.x - (pivot_x as i32 * (sx_pct as i32 - 100)) / 100;
|
||||||
@@ -977,8 +1102,16 @@ fn blit(
|
|||||||
if sw == 0 || sh == 0 {
|
if sw == 0 || sh == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
|
// `scale = 0` means COLLAPSED TO NOTHING, not "unset". This used to coerce
|
||||||
let sy_pct = if kf.scale_y == 0 { 100 } else { kf.scale_y };
|
// 0 → 100 %, which drew a fully-collapsed element at full size. The disc
|
||||||
|
// settles it: of 15 493 elements with a keyframe group, **2 166 have at
|
||||||
|
// least one zero-scale keyframe and not one has zero on every keyframe** —
|
||||||
|
// and 1 762 of them grow back out of it (`ptlogo_eff3.t32` runs 0 % → 200 %).
|
||||||
|
// An "unset" marker that no element ever uses throughout is not a marker.
|
||||||
|
let (sx_pct, sy_pct) = (kf.scale_x, kf.scale_y);
|
||||||
|
if sx_pct == 0 || sy_pct == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let dw = (sw * sx_pct / 100).max(1);
|
let dw = (sw * sx_pct / 100).max(1);
|
||||||
let dh = (sh * sy_pct / 100).max(1);
|
let dh = (sh * sy_pct / 100).max(1);
|
||||||
// Keep the pivot point fixed as the element scales.
|
// Keep the pivot point fixed as the element scales.
|
||||||
@@ -1032,6 +1165,10 @@ fn blit(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let di = ((ty as u32 * cw + tx as u32) * 4) as usize;
|
let di = ((ty as u32 * cw + tx as u32) * 4) as usize;
|
||||||
|
// Straight alpha-over. `T8aD +0x04` bit 0x02 was tested as an
|
||||||
|
// ADDITIVE selector and REFUTED — it moved every metric against the
|
||||||
|
// title capture the wrong way (see the doc comment on
|
||||||
|
// `T8adImage::flags`), so the bit is carried but not acted on.
|
||||||
for (k, sc) in [sr, sg, sb].into_iter().enumerate() {
|
for (k, sc) in [sr, sg, sb].into_iter().enumerate() {
|
||||||
let dc = canvas[di + k] as u32;
|
let dc = canvas[di + k] as u32;
|
||||||
canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8;
|
canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8;
|
||||||
@@ -1084,6 +1221,9 @@ mod tests {
|
|||||||
fn kf(x: i32, y: i32, time: u32) -> Keyframe {
|
fn kf(x: i32, y: i32, time: u32) -> Keyframe {
|
||||||
Keyframe {
|
Keyframe {
|
||||||
fade: 0xffff_ffff,
|
fade: 0xffff_ffff,
|
||||||
|
rotation_deg: 0,
|
||||||
|
unknown_4: 0,
|
||||||
|
unknown_8: 0,
|
||||||
scale_x: 100,
|
scale_x: 100,
|
||||||
scale_y: 100,
|
scale_y: 100,
|
||||||
tint: 0xffff_ffff,
|
tint: 0xffff_ffff,
|
||||||
@@ -1197,12 +1337,16 @@ mod tests {
|
|||||||
// leaves the top-left quadrant bare. See
|
// leaves the top-left quadrant bare. See
|
||||||
// `docs/re/structures/ui-rat-layout.md`.
|
// `docs/re/structures/ui-rat-layout.md`.
|
||||||
let img = t8ad::T8adImage {
|
let img = t8ad::T8adImage {
|
||||||
|
flags: 0,
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 360,
|
height: 360,
|
||||||
rgba: vec![255u8; 640 * 360 * 4],
|
rgba: vec![255u8; 640 * 360 * 4],
|
||||||
};
|
};
|
||||||
let k = Keyframe {
|
let k = Keyframe {
|
||||||
fade: 0xffff_ffff,
|
fade: 0xffff_ffff,
|
||||||
|
rotation_deg: 0,
|
||||||
|
unknown_4: 0,
|
||||||
|
unknown_8: 0,
|
||||||
scale_x: 200,
|
scale_x: 200,
|
||||||
scale_y: 200,
|
scale_y: 200,
|
||||||
tint: 0xffff_ffff,
|
tint: 0xffff_ffff,
|
||||||
@@ -1230,6 +1374,7 @@ mod tests {
|
|||||||
// `ptcopyright.t32` is 694x20 at (293,655), and the capture's glyph run
|
// `ptcopyright.t32` is 694x20 at (293,655), and the capture's glyph run
|
||||||
// starts at x = 295 — inside that rect, not offset by a pivot.
|
// starts at x = 295 — inside that rect, not offset by a pivot.
|
||||||
let img = t8ad::T8adImage {
|
let img = t8ad::T8adImage {
|
||||||
|
flags: 0,
|
||||||
width: 694,
|
width: 694,
|
||||||
height: 20,
|
height: 20,
|
||||||
rgba: vec![255u8; 694 * 20 * 4],
|
rgba: vec![255u8; 694 * 20 * 4],
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
# `sylph-agent`). They are never printed, never logged, and never passed on a
|
# `sylph-agent`). They are never printed, never logged, and never passed on a
|
||||||
# command line.
|
# command line.
|
||||||
#
|
#
|
||||||
# push-work push the current branch
|
# push-work push the current branch, and any annotated tags on it
|
||||||
|
#
|
||||||
|
# --follow-tags publishes ANNOTATED tags reachable from the pushed commits. That
|
||||||
|
# is what makes a pinned decoder state durable: the port depends on commits of
|
||||||
|
# ours by revision, and a commit reachable only from a topic branch is orphaned
|
||||||
|
# by a squash-merge. Lightweight tags are deliberately not pushed.
|
||||||
# push-work --dry-run say what it would do
|
# push-work --dry-run say what it would do
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -50,7 +55,17 @@ if [ ! -s "$HOME/.git-credentials" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# `store` reads the file we mounted; nothing is written back (it is read-only).
|
# `store` reads the file we mounted; nothing is written back (it is read-only).
|
||||||
git config --local credential.helper "store --file=$HOME/.git-credentials"
|
# Applied to THIS COMMAND ONLY, via `-c`, never `git config --local`.
|
||||||
|
#
|
||||||
|
# Writing it to --local config persists it in the repository, and this repo is a
|
||||||
|
# bind mount the host also uses -- so the host's git inherited
|
||||||
|
# `store --file=/sylph-home/re/.git-credentials`, a path that exists only inside
|
||||||
|
# the container, and every host push then failed with
|
||||||
|
# `unable to get credential storage lock: No such file or directory`.
|
||||||
|
#
|
||||||
|
# A tool that configures a shared repository to suit itself breaks every other
|
||||||
|
# user of that repository. Keep it to the invocation.
|
||||||
|
CRED_HELPER="store --file=$HOME/.git-credentials"
|
||||||
|
|
||||||
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
|
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
|
||||||
echo "push-work: $branch — $ahead commit(s) to publish"
|
echo "push-work: $branch — $ahead commit(s) to publish"
|
||||||
@@ -63,5 +78,5 @@ fi
|
|||||||
# --force-with-lease is deliberately NOT offered. If this is rejected as
|
# --force-with-lease is deliberately NOT offered. If this is rejected as
|
||||||
# non-fast-forward, someone else moved the branch: fetch and merge, do not
|
# non-fast-forward, someone else moved the branch: fetch and merge, do not
|
||||||
# overwrite.
|
# overwrite.
|
||||||
git push --set-upstream origin "$branch"
|
git -c "credential.helper=$CRED_HELPER" push --follow-tags --set-upstream origin "$branch"
|
||||||
echo "push-work: pushed $branch"
|
echo "push-work: pushed $branch"
|
||||||
|
|||||||
@@ -120,3 +120,13 @@ turn ends — but sequential tool calls within a turn are fine.
|
|||||||
|
|
||||||
The loop runs on a fixed interval set by the harness, so you do **not** need to
|
The loop runs on a fixed interval set by the harness, so you do **not** need to
|
||||||
arm the next wakeup yourself. Spend that attention on the write-up instead.
|
arm the next wakeup yourself. Spend that attention on the write-up instead.
|
||||||
|
|
||||||
|
**And never stop it.** Do not call `ScheduleWakeup` at all — not to re-pace the
|
||||||
|
loop, not to tidy up, and above all not with `stop`. Ending the loop ends the
|
||||||
|
run: the container exits, and the next iteration never happens. If the cadence
|
||||||
|
is genuinely wrong, say so in your reply and leave it to a human — the interval
|
||||||
|
is set outside this prompt and is not yours to optimise.
|
||||||
|
|
||||||
|
This is not hypothetical. A run ended at 2026-08-29 04:0x with a clean exit 0
|
||||||
|
while the display title read "Loop interval optimization", leaving four files of
|
||||||
|
work uncommitted in the tree.
|
||||||
|
|||||||
@@ -27,26 +27,488 @@ authored version can be deleted.
|
|||||||
|
|
||||||
| | Question | State | Answer / link |
|
| | Question | State | Answer / link |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Q1 | keyframe time unit + ramp shape | ❔ open | blocks all animation |
|
| Q1 | keyframe time unit + ramp shape | ✅ answered, 🟡 one gap | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` — settled**, the idle title presents at 28.5 fps so the game is 30 Hz. 🟡 **The interpolation law is settled; the group TIMELINE for multi-keyframe elements is not** — `palogo_gamearts` is still at full alpha 9 frames after its declared `a=32`, and its declared 80-frame fade-in never draws — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md). ✅ **REPLICATED 2026-08-29 — for ANIMATION, read `+36` as the time the NEXT pose is reached.** Three elements across two screens: `palogo_gamearts` and `palogo_seta` hold full alpha for **83 frames** and `palogo_sqex` for **≥77**, where the current reading predicts **6–8** and the shifted one **80–102**. The elements that cannot discriminate (the `_eff` glows, on which the linear law was measured) fit both. ⚠️ Our decoder still defaults to the other reading (`SYLPHEED_KF_TIME_SHIFT=1` to flip) because it changes `rest()` on one element — but that is an unsound fallback guessing either way, so **static rendering is unaffected and animation timing should use the shift** |
|
||||||
| Q2 | which build is which screen state | 🟡 partial | build 4 title, 5 main menu, 6/8/9 submenus, `palogo` splash — unconfirmed against captures |
|
| Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`ui-title-build-map.md`](../re/ui-title-build-map.md) |
|
||||||
| Q3 | paint order for the six screens | ❔ open | runtime-solved only; declaration table is refuted |
|
| Q3 | paint order for the six screens | ✅ answered, ❔ tie-break | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md). ⚠️ **The key does not fully order a screen**: elements sharing a key are tied, and the tie-break is ❔ **undecodable from the bundle** — declaration table, `T8aD` header (exhaustive: every offset 0x00–0x7f at u8/u16/u32, both directions, **0** fields match the measured order against **64** for the control) and the RATC child order all give the same order the game does *not* use. Your exposure is **2 overlapping tied pairs on `EXTRAS`** — [`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md) |
|
||||||
| Q4 | button → GamePart | ❔ open | labels are baked into sprites |
|
| Q4 | button → GamePart | ✅ answered | **measured** which screen all **5** buttons open — `NEW GAME` → `DIFFICULTY` → `SELECT DATA`, not a hang. The **GamePart id is still a name match**, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||||||
| Q5 | navigation semantics | ❔ open | |
|
| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||||||
| Q6 | boot sequence + what drives it | 🟡 partial | order observed; the driver is not decoded |
|
| Q6 | boot sequence + what drives it | ✅ answered | sequence **measured** end to end; the driver is **code, not data** — four search spaces closed, so the port **authors** the sequence — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
|
||||||
| Q7 | transitions | ❔ open | |
|
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
|
||||||
| Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not |
|
| Q8 | menu audio bindings | ✅ answered | cue vocabulary + bank **decoded**; event binding is a **name match** (the authors' own event names). ✅ **You CAN have the SE audio** — ⚠️ an earlier version of this row said it was "undecodable from the disc"; that was **retracted** and the row was stale. Three cues are located in `Static.slb` and **decode to PCM**: d-pad move `0x1ec0` (4 packets), Ⓑ back `0x0ec0` (2), Ⓐ confirm `0x5d6c0` (6), all mono 48 kHz. The bank is a packed run of XMA waves with no delimiter, so a wave is only (offset, packet count) — and ⚠️ the file order is **not** cue-id order, so the index cannot be counted out — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |
|
||||||
| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified |
|
| Q9 | video binding + playback rules | ✅ answered | **decoded** from the movie manifest: `ADVERTISE_MOVIE`→`ADV.wmv` (boot intro *and* attract are one asset), `MS00A`→`S00A.wmv` is the new-game intro, `STAFF_ROLL`→the credits reel. ✅ **one Ⓐ skips a movie** (title at 57 s vs a 193 s baseline) — [`movie-binding.md`](../re/movie-binding.md) |
|
||||||
| Q10 | music-bank sub-wave roles (intro+loop?) | ❔ open | we concatenate blindly today |
|
| Q10 | music-bank sub-wave roles (intro+loop?) | ✅ answered | **two stems of one performance, played together** — sample-synchronous, equal duration, 32/32 banks. **Concatenating is wrong.** Not a seamless loop either — [`structures/bgm-two-stems.md`](../re/structures/bgm-two-stems.md) |
|
||||||
| S1 | Ready Room go/no-go | ❔ open | probe not run |
|
| S1 | Ready Room go/no-go | ✅ **no-go** | it is 2D and enumerates fine (60 builds), but `GP_READY_ROOM.pak` holds **briefing/tactical-map** content, not the six-button Ready Room menu — [`ready-room-probe.md`](../re/ready-room-probe.md) |
|
||||||
|
|
||||||
## Already settled — the port can rely on these today
|
## Already settled — the port can rely on these today
|
||||||
|
|
||||||
* **`GP_TITLE.pak` is the whole title-side tree.** Build 4 is the title with the
|
### ⬅ Answers to the port's five asks (2026-08-29)
|
||||||
animating wordmarks, build 5 the five-button main menu, builds 6/8/9 are
|
|
||||||
submenus, and the developer splash is the `palogo` bundle in the same archive.
|
* **1 — how to recognise the splash.** ❔ **No content rule exists; you are
|
||||||
✅ decoded (enumeration), 🟡 the state labels are not yet capture-confirmed.
|
authoring this.** Design size fails (every extra composable bundle sampled is
|
||||||
|
1280×720, same as every screen) and element count fails (the fragments run
|
||||||
|
2…15 elements, the splash halves have 3 and 7 — the ranges overlap).
|
||||||
|
✅ **But `GP_TITLE` needs no rule.** There, `--all` adds exactly **four**
|
||||||
|
bundles and all four are real screens — no fragments at all — and the `--all`
|
||||||
|
index equals the pak **entry** index 1:1 across all 16, so addressing by entry
|
||||||
|
index does not mean something different from elsewhere.
|
||||||
|
🔴 **And there are TWO splash screens; you have one.** Entries **11/14** are the
|
||||||
|
developer logos (GAME ARTS / SETA / studio anima). Entries **10/13** are the
|
||||||
|
**SQUARE ENIX publisher** wordmark — the *first* thing the boot shows — and you
|
||||||
|
do not have them. The pairs are region twins (`™` on 10, `®` on 13). All four
|
||||||
|
draw every element they declare.
|
||||||
|
[`ui-splash-addressing.md`](../re/ui-splash-addressing.md) ·
|
||||||
|
[render grid](../re/captures/title-builds/splash-both-halves-rendered.png)
|
||||||
|
|
||||||
|
* **2 — the ~0.4 s fade-out is (a)**, and it is bigger than the fade quad.
|
||||||
|
Every element of a screen ends on **exactly one** untimed keyframe — so there
|
||||||
|
is one unknown duration per screen, which rules out (b). That final block is
|
||||||
|
where the screen plays out: the quad goes `a=255` (black) while the buttons,
|
||||||
|
`ptmsg` and the glows go `a=0` and the two frames hold. (c) is refuted by a
|
||||||
|
null test on the capture: a black quad alone keeps the button÷background
|
||||||
|
brightness **ratio constant**, and measured through the fade it falls
|
||||||
|
**6.50 → 1.94, a 3.4× monotonic drop**. So: write one authored constant
|
||||||
|
(~0.4 s / ~24 units) and **play the group to its end on every element** — do
|
||||||
|
not fade a black rectangle over a frozen screen.
|
||||||
|
[`screen-transitions.md`](../re/screen-transitions.md)
|
||||||
|
|
||||||
|
* **3 — focus: your choice is fine, and it is not your bug.** ✅ The focused
|
||||||
|
sprite **completely covers** the base — `f` alpha ≥ base alpha at **100.0 %** of
|
||||||
|
base-visible pixels on three pairs across both languages, once aligned properly
|
||||||
|
(the true offset is **(7,7)**, and at the centre alignment it reads a
|
||||||
|
misleading 78–84 %). Compositing both ways differs by **RMSE 1.1 inside the
|
||||||
|
button rectangle**, max 12/255 on ~25 px — unmeasurable at frame level.
|
||||||
|
🔴 **What you are actually missing is the focus record's SECOND element.**
|
||||||
|
`ptbtn0Nf.rat` declares **two** sprites — `ptbtneff01.t32` (a 42×46 **glowing
|
||||||
|
ring**, focus only) then `ptbtn0Nf.t32` (the bright label) — where the base
|
||||||
|
record declares one. That ring is the marker you say you draw nowhere. ⚠️ Note
|
||||||
|
the small dot-in-circle at each underline's left end is *not* it: that is on
|
||||||
|
every button all the time, part of the base art.
|
||||||
|
[`structures/ui-button-focus-record.md`](../re/structures/ui-button-focus-record.md)
|
||||||
|
|
||||||
|
* **4 — rotation: not mine to decide alone.** Raised with the human; see
|
||||||
|
MISSION. What I can say without a decision: the two sub-questions are not
|
||||||
|
equally open. Rotation is about the **declared pivot** — that anchor is
|
||||||
|
*measured*, not assumed: the title's two `ptloop` sweeps scale 600 %/800 %
|
||||||
|
vertically, where the pivot term is worth 450 and 630 px, and the GPU capture
|
||||||
|
puts both quad centres at y **359.1**/**360.0** against the pivot formula's
|
||||||
|
**360.0**; top-left anchoring predicts 810/990 and centre-as-position 270. So
|
||||||
|
if you draw rotation, rotate about the declared pivot.
|
||||||
|
⚠️ It changes nothing on your five screens **at rest** — they have zero
|
||||||
|
top-level rotations, and the title's two nested ones sit entirely off-screen at
|
||||||
|
rest. [`structures/ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||||||
|
|
||||||
|
* **5 — the capture is not gamma-neutral, and RMSE against it has a floor.**
|
||||||
|
Measured on flat patches (16×16, both images `std < 8`):
|
||||||
|
`capture ≈ 255·(render/255)^γ` with **γ ≈ 1.49** (main menu), **1.49**
|
||||||
|
(`EXTRAS`), **1.34** (title). The chain says this is a ramp **the game
|
||||||
|
installed**, not a capture-path artefact: canary's swap-path gamma stage is a
|
||||||
|
pure 256-entry LUT that defaults to identity, and the game is measured calling
|
||||||
|
`VdGetCurrentDisplayGamma` once at video init. ⚠️ **Reach: the flat patches are
|
||||||
|
almost all dark (render ~0–60), so nothing here constrains midtones or
|
||||||
|
highlights** — which is where γ 1.4 does its visible work. So: yes, there is a
|
||||||
|
floor; a γ ≈ 1.4 darkening gets closer and is **authored**, best applied where
|
||||||
|
it was measured rather than extrapolated. Do not chase RMSE below it.
|
||||||
|
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||||||
|
|
||||||
|
|
||||||
|
* **`GP_TITLE.pak` is eight screens, each shipped twice — English and Japanese.**
|
||||||
|
Build 4 is the English title art and 7 its Japanese twin; **2/3 are the
|
||||||
|
`PRESS Ⓐ BUTTON` plate, a build of their own** composited over the title and
|
||||||
|
faded in a beat later; 5/8 are the five-button main menu; 6/9 are the `EXTRAS`
|
||||||
|
submenu — the **only** submenu inside this archive. Builds 0/1 and 10/11 are a
|
||||||
|
`DELTASABER / SYLPHEED A.I.` plate that was **never seen running**, in the boot
|
||||||
|
path, any title-side screen, or the attract loop. ✅ measured against live
|
||||||
|
captures for the four English screens the boot path shows;
|
||||||
|
[`ui-title-build-map.md`](../re/ui-title-build-map.md).
|
||||||
|
**Withdrawn:** the earlier "builds 6/8/9 are submenus" — 8 is the Japanese main
|
||||||
|
menu. The other four main-menu buttons leave the archive, and where each one
|
||||||
|
goes is **measured** — see the button-destination bullet below.
|
||||||
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
|
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
|
||||||
decoration, `0x10` = primitive. ✅ decoded.
|
decoration, `0x10` = primitive. ✅ decoded **for the title-side screens**.
|
||||||
|
⚠️ `0x3002` is one member of a `0x3000` family with sub-bits, and it is not the
|
||||||
|
only button kind on the disc: `GP_READY_ROOM`'s 902 bundles contain **zero**
|
||||||
|
`0x3002` and use `0x3000` / `0x3004` / `0x300c` / `0x3008` instead. Nothing in
|
||||||
|
this milestone changes — every screen in scope is `GP_TITLE` — but do not ship
|
||||||
|
`kind == 0x3002` as a general button test.
|
||||||
|
(The kind is the 4th `u32` of the 60-byte declaration entry, at `+40`.)
|
||||||
|
* **The title's settled pose is `rest` — and always pass `--primitives`.**
|
||||||
|
Against a plate-free capture of the real screen, `screen render --build 4
|
||||||
|
--black` edge-correlates **0.9163 at (0,0)**, so the geometry of `rest` is the
|
||||||
|
arrived pose; the timeline is not needed for the title.
|
||||||
|
⚠️ **But without `--primitives` the whole frame is +13.14 too bright** (R +12.35,
|
||||||
|
G +13.58, B +13.48); with them, **+0.55**. The missing element is
|
||||||
|
**`pteff02.prm`**, the 25 % dim, and `--primitives` is off by default. **That is
|
||||||
|
the "washed-out cyan slab"** — a dim that should be there and isn't, not a glow
|
||||||
|
that shouldn't. Because the art is blue-dominant, the shortfall reads cyan.
|
||||||
|
🔴 **A second, separate defect — and it is the "slab".** With the dim in place
|
||||||
|
the residual is localised to one band (y ≈ 112–225): the game draws the logo's
|
||||||
|
`Z` **swoosh thin with a pink/magenta edge**, our render draws it **thick and
|
||||||
|
solid white**. Crop:
|
||||||
|
[`title-swoosh-capture-vs-render.png`](../re/captures/title-builds/title-swoosh-capture-vs-render.png).
|
||||||
|
The elements are `ptlogo_back2.t32`, `ptlogo_back2eff.t32` and
|
||||||
|
`ptlogo_back2eff1…5`. 🟡 Those five are also the group with the **known
|
||||||
|
unsolved paint-order tie-break** (key `0x8083`) — same screen, same elements —
|
||||||
|
but a blend-order swap explains white-instead-of-pink poorly.
|
||||||
|
🔴 **The pivot mismatch is NOT the cause — checked and refuted.** These elements'
|
||||||
|
declared pivots really do belong to the *other language's* sprite
|
||||||
|
(`ptlogo_back2`'s `(500,117)` is exactly half the Japanese 1000×234, not its own
|
||||||
|
English 1118×262; 24 of 109 title elements are off by > 8 px). **But it cannot
|
||||||
|
affect this render**: `blit` sizes a sprite from its **texture**, and applies the
|
||||||
|
pivot only as `kf.x − pivot·(scale−100)/100` — and **all seven swoosh elements
|
||||||
|
are scale `(100,100)` at every keyframe**, so the term is zero.
|
||||||
|
✅ **That formula is now MEASURED, not just implemented (2026-08-28).** The
|
||||||
|
title's two `ptloop` sweeps scale **600 %** and **800 %** vertically, where the
|
||||||
|
pivot term is worth 450 and 630 px, and the GPU capture puts both quad centres
|
||||||
|
at y **359.1** and **360.0** — against the formula's **360.0** for both.
|
||||||
|
Top-left anchoring predicts 810 and 990; treating the position as the centre
|
||||||
|
predicts 270. Horizontally the same term makes the group's `t` solved from
|
||||||
|
**position** agree with `t` solved from **vertex alpha** to **0.33 / 0.65
|
||||||
|
units**, versus ~8 units without it. So: **anchor scaling on the pivot, not on
|
||||||
|
the top-left corner.**
|
||||||
|
🟡 It does *not* prove interpolation is linear — both fields were inverted
|
||||||
|
through the same linear map, so a shared easing curve would cancel. It does
|
||||||
|
show position and alpha ride **one shared parameter**.
|
||||||
|
⚠️ It *would* bite `ptlogo1`/`ptlogo2`, which scale 100 → 150 during the
|
||||||
|
build-in. Not at rest, and not on the swoosh.
|
||||||
|
🔴 **Ruled out for the COLOUR**: every swoosh keyframe's fade is `0x??ffffff`
|
||||||
|
(white RGB, alpha only), no tint is non-white, and the texture decodes
|
||||||
|
*blue*-leaning (175,174,198).
|
||||||
|
🟡 **What is left is the BLEND.** Size and position are the texture's own and
|
||||||
|
match; fade, tint and texture colour are all ruled out. Seven overlapping
|
||||||
|
mostly-transparent sprites (`ptlogo_back2` 5.4 % opaque, its glow 10.3 %, the
|
||||||
|
five `eff` segments 10–23 %, all white or warm) stacked with plain alpha-over
|
||||||
|
saturate to opaque white — which is exactly what we draw, and would read as
|
||||||
|
"thicker" against the game's thin coloured stroke.
|
||||||
|
🟡 **And there is a candidate field for it.** The `T8aD` header word at
|
||||||
|
**`+0x04`** splits the title's sprites exactly along effect-vs-normal:
|
||||||
|
`pteff01`, `pteff03a`, `ptlogo_back2eff1…5`, `ptlogoall_eff`/`_eff2` are
|
||||||
|
**`0x8832`**; `ptlogo1`/`2`, `ptlogo_tm`, `ptbase2`, `ptlogo_back2`,
|
||||||
|
`ptcopyright` and `ptlogo_back2eff` are **`0x8830`**. One bit — **`0x02`**.
|
||||||
|
Disc-wide it is a real independent flag: 19 216 sprites, 18 distinct values,
|
||||||
|
bit `0x02` set in **27.1 %**, and it toggles against otherwise-identical words
|
||||||
|
(`0x8830`/`0x8832`, `0x0830`/`0x0832`, `0x0810`/`0x0812`, `0x0030`/`0x0032`).
|
||||||
|
⚠️ **Correlation only — untested.** Nothing yet shows it *means* additive; the
|
||||||
|
test is to blend bit-`0x02` sprites additively and re-correlate the title
|
||||||
|
against the capture. Note `ptlogo_back2eff` is `0x8830` despite its name, so
|
||||||
|
this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: **not** "the name contains `eff`" — and not even the one-way "bit ⇒ `eff` name", which held 10/10 on the title but fails on **2 657 of 4 995** bit-set sprites disc-wide (`P(eff|set) = 0.468`). What survives is a 3.3× association, and the counterexamples are rings, glows and lights — effect-like art without the naming convention and **not** "the element is transient" (`pteff03`/`pteff03a` carry the bit and persist to `t=250`). 🛑 **Parked** — four candidate meanings are now dead (additive blend; `eff` name, both directions; transient element; **premultiplied alpha**, refuted because flagged sprites violate `RGB ≤ A` *more* than unflagged, 55.5 % vs 33.7 %) and none produced a positive account. It blocks nothing — your screens composite at 0.947 correlation against a capture without it. ⚠️ Practical note for an asset pipeline: `T8aD` headers appear in **RATC child order** (18/18 verified on build 4 against decoded dimensions), which is the only sound way to attribute a header to a name when two sprites share a size — and two here do.
|
||||||
|
⚠️ **The rotated draw is NOT the swoosh.** It was identified as the swoosh by
|
||||||
|
elimination; that is **refuted** — its quads span y −209…925 in screen space,
|
||||||
|
the swoosh is a band at y 126…360. ✅ **It is the two `ptloop` sweeps**
|
||||||
|
(`ptloop01.rat` → `pteff03.t32`, `ptloop02.rat` → `pteff03a.t32`), confirmed by
|
||||||
|
edge length: 400 × 1076 and 400 × 1444 against 399×180 at the two elements'
|
||||||
|
**different** declared scales, 600 % (= 1080) and 800 % (= 1440). What stands
|
||||||
|
from the old bullet: the game *does* submit rotated quads this compositor
|
||||||
|
cannot draw, and vertex colours are white.
|
||||||
|
✅ **SOLVED 2026-08-28 by draw capture — it is the GEOMETRY.** The game submits
|
||||||
|
the swoosh as **two rotated parallelograms** (edges `(0.54,−0.56)` and
|
||||||
|
`(0.44,0.79)`, ~45° and ~61°, extending to `y=±1.81` NDC).
|
||||||
|
`ui_layout::blit` draws **axis-aligned rectangles only**, so it blits the sprite
|
||||||
|
upright — right on average, right in position, wrong in shape, which is the
|
||||||
|
measured signature exactly. **A port that blits upright rects will have the same
|
||||||
|
defect.**
|
||||||
|
🔴 Vertex colour is refuted with it: every colour in the capture is
|
||||||
|
`<alpha>FFFFFF`, white RGB.
|
||||||
|
✅ **CLOSED 2026-08-28 — the rotation IS on the disc, at keyframe `+12`.**
|
||||||
|
It is a signed angle in **degrees**, clockwise-positive in screen space
|
||||||
|
(Y down), and `ui_layout::Keyframe` now carries it as `rotation_deg`.
|
||||||
|
Confirmed against the framebuffer, not against our own renderer: the two
|
||||||
|
`ptloop` records declare `+12` = **30** and **−45**, and the GPU capture
|
||||||
|
submits their quads at **+30.26°** and **−45.28°** — magnitude and sign, on two
|
||||||
|
different values. Corroborated separately by shape: `GP_BUNK` entry `117ca14f`
|
||||||
|
holds a group whose `+12` ramps **0 → 360** with position, scale and alpha all
|
||||||
|
constant — a spin in place. Disc-wide `+12` is non-zero in **14.50 %** of
|
||||||
|
83 862 keyframe blocks.
|
||||||
|
⚠️ **Two things the port must know about it.**
|
||||||
|
(1) **Rotation lives in BOTH the top-level table and nested `.rat` leaf
|
||||||
|
records.** ⚠️ I told you one iteration ago that it looked nested-only; that was
|
||||||
|
three archives' worth of pattern and **it is refuted** — `GP_DIALOG` and
|
||||||
|
`GP_DEBRIEFING_PILOTLOG` rotate top-level elements. The actionable half stands:
|
||||||
|
the *title's* rotations are nested, so a composer reading only the declaration
|
||||||
|
table gets zero rotation on exactly the elements that move there.
|
||||||
|
The clearest examples are top-level and show up in
|
||||||
|
`screen info --build 0 --geometry dat/GP_DIALOG.pak`: `pceff03.t32` and
|
||||||
|
`pceff04.t32` ramp `r=` **90 → 30 → 10 → 3 → 0** while their alpha ramps
|
||||||
|
0 → 255 and they slide into place — a swing-in that settles upright; and build
|
||||||
|
6's `pzeff02.t32` ramps **43 → 61 → 75 → 90** while scaling 112 % → 200 % and
|
||||||
|
fading to 0 — a spin-out burst.
|
||||||
|
(2) **`sylpheed-cli screen render` still does not rotate.** The field is
|
||||||
|
decoded, not rendered; `ui_layout::blit` is axis-aligned only. So the reference
|
||||||
|
renderer and the port will *both* draw these upright until a rotating blit
|
||||||
|
exists — and per your own rule, the two of them agreeing about it means
|
||||||
|
nothing.
|
||||||
|
✅ **Checked 2026-08-29: this does NOT affect your five screens at rest.**
|
||||||
|
Title, main menu, `EXTRAS` and both splash halves have **zero** top-level
|
||||||
|
elements with a non-zero rotation. The only rotations on any of them are the
|
||||||
|
title's two nested `ptloop` records (`r = 30` and `−45`), and at rest those sit
|
||||||
|
at `x = 1521` and `x = −839` — a 399-wide sprite entirely off both edges of a
|
||||||
|
1280 screen. So a static composite is unaffected; the caveat applies only if
|
||||||
|
you animate the title's build-in, where the sweeps cross the screen rotated.
|
||||||
|
🟡 The neighbouring words `+4` and `+8` are still unexplained: signed, non-zero
|
||||||
|
in ~4.8 % / 4.6 % of blocks, almost entirely `±180`/`±90`. That distribution
|
||||||
|
looks like a flip flag rather than a free angle, but nothing observed turns on
|
||||||
|
them — **do not transcribe them as X/Y rotation.**
|
||||||
|
[`ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||||||
|
⚠️ The earlier "pink versus white" reading compared two differently-shaped
|
||||||
|
renderings and should be re-checked after geometry, not carried as a separate
|
||||||
|
defect.
|
||||||
|
❔ **Classified: undecodable from the disc, with reach.** Seven candidates
|
||||||
|
eliminated — pivot (inert at scale 100 *and* no measured displacement), `fade`,
|
||||||
|
`tint`, texture colour, additive blend via `+0x04` bit `0x02`, and
|
||||||
|
capture-not-settled (the band is identical from t = 4.0 s to t = 21.5 s). The
|
||||||
|
residual is stable and modest: band mean **+1.83**, edge-corr **0.70** vs ≈ 0.92
|
||||||
|
frame-wide. A next attempt should use a **per-draw GPU capture** of the running
|
||||||
|
guest — not another field — but ⚠️ that capture records prim/indices/shader
|
||||||
|
hashes/**texture bindings**/**vertex attributes** and **no blend state**, so it
|
||||||
|
can test a per-draw *vertex colour* today and would need a Canary change to dump
|
||||||
|
`RB_BLENDCONTROL`. ✅ And the plate-free capture is sound; use it.
|
||||||
|
🔴 **Refuted:** it is *not* that our dim covers the whole frame instead of
|
||||||
|
sitting beneath the UI — the logo reads +2.36 against a background of −0.74, so
|
||||||
|
the paint order is being honoured.
|
||||||
|
|
||||||
|
* **The title's motion, decoded and attributed.** After building in, the *title
|
||||||
|
art* is essentially static — a 22 s capture measures the wordmark region at
|
||||||
|
sd 0.06 and the bottom-right corner at sd 0.003. Two things do move:
|
||||||
|
* ✅ **Two slow light sweeps in build 4.** `ptloop01.rat` (an `opt `-linked
|
||||||
|
`RATC` at `0xbb5966`) sweeps `pteff03.t32` left→right over **450 units =
|
||||||
|
7.5 s**; `ptloop02.rat` sweeps `pteff03a.t32` right→left over **570 units =
|
||||||
|
9.5 s**. Ordinary 40-byte keyframe blocks from `+0x68`, three keyframes each.
|
||||||
|
* ✅ **The `PRESS Ⓐ BUTTON` plate pulses**, and it is the loudest thing on
|
||||||
|
screen — a capture's per-tile amplitude map puts **sd 7.65** in the band
|
||||||
|
x ≈ 318–954, y ≈ 560–672 against **0.06** on the wordmark. It is
|
||||||
|
`ptbtn00f.rat`, the plate's highlight variant (build **2**, not build 4),
|
||||||
|
whose alpha ramps **`0x00 → 0x06 → 0x4a → 0x50` (hold) `→ 0x4a → 0x06 → 0x00`**
|
||||||
|
over **eight** keyframes at `t = 6, 29, 35, 50, 58, 97, 105, ?` — a glow that
|
||||||
|
fades in and back out, **closing on fully transparent**, so it is a complete
|
||||||
|
cycle rather than a one-shot ramp.
|
||||||
|
🟡 Its cycle **length** is not readable, and this is now observed rather than
|
||||||
|
assumed: the eighth block's time slot literally contains the ASCII terminator
|
||||||
|
`end `, so the record ends there and the value does not exist. Declared span is
|
||||||
|
therefore **≥ 105 units = 1.75 s** against a measured **≈ 2.3 s** — which would
|
||||||
|
need a final step of ≈ 33 units. That number is **fitted to the measurement, not
|
||||||
|
read**; the port should take ≈ 2.3 s as measured.
|
||||||
|
⚠️ An earlier version of this bullet said "the title screen loops at ≈ 2.2 s"
|
||||||
|
and attributed it to `ptloop01/02`. Both halves were wrong: it is the **plate**,
|
||||||
|
and it is a different build.
|
||||||
|
* 🟡 **Our composite is brighter than the emulator's frame — measured, and you
|
||||||
|
would be *authoring* if you apply it.** Alignment is exact (best offset
|
||||||
|
dy=0 dx=0, correlation **0.9466**), so only the tone differs. Fitting on 16×16
|
||||||
|
patches that are flat in **both** images: `capture ≈ 255·(render/255)^γ` with
|
||||||
|
γ = **1.491** (main menu), **1.493** (`EXTRAS`), **1.338** (title).
|
||||||
|
⚠️ **Narrow reach.** Those flat patches span only render values ~0–60, where a
|
||||||
|
gamma and a plain scale are nearly indistinguishable — on both menus the errors
|
||||||
|
are 0.28 vs 0.34 and 0.22 vs 0.28. Only the title separates them (1.08 vs
|
||||||
|
9.02). Nothing here constrains midtones or highlights.
|
||||||
|
🔴 The held-out control **failed to discriminate**: the splash's 2 918 flat
|
||||||
|
patches are pure black (render 0–4), so every model scores ≈ 0.
|
||||||
|
🔴 **My "it may be the emulator" caveat is withdrawn.** I said canary applies
|
||||||
|
`kernel_display_gamma_type = 2` (BT.709) on output. It does not — that cvar is
|
||||||
|
a value a **kStub getter reports to the guest** (`VdGetCurrentDisplayGamma`),
|
||||||
|
which the game uses to build its own ramp; canary then applies **the guest's**
|
||||||
|
ramp from the `DC_LUT` registers in the swap path (`apply_gamma_table.ps` /
|
||||||
|
`apply_gamma_pwl.ps`). So there is no emulator post-process to subtract, and
|
||||||
|
any gamma in a capture is one the game installed.
|
||||||
|
✅ **Measured 2026-08-29: the game DOES query the display gamma.** Booted with
|
||||||
|
Kernel logging on (`--log_mask=12 --log_level=3`, which changes nothing about
|
||||||
|
the output), `VdGetCurrentDisplayGamma` is called once at video init, between
|
||||||
|
`VdGetSystemCommandBuffer` and `VdSetDisplayMode` — the moment a ramp-builder
|
||||||
|
would ask. Control in the same log: 359 `VdRetrainEDRAM` lines, so an absent
|
||||||
|
call would have shown.
|
||||||
|
🟡 **That it then writes the ramp is inferred, not observed** — but the chain
|
||||||
|
is closed: canary's swap-path gamma stage is a **pure 256-entry LUT** with no
|
||||||
|
other transfer (`apply_gamma_table.xesli`), and that LUT **defaults to
|
||||||
|
identity** (`CommandProcessor::Initialize`, whose comment says the linear
|
||||||
|
default is "what games set when starting with the sRGB return value"). Identity
|
||||||
|
cannot produce the measured γ ≈ 1.4, so a non-identity ramp was written.
|
||||||
|
⚠️ The weak joint is that this assumes our composite reproduces the *pre-ramp*
|
||||||
|
framebuffer; what carries it is that a systematic ~1.4 across three screens is
|
||||||
|
not the shape of a compositor bug. A fixed sRGB stage does not fit either
|
||||||
|
direction (encode brightens; decode darkens far more).
|
||||||
|
**Practical upshot for you is unchanged:** the darkening is the game's own
|
||||||
|
display ramp, so it belongs in a port as a display profile, not baked in.
|
||||||
|
⚠️ Note the ramp depends on the display type the game is *told*; canary
|
||||||
|
hard-codes TV/BT.709, which on hardware is a console setting. **So this is a
|
||||||
|
display profile, not a fixed property of the game** — reasonable to expose as a
|
||||||
|
setting rather than bake in.
|
||||||
|
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||||||
|
|
||||||
|
* ✅ **FIXED 2026-08-29 — the dropped background was a name-decoding defect, and
|
||||||
|
`screen render` now draws it.** The reference composites for all five screens
|
||||||
|
changed; regenerate anything you diffed against before that date. Cause: a
|
||||||
|
RATC child's name is stated by an **`opt ` block** immediately before it
|
||||||
|
(`"opt " | BE32 len | name | NUL | 3 bytes | magic`), and our parser instead
|
||||||
|
guessed it from the last printable run of bytes. For this one child the 3
|
||||||
|
trailing bytes are `38 41 58` = `"8AX"`, which beat the real name
|
||||||
|
`pteff05.t32`. **`8AX` was never a name** — earlier text on this page treating
|
||||||
|
it as one was wrong. Disc-wide: 18 002 children, 17 918 already agreed with the
|
||||||
|
`opt ` reading and **24 did not**, every one the same 3-byte-tail failure.
|
||||||
|
Effect on your five screens: mean brightness unmoved, high-frequency detail
|
||||||
|
**×1.15…×1.30** — the same art at twice the resolution, which is exactly what
|
||||||
|
[8AX](../re/structures/ui-8ax-fullres-background.md) said the game draws.
|
||||||
|
⚠️ Both backgrounds are now drawn (`ptbase` upscaled, then the full-res one over
|
||||||
|
it): correct, but wasted fill. Draw only the full-res one, taking its *timing*
|
||||||
|
from `ptbase`'s element, which carries the keyframes.
|
||||||
|
[`structures/ratc-child-names.md`](../re/structures/ratc-child-names.md)
|
||||||
|
✅ **And the fix has no remaining hole.** 60 of the disc's 18 002 RATC children
|
||||||
|
still have no `opt ` block; all 60 are now accounted for and **none is on your
|
||||||
|
screens**. They are the ten frames of the disc's only `.tan` **frame sequence**
|
||||||
|
(`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle), where
|
||||||
|
a single `opt ` block names the whole run — so a name-resolution miss is not
|
||||||
|
hiding anything else the way `8AX` was. ⚠️ Two notes if you ever read outside
|
||||||
|
`GP_TITLE`: `ratc::parse` **over-reports** there, listing a `.tan`'s frames as
|
||||||
|
anonymous children; and a RATC bundle names exactly six kinds of resource —
|
||||||
|
`.t32` (14 756), `.rat` (3 311), `.prm` (367), `.tbm` (224), `.sbo` (54),
|
||||||
|
`.tan` (6).
|
||||||
|
[`structures/ratc-tan-frame-sequence.md`](../re/structures/ratc-tan-frame-sequence.md)
|
||||||
|
<details><summary>the original entry, kept because its reasoning still stands</summary>
|
||||||
|
|
||||||
|
🟡 **`screen render` silently drops one full-screen element per screen — and
|
||||||
|
you must NOT simply draw it.** Auditing what the composer omits on your five
|
||||||
|
screens: everything is accounted for (`kind & 0x4` ghost instances, `.prm`
|
||||||
|
primitives, `loop*` animations) except **`pteff04.t32`** on the title and
|
||||||
|
**`pteff05.t32`** on both menus. Those are `kind 0x0`, one keyframe, rest
|
||||||
|
`a = 255`, pivot `(640,360)` — full-screen and opaque.
|
||||||
|
**Cause:** the element declares `pteff05.t32`, but the `T8aD` behind its `opt `
|
||||||
|
link is registered under the name **`8AX`**, so the sprite lookup misses and a
|
||||||
|
silent `continue` drops it.
|
||||||
|
✅ **It does not currently show,** because `ptbase.t32` (640×360, drawn at
|
||||||
|
200 %) is *the same artwork at half resolution* — its 2× upscale differs from
|
||||||
|
`8AX` by mean 2.05, and our background is pixel-identical to `8AX` in every
|
||||||
|
patch sampled.
|
||||||
|
✅ **SETTLED 2026-08-29 — the game draws the full-res `8AX`, so use it.**
|
||||||
|
Previously parked as "needs a per-draw capture"; it did not. The two carry the
|
||||||
|
same art at two resolutions, so what separates them is the detail `8AX` has
|
||||||
|
that an upscale cannot. Correlating the capture's departure-from-upscale
|
||||||
|
against the 8AX-only detail (both first mapped through the measured gamma):
|
||||||
|
main menu **+0.0475** vs controls +0.0032 / −0.0075, title **+0.0634** vs
|
||||||
|
+0.0095 / +0.0086 — **two screens, both 68 % of the theoretical ceiling, 7–15×
|
||||||
|
their matched controls**.
|
||||||
|
**So: resolve the name and draw `8AX` at 1:1.** Upscaling the 640×360 `ptbase`
|
||||||
|
2× is *wrong*, not merely softer. ⚠️ Do not draw **both** — an opaque
|
||||||
|
full-screen layer over an identical one costs fill and hides later changes; and
|
||||||
|
note `ptbase`'s element is the one carrying the keyframes, so you need its
|
||||||
|
timing with `8AX`'s pixels.
|
||||||
|
⚠️ It does not show whether `ptbase` is *also* drawn underneath — the full-res
|
||||||
|
background is
|
||||||
|
~86 % opaque and would hide it either way.
|
||||||
|
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
* ✅ **Paint order: your exposure is two element pairs, on one screen.** We use
|
||||||
|
an order *measured from the running game* where one exists and a derived order
|
||||||
|
(a sort on each sprite's layer key) elsewhere. Checked, rather than assumed:
|
||||||
|
the derived order reproduces the measured one **exactly** on the main menu
|
||||||
|
(0 inverted pairs) and the developer splash (0). On the **title** it differs by
|
||||||
|
8 pairs — **all same-layer-key ties** — and two of those are total occlusions
|
||||||
|
(`back2eff5` is 1133×280 and *fully contains* `back2eff3` and `back2eff4`;
|
||||||
|
derived puts it on top, the game puts it underneath). The title is unaffected
|
||||||
|
in practice because it has a measured order.
|
||||||
|
Per screen: title **measured**, main menu **measured**, developer splash
|
||||||
|
**measured**, publisher splash derived but with **0 ties** (fully determined),
|
||||||
|
and **`EXTRAS` derived with 15 tied pairs of which only 2 overlap**.
|
||||||
|
✅ **That narrows again to ONE, and the capture is consistent with it.** Of the
|
||||||
|
two overlapping pairs, `ptloop01`×`ptloop02` are `loop*` animations that
|
||||||
|
`compose` skips by default, so their tie is unreachable. The remaining pair is
|
||||||
|
`ptframe3`×`ptframe4`, overlapping 102×132 px — and against `live-extras.png`
|
||||||
|
that contested region correlates **+0.9622**, *better* than the whole frame
|
||||||
|
(+0.9440) and inside the range of regions where order cannot matter (+0.8502 /
|
||||||
|
+0.9903). 🟡 Consistent with, not proof — correlation cannot see a swap between
|
||||||
|
locally similar art. **15 → 2 → 1 → consistent** is the whole paint-order risk
|
||||||
|
on your five screens.
|
||||||
|
[`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md)
|
||||||
|
|
||||||
|
* ✅ **How good are the five screens, actually?** One page with the numbers:
|
||||||
|
[`five-screens-acceptance.md`](../re/five-screens-acceptance.md). Rendered and
|
||||||
|
correlated against the live captures — title **0.9500**, main menu **0.9460**,
|
||||||
|
`EXTRAS` **0.9440**, publisher splash **0.9600**, developer splash **0.9643**,
|
||||||
|
and every one aligns at **exactly dy=0 dx=0** over a ±2 px search, so placement
|
||||||
|
and scale are right and the residual is tone and detail rather than geometry.
|
||||||
|
Every undrawn element is accounted for (ghost instances, `.prm` primitives,
|
||||||
|
`loop*` animations, and the `8AX` name mismatch whose art is on screen anyway) —
|
||||||
|
the counts add up exactly, with nothing unexplained. The residual, ranked: tone
|
||||||
|
(γ≈1.4, the game's own ramp), `8AX` resolution, one `EXTRAS` paint-order tie,
|
||||||
|
and rotation-not-rendered which does not affect these five at rest.
|
||||||
|
⚠️ Reach: static composites at rest against single frames — this says nothing
|
||||||
|
about animation.
|
||||||
|
|
||||||
|
* ✅ **A static composite is only meaningful for a screen that SETTLES — the two
|
||||||
|
splashes are animations.** Measured with its control: suppressing the `_eff`
|
||||||
|
glows takes the **publisher splash 0.9604 → 0.9982** and the **developer splash
|
||||||
|
0.9659 → 0.9980**, while the same edit makes the title −0.002, the main menu
|
||||||
|
**−0.092** and `EXTRAS` **−0.107** worse. The draw log says why — on the
|
||||||
|
developer splash the glows draw on frames 94–115 and the logos on 116–211, so
|
||||||
|
at the captured moment every glow is already finished, including the two with
|
||||||
|
plateaus that `rest_plateau` renders visible.
|
||||||
|
So `rest_plateau` is right where a screen settles and over-draws where it does
|
||||||
|
not. **There is no "resting pose" for the splashes** — they play through and
|
||||||
|
leave, and a static composite of them is a picture of one arbitrary frame
|
||||||
|
(≈0.998 for the frame these captures hold). **Play the timeline for the two
|
||||||
|
splashes; composite statically for title / main menu / `EXTRAS`.**
|
||||||
|
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||||||
|
|
||||||
|
* ❔ **A group's DURATION is in the data; its START TIME is not.** Testing whether
|
||||||
|
the splash timeline *played* reproduces the capture: each element is on screen
|
||||||
|
for its declared span to within 2 % (glows 44 units observed vs 45 declared;
|
||||||
|
logos 192 vs 195). But every glow declares the same times `15,30,45` and every
|
||||||
|
logo the same `15,30,190,194,206,210`, so on one clock they would overlap almost
|
||||||
|
entirely — and they **do not overlap at all**. The glows run frames 94–115, the
|
||||||
|
logos 116–211, strictly sequential.
|
||||||
|
🔴 The obvious candidate is dead: each group header carries an undecoded
|
||||||
|
**lead-in word**, and it is `0x00000000` for all seven elements. Not the
|
||||||
|
keyframe times, not that word, not declaration order, not the RATC child order.
|
||||||
|
✅ **Here is the sequence to author, measured** (1 frame = 1/30 s):
|
||||||
|
publisher `palogo_sqex` **3.00 s+** (a floor — it is already at full alpha on
|
||||||
|
the capture's first frame), a **0.10 s** gap with nothing drawn, then the
|
||||||
|
developer **glows for 0.73 s**, then the developer **logos for 3.20 s**, the
|
||||||
|
glow→logo switch being a single frame boundary with no overlap. Each phase is
|
||||||
|
within 2 % of its element's declared span, so the *durations* come from the
|
||||||
|
bundle and only the *ordering* is authored.
|
||||||
|
⚠️ One capture, one run; and `palogo_anima`/`_eff` get 0 draws in all 214
|
||||||
|
frames, so a third pair's phase is not in this measurement.
|
||||||
|
[`data/splash-phase-timeline.txt`](../re/data/splash-phase-timeline.txt)
|
||||||
|
⚠️ **So you must author the sequencing.** The observed order on the developer
|
||||||
|
splash — both glows, then both logos — is *measured for one screen*, not a
|
||||||
|
decoded rule.
|
||||||
|
✅ **Verified against its own refutation:** across all 235 captured frames,
|
||||||
|
**zero** contain both a glow and a logo; the switch is one clean boundary with
|
||||||
|
two sprites either side.
|
||||||
|
🔴 **And it goes further than start times — a bundle's declared elements are
|
||||||
|
not what gets drawn.** ✅ **And the mechanism is now established, by
|
||||||
|
elimination.** The competing story — "two compositions shown in sequence" —
|
||||||
|
needs a bundle declaring the glows *without* the logos, and no such bundle
|
||||||
|
exists: only four entries in `GP_TITLE` carry `palogo` elements, and each
|
||||||
|
developer entry (11, 14) declares **all six** logos and glows. So whichever
|
||||||
|
bundle was active, only a subset of its elements was drawn at a time.
|
||||||
|
(An earlier texture-base test could not separate the two — it failed its own
|
||||||
|
control, since the publisher splash is a different bundle and shares the base
|
||||||
|
`0x11C30000`, a reused upload slot rather than an identity.) Entry 11 declares *three* logo/glow pairs; only two are ever drawn.
|
||||||
|
`palogo_anima` gets **0 frames** while `palogo_gamearts` gets **95**, from
|
||||||
|
byte-identical keyframe times. (Reach: the capture covers frames 1–214, so
|
||||||
|
"never in the window".) **Compositing every element of a bundle does not
|
||||||
|
reproduce what the game shows over time** — it is right for a static screen
|
||||||
|
that settles, and it is not a timeline.
|
||||||
|
[`structures/ui-group-start-time.md`](../re/structures/ui-group-start-time.md)
|
||||||
|
|
||||||
|
* ✅ **The splash's backdrop is an opaque black `.prm` from the bundle, painted
|
||||||
|
first.** Measured: every frame opens with the clear and then an **untextured
|
||||||
|
1280×720 quad, vertex colour `FF000000` in all 212 frames** — matching
|
||||||
|
`palogo_eff0.prm`'s declaration (`kind 0x10`, pivot 640×360, one keyframe,
|
||||||
|
`a=255`). That is why a splash render needs `--black` rather than the default
|
||||||
|
backdrop. ⚠️ Not a general rule: the main menu's measured order puts one `.prm`
|
||||||
|
at position 4 and the other **last** (the transition fade).
|
||||||
|
✅ **The splash's recorded paint order `[0,2,4,6,1,3,5]` is a real depth
|
||||||
|
order** — read off the runtime child array, and independently backed by the
|
||||||
|
static layer key (glows `0xa100` sort before logos `0xa110`;
|
||||||
|
`paint_order_audit` reports derived == measured, 0 ties). A same-day claim on
|
||||||
|
this page that it was "a temporal sequence, not depth" was **withdrawn**; see
|
||||||
|
the ❌ section in the linked page for why, if you read it before the fix.
|
||||||
|
⚠️ Separately and still true — and it is an **activation** fact, not a
|
||||||
|
paint-order one: the splash's glows and logos are never on screen together
|
||||||
|
(glows f94–115, logos f116–211,
|
||||||
|
[timeline](../re/data/splash-phase-timeline.txt)). Compositing all seven at
|
||||||
|
once reproduces no frame of the real screen. Depth says *what covers what*;
|
||||||
|
it does not say *what is up*.
|
||||||
|
[`structures/ui-prm-primitives.md`](../re/structures/ui-prm-primitives.md)
|
||||||
|
|
||||||
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
|
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
|
||||||
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
|
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
|
||||||
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
|
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
|
||||||
@@ -54,37 +516,402 @@ authored version can be deleted.
|
|||||||
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
|
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
|
||||||
* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a
|
* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a
|
||||||
naming convention that holds for all 54 real pairs, not a decoded field.
|
naming convention that holds for all 54 real pairs, not a decoded field.
|
||||||
|
* **`rest()` was wrong for elements with no exit animation — fixed 2026-08-28.**
|
||||||
|
A trailing run of identical keyframes was always treated as the exit and
|
||||||
|
excluded; on an element that has no exit it *is* the hold, and `rest()` fell
|
||||||
|
back to the element's **first** keyframe — off-position and transparent. Six
|
||||||
|
elements on the main menu were affected, including `ptframe1`/`ptframe2`, the
|
||||||
|
bright circuit bracket around the menu, which both the port's composite **and**
|
||||||
|
`sylpheed-cli screen render` were dropping.
|
||||||
|
The rule now: **a trailing run is the hold exactly when it is visible** (alpha
|
||||||
|
≠ 0). ⚠️ Not the pose-equality test the report proposed — `pgptitle.rat`'s
|
||||||
|
trailing run also matches its last timed keyframe, and adopting that would erase
|
||||||
|
the word PAUSE. Oracle correlation over the bracket region improved
|
||||||
|
**0.9596 → 0.9748**; the PAUSE control is unchanged.
|
||||||
|
✅ **Disc-wide**: over 2 859 bundles / 13 991 elements, `rest` moves for **30**
|
||||||
|
(0.21 %) — **4 invisible → visible, 0 visible → invisible**. Tests green
|
||||||
|
(131 passed).
|
||||||
|
❔ **Back to the port agent:** on the English main menu exactly **two** elements
|
||||||
|
satisfy your pose-equality condition (`ptframe1`/`ptframe2`), so your six span
|
||||||
|
the whole export. If any of the other four have a **transparent** trailing run,
|
||||||
|
this rule leaves them alone on purpose. **Which screens are they on, and does a
|
||||||
|
capture show any of them drawn?** If so the alpha rule is incomplete.
|
||||||
|
This also closes the old ❔ on `ptframe1`/`ptframe2` "resting at alpha 0 but the
|
||||||
|
capture shows the frame plainly".
|
||||||
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
|
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
|
||||||
a keyframe is the **start of a ramp**.
|
a keyframe is the **start of a ramp**.
|
||||||
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
|
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
|
||||||
|
* **That ramp is linear, and it runs at 2 keyframe time units per rendered
|
||||||
|
frame.** Measured frame-by-frame off the running game's own draw stream: a
|
||||||
|
declared 15-unit fade lands on `round(255·k/15)` for all seven of its samples,
|
||||||
|
with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames.
|
||||||
|
**measured**, not decoded — the disc says `t=30`, it does not say what a `t` is.
|
||||||
|
🟡 **But a multi-keyframe element's TIMELINE does not reproduce (2026-08-28).**
|
||||||
|
The linear law and the 2-units-per-frame rate rest on the splash's `_eff`
|
||||||
|
glows, and those are exact. Applying *their* calibration — with no free
|
||||||
|
parameter left — to `palogo_gamearts` in the same bundle and the same frames:
|
||||||
|
the logo is still at `a=255` nine frames after its declared `a=32`, its
|
||||||
|
fade-out runs ~17 frames late, and its declared 80-frame fade-in is never
|
||||||
|
drawn at all (and that is not culling — the same element is submitted down to
|
||||||
|
`a=7` on the way out). Its declared fade-out also spends 12 of 16 units
|
||||||
|
dropping only 23/255 of the alpha; the capture shows no such plateau.
|
||||||
|
🔵 **Followed up, and the candidate is now strongly favoured — but not
|
||||||
|
adopted.** That `+36` holds the *next* pose's time is supported by a
|
||||||
|
calibration-free measurement: the observed full-alpha **hold : fade-out** ratio
|
||||||
|
is 83 : 13 frames = **6.38**, the shifted reading predicts **8.00**, and the
|
||||||
|
current reading predicts **0.25** — off by **26×**. With the glow's 2
|
||||||
|
units/frame fixed and nothing else free, the current reading says the logo
|
||||||
|
holds full alpha for **2.0 frames**; the game holds it for **83**. The shift
|
||||||
|
also makes `rest()`'s plain dwell rule pick the visible hold instead of a
|
||||||
|
fully transparent pose, and removes the decoder's "last block's time is
|
||||||
|
unreadable" special case.
|
||||||
|
~~🔴 **What stops it:** … `GP_TITLE` build 7 … twins should match in
|
||||||
|
brightness …~~ **Withdrawn 2026-08-28.** That render difference is **not**
|
||||||
|
evidence about the time association. It is one element — `ptlogo_eff3.t32`, a
|
||||||
|
transient bloom that grows `0 % → 200 %` at full alpha while rotating, then
|
||||||
|
collapses — and it has **no resting pose at all**. `rest()` falls through to
|
||||||
|
its dwell fallback and returns whichever end of that movement the indexing
|
||||||
|
lands on: the invisible frame as decoded, the 200 % peak shifted. An 896×389
|
||||||
|
sprite at 200 % is larger than the screen, which is the whole 13.1 % and the
|
||||||
|
whole luminance gap. I was comparing a heuristic, not a decode.
|
||||||
|
🔴 **And that is a real defect you should know about:** `rest()`'s dwell rule
|
||||||
|
is unsound *whenever it runs*. A dwell gap is time spent interpolating **from**
|
||||||
|
pose k **to** pose k+1; neither is held unless they are equal — which is a
|
||||||
|
plateau, and the plateau path has already returned by then. **So any element
|
||||||
|
with no two adjacent identical poses has a guessed rest pose**, in our renderer
|
||||||
|
and in anything built from it. Measured disc-wide: **2 305 of 15 493 elements
|
||||||
|
(14.88 %)** — ⚠️ **corrected 2026-08-29 down from a published 3 807 (24.57 %)**,
|
||||||
|
which counted 1 502 *single-keyframe* elements as guesses; those have one pose
|
||||||
|
and it is unambiguously their rest. Of the real 2 305, **195 get a degenerate
|
||||||
|
`scale = 0 %` pose**, and the two
|
||||||
|
candidate rules agree only **50.2 %** of the time.
|
||||||
|
✅ **This does not block you — your exposure is TWO elements, both on the
|
||||||
|
splashes.** The fallback is reached only by an element that is plateau-less
|
||||||
|
*and* multi-keyframe; a single-keyframe element short-circuits. Title, main
|
||||||
|
menu and `EXTRAS` reach it **zero** times, which is why three different
|
||||||
|
fallback rules render them to identical correlations. The publisher and
|
||||||
|
developer splashes reach it once each — and there, `SYLPHEED_REST_RULE=last`
|
||||||
|
(the final keyframe) scores **+0.9982** and **+0.9758** against the current
|
||||||
|
rule's +0.9600 and +0.9643. ⚠️ Measured against single frames of a transient
|
||||||
|
animation, so it fixes which pose matches *those* captures, not which is
|
||||||
|
canonically at rest. Default unchanged — it is better on both screens where it
|
||||||
|
fires and identical on the other three, but it would move 2 305 elements
|
||||||
|
disc-wide on two measurements.
|
||||||
|
✅ **And the picture is now coherent.** Applying the last keyframe to *every*
|
||||||
|
element (not just the plateau-less ones) collapses all five screens — title
|
||||||
|
0.9500→0.6819, main menu 0.9460→0.6416, `EXTRAS` 0.9440→0.5745, and both
|
||||||
|
splashes render **blank**. The reason is the model: a group is **entry → hold →
|
||||||
|
exit**, and the exit is the screen's *dismissal*. A displayed screen is sitting
|
||||||
|
at the **hold**, not at its final pose — so `rest_plateau` is right, and the
|
||||||
|
last keyframe is the *post-exit* state. It is right for a **transient** element
|
||||||
|
precisely because a transient's settled state is "gone". The draw capture agrees
|
||||||
|
independently: on the developer splash the `_eff` glows draw on frames 94–115
|
||||||
|
and the logos on 116–211, so the glows are already over when the logos are up.
|
||||||
|
**Three independent observables — animation timing, static composites and the
|
||||||
|
per-frame draw log — all support: plateau where there is one, last keyframe
|
||||||
|
where there is not.**
|
||||||
|
📊 **Structurally, `last` is defensible for 2 293 of the 2 305 (99.5 %)**: 1 618
|
||||||
|
end invisible (a transient, gone at rest), 675 end at maximum alpha (faded in
|
||||||
|
and stopped — the final pose *is* settled), and only **12** end visible below
|
||||||
|
full alpha, which are genuinely unclear. The dwell rule returns a mid-movement
|
||||||
|
frame by construction. ⚠️ The 1 618 rest on an assumption worth seeing: that
|
||||||
|
such an element's animation has finished by the time the screen settles — shown
|
||||||
|
by the draw log for the two splash glows, unshown for the rest.
|
||||||
|
📊 The disc-wide blast radius, for whoever decides: the rules **differ on 82.3 %**
|
||||||
|
of those 2 305, so "either is fine" is not available — and the current rule
|
||||||
|
returns a **zero-scale** (collapsed, pre-roll) pose for **195** of them against
|
||||||
|
**43** under `last`, a 4.5× reduction in provably-degenerate results. That
|
||||||
|
argues the same way as the captures, from the data's own structure. The single disagreement
|
||||||
|
is `palogo_anima_eff.t32` on the developer splash, and the current answer is
|
||||||
|
the defensible one there — see below. Still worth flagging plateau-less
|
||||||
|
elements in an export rather than silently inheriting our guess; it is one pass
|
||||||
|
over the keyframes.
|
||||||
|
✅ **One concrete part of it is fixed (2026-08-28): `scale = 0` no longer
|
||||||
|
renders at full size.** `blit`/`fill_quad` coerced `scale == 0` to 100 %, so an
|
||||||
|
element collapsed to nothing was drawn full-size. The disc settles the reading:
|
||||||
|
2 166 elements have a zero-scale keyframe, **not one is zero throughout**, and
|
||||||
|
1 762 grow back out of zero — so 0 means collapsed, not "unset". **Renders are
|
||||||
|
24/24 byte-identical** on `GP_TITLE` (all 16 builds), `GP_PAUSE_MENU` and
|
||||||
|
`GP_OPTIONS`, so nothing you rely on moves; the 126 elements the old
|
||||||
|
code actually painted are **all in `GP_READY_ROOM.pak`**, already a no-go.
|
||||||
|
[`structures/ui-rat-layout.md`](../re/structures/ui-rat-layout.md)
|
||||||
|
🔴 **"rest = last keyframe" is refuted** as the fix. The splash has three
|
||||||
|
sibling glows with identical structure and times, differing in one byte
|
||||||
|
(`a=212` vs `a=255` at `t=45`); that rule would make `anima_eff` alone
|
||||||
|
invisible while its two siblings stay lit. The capture agrees weakly — box-mean
|
||||||
|
ratios capture/render are gamearts 0.717, seta 0.723, **anima 0.772**, and a
|
||||||
|
glow we drew that the game does not would put anima *below* its siblings.
|
||||||
|
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||||||
|
🟡 **The shift is still not adopted**, now for a different reason: it flips
|
||||||
|
this element to the visibly wrong answer, so it and a decision about
|
||||||
|
plateau-less elements have to land together, and neither half has a capture to
|
||||||
|
verify against.
|
||||||
|
**Default unchanged**, experiment reachable via `SYLPHEED_KF_TIME_SHIFT=1`.
|
||||||
|
**What this means for you:** the interpolation *law* is settled (linear, 2
|
||||||
|
units/frame); a multi-keyframe group's *timing* is not — do not expect a
|
||||||
|
2-frame hold where the game holds 83.
|
||||||
|
✅ **The seconds conversion is settled: `1 unit = 1/60 s`, a 30 Hz title.** The
|
||||||
|
re-test this page used to name has been run — 300 submitted frames timed on the
|
||||||
|
**idle** title (nothing loading) came out at **28.8 and 28.3 fps**, the same
|
||||||
|
rate as the 27.6 fps measured during the loading splash. The competing 60 Hz
|
||||||
|
reading is excluded: it needs the emulator at 47 % of real time while idling on
|
||||||
|
a screen that costs ~5 draws per frame.
|
||||||
|
A second, independent line agrees — the transition quad is declared black for
|
||||||
|
12 units (0.20 s under this conversion) and a capture measured the pure-black
|
||||||
|
plateau at 0.17–0.23 s
|
||||||
|
([`screen-transitions.md`](../re/screen-transitions.md)).
|
||||||
|
⚠️ Still **measured, not decoded**: no field on the disc says "sixtieths of a
|
||||||
|
second".
|
||||||
|
* **The paint order is derivable from the file.** Each `T8aD` sprite header
|
||||||
|
carries a **`u16` layer key at `+0x0A`** (the upper half of the 32-bit word at
|
||||||
|
`+0x08` is zero in all 21 184 sprites on the disc). Paint order is that key,
|
||||||
|
**stable-sorted** so equal keys keep declaration order; elements with no sprite
|
||||||
|
(`.prm` primitives, `.tbm`) have no key and take an implied one — the backdrop
|
||||||
|
and dim quads sort early, the screen-transition fade (`pteff00.prm`,
|
||||||
|
`pfeff00.prm`) sorts **last**. ✅ decoded. Checked against five paint orders read
|
||||||
|
off the running game, one of them (`GP_SAVE_LOAD`'s slot-list header, 6
|
||||||
|
instances) **exact and independent of the screens the rule was fitted to**, and
|
||||||
|
against a fresh `EXTRAS` capture. It reorders 341 of the disc's 965 builds, so
|
||||||
|
it is not a no-op dressed as a rule.
|
||||||
|
🟡 **The one residual: ties.** Where two elements share a key the game
|
||||||
|
sometimes paints them in an order nothing predicts — eight candidates refuted,
|
||||||
|
including declaration order, RATC child order, keyframe times, resting X/Y and
|
||||||
|
`kind`. Measured cost: on the three screens with ground truth it changes the
|
||||||
|
blend of **one element on one screen** (a title glow). Take the stable sort and
|
||||||
|
accept that.
|
||||||
|
|
||||||
|
* **Ⓑ returns to the title, and that title still works.** Ⓐ on the Ⓑ-returned
|
||||||
|
title opens the main menu — measured, with Ⓐ on the boot title as the control in
|
||||||
|
the same run. (Only the *attract*-returned title is inert, which is an emulator-
|
||||||
|
harness curiosity, not a port concern.)
|
||||||
|
* **Menu movement, measured off the running game.** ⬆⬇ move one item per press
|
||||||
|
and **wrap at both ends** (5-item main menu and 3-item `EXTRAS` both). ⬅➡ do
|
||||||
|
nothing. Ⓑ goes up one level **and restores focus to the item you came from**;
|
||||||
|
Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. **Initial
|
||||||
|
focus is not stable**: four boots of the same script gave `TUTORIAL`,
|
||||||
|
`TUTORIAL`, `NEW GAME`, `NEW GAME`. Do not hardcode it; pick one and say you
|
||||||
|
picked it. All **measured**, none of it on the disc.
|
||||||
|
* **Each button's destination is measured; its GamePart id is not.**
|
||||||
|
**`NEW GAME` → `DIFFICULTY`** (`EASY`/`NORMAL`/`HARD`/`BACK`, opening on
|
||||||
|
`NORMAL`) **→ `SELECT DATA`** — it does *not* hang; the run then hits the
|
||||||
|
already-documented `sub_823070B0` cache crash, which is not a menu problem.
|
||||||
|
`LOAD GAME` → the save-slot list, `TUTORIAL` → the lesson list, `OPTIONS` →
|
||||||
|
the settings menu, `EXTRAS` → `GP_TITLE` build 6, `EXTRAS ▸ MISSION SELECT` →
|
||||||
|
the stage list. The
|
||||||
|
GamePart ids (`3`, `25`, `8`, `5`, `7`) are the entries of the decoded id table
|
||||||
|
whose **names match the screens seen**; that binding is authored, not measured.
|
||||||
|
|
||||||
|
* **A screen change is a fade through black.** Each screen carries a full-screen
|
||||||
|
black `.prm` quad that paints last (`pteff00.prm` / `pfeff00.prm`) whose
|
||||||
|
keyframe group *is* the transition: black at `T0`, clear by `T1`, clear until
|
||||||
|
`T2`, then back to black on exit. ✅ decoded, with a disc-wide check — and in
|
||||||
|
`GP_TITLE` exactly the six **screen** builds carry it while the six overlays do
|
||||||
|
not. The fade-in length is `T1 − T0` and is read from the file (0.87 s for
|
||||||
|
`EXTRAS`, 0.97 s main menu, 4.08 s title). ❔ **The fade-OUT length is not on
|
||||||
|
the disc** — the last keyframe has no time slot; **measured ~0.4 s**, twice.
|
||||||
|
The black hold measures 0.17–0.23 s. ⚠️ Do not time the fade-in off a capture's
|
||||||
|
brightness: the incoming screen's own element animations dominate it and run
|
||||||
|
much longer than the quad.
|
||||||
|
|
||||||
|
* **The movie manifest names every boot-side video by role.** `dat/tables.pak`
|
||||||
|
entry `0x5b983a08`: `LOGO1`–`LOGO4` → `logo1.wmv`–`logo4.wmv` (**not on the
|
||||||
|
disc** — this is why the splash is a screen), **`ADVERTISE_MOVIE` → `ADV.wmv`**,
|
||||||
|
`STAFF_ROLL` → `SYLPH_HD720p_8M-CBR_2ch.wmv`, **`MS00A` → `S00A.wmv`** (the
|
||||||
|
new-game intro, 93.9 s, with subtitle + `VOICE_S00A` + a text overlay),
|
||||||
|
`MS01A` → `S01A.wmv`. ✅ decoded — and **`S00A.wmv` is now also measured**: matched off the running game at 0.96–1.000 with a strictly monotone playhead over 25 consecutive 0.5 s samples. It starts ~4.5 s after Ⓐ on the save slot.
|
||||||
|
**The boot intro and the attract movie are the SAME asset** — there is no
|
||||||
|
separate boot slot, and 15 of 19 captured attract frames match `ADV.wmv` with a
|
||||||
|
monotonically advancing playhead ending at its full 137 s. One video, not two.
|
||||||
|
✅ The attract movie **plays to its end**; nothing cuts it short.
|
||||||
|
✅ **A movie is skippable with a single Ⓐ.** Measured: one tap ~45 s into the
|
||||||
|
boot brought the title at ~57 s against a ~193 s no-input baseline over three
|
||||||
|
boots, with Canary's own keystroke counter proving exactly one press was
|
||||||
|
delivered — and the skipped-to title is **fully functional** (`PRESS Ⓐ` plate
|
||||||
|
present, Ⓐ opens the main menu). What breaks the boot is *hammering*: 88
|
||||||
|
presses left a permanent black screen. One press is fine.
|
||||||
|
|
||||||
|
* **The menu's sound events are named on the disc.** `tables.pak`'s `SOUNDS`
|
||||||
|
record carries 322 `SE_*` cues, and the low block is the UI vocabulary — named
|
||||||
|
after the *event*: `SE_UI_CURSOR` (2), `SE_UI_DECIDE` (3), `SE_UI_CANSEL` (4),
|
||||||
|
`SE_UI_IMPOSI` (5, the error), `SE_UI_SUB_WIN_OPN`/`_CLS` (8/9),
|
||||||
|
`SE_UI_SPLASH_IN`/`_OUT` (12/13). ✅ decoded, full list in
|
||||||
|
[`data/se-ui-cues.txt`](../re/data/se-ui-cues.txt). They all live in **one**
|
||||||
|
bank — `BANK_SE` is a single field naming `Static.slb`, and 0 of the 322 has
|
||||||
|
its own `FILES` entry.
|
||||||
|
🟡 **Which event fires which cue is a name match**, not a measurement — strong,
|
||||||
|
because these are the authors' own event names, but nobody has watched the game
|
||||||
|
emit cue 2 on a d-pad press. The port is authoring it.
|
||||||
|
✅ **The SE audio IS extractable — by playing it.** (This corrects an earlier
|
||||||
|
"cannot be extracted" on this page.) The disc carries no index: `Static.slb` has
|
||||||
|
**0 `RIFF`/`seek`/`WAVE`** and there is **no XACT container anywhere** (0 ×
|
||||||
|
`XGSF`/`SDBK`/`WBND` in 1.08 GB of `sound.pak`; no `XACT` string in the
|
||||||
|
executable — those extensions are the authoring tool's). But Canary's
|
||||||
|
`--xma_param_probe=true` logs every stream's head bytes, and searching them in
|
||||||
|
`Static.slb` locates the wave exactly:
|
||||||
|
**d-pad move → `0x1ec0` (4 pkts / 8 192 B, 0.533 s); Ⓐ confirm → `0x5d6c0`
|
||||||
|
(6 pkts / 12 288 B, 1.016 s); Ⓑ back → `0x0ec0` (2 pkts / 4 096 B, 0.344 s)** —
|
||||||
|
every cue the five screens need. Move and back reproduce with identical head
|
||||||
|
bytes across **two independent boots**. Each matched at one offset only, and
|
||||||
|
the first two are contiguous
|
||||||
|
(`0x0ec0 + 4096 = 0x1ec0`) — the bank is a packed run of whole 2 048-byte
|
||||||
|
packets with no delimiters, which is why nothing could be scanned for.
|
||||||
|
❔ **⬅ and ➡ play nothing distinct** — no new stream on either, so leave them
|
||||||
|
silent (the probe dedups, so this excludes a *distinct* invalid cue, not a
|
||||||
|
quiet replay of an already-heard one).
|
||||||
|
🟡 The Ⓐ press both confirms and opens a screen, so whether its wave is the cue
|
||||||
|
named `SE_UI_DECIDE` or `SE_UI_SUB_WIN_OPN` is not separated.
|
||||||
|
⚠️ The order is **not** cue-id order, so the index must be observed per cue, not
|
||||||
|
counted. ✅ **The slices decode**: 0.533 s, 0.344 s and 1.016 s of mono 48 kHz
|
||||||
|
audio with the attack-and-decay shape of UI blips, via
|
||||||
|
`tools/re-capture/slb_extract_wave.py` — whose wrapper reproduces a known-good
|
||||||
|
`BGM_001` decode to the same 173.808875 s, so it is verified, not assumed.
|
||||||
|
|
||||||
|
* **The boot sequence is not data-driven — the port authors it.** ❔ Four places
|
||||||
|
were checked and the order is in none: `config.ini`'s `[SYSTEM]` is empty, the
|
||||||
|
movie manifest carries assets not transitions, the requested GamePart id lives
|
||||||
|
**only as a stack argument in flight** (no persistent field, no literal store),
|
||||||
|
and the string `GP_ADVERTISE_DEMO` has **zero xrefs**. A transition is a call
|
||||||
|
with an id argument, chosen by code.
|
||||||
|
🟡 **But the states have names, and the transition uses them.** `sub_821C6458`,
|
||||||
|
the title part's state function, calls **`sub_821CC860(…, "<NAME>", 0)`** with
|
||||||
|
`TITLE_SCREEN`, `TITLE_MENU` and `LOADING` — the three states measured off the
|
||||||
|
game, in the game's own words — and installs the result. So a transition is a
|
||||||
|
call with a **name** argument, which is also why `GP_ADVERTISE_DEMO` has no
|
||||||
|
xrefs: at this level the screen graph is name-keyed, not id-keyed.
|
||||||
|
✅ The argument is now **decoded** at 46 of that function's 48 call sites (28
|
||||||
|
distinct names). ⚠️ It is a **generic name-keyed lookup**, not a screen factory —
|
||||||
|
its arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
|
||||||
|
`GAMMA_RGB`. An earlier version of this page claimed `DIFFICULTY` and
|
||||||
|
`EXTRA_MENU` as corroborated screen names; **neither is ever the argument**, and
|
||||||
|
only `TUTORIAL_MENU` survives.
|
||||||
|
✅ **And the state machine itself is decoded**: `state = this+136`, ten states
|
||||||
|
dispatched through a jump table at `0x821C6498`, with **18 transitions** each a
|
||||||
|
literal `li`/`stw`. States **0, 2, 8** install `TITLE_SCREEN`, `TITLE_MENU`,
|
||||||
|
`LOADING`. `4 → 0` is the only edge back to the title, reached from `2 → 4` —
|
||||||
|
which matches Ⓑ-returns-to-title as measured. Full graph in
|
||||||
|
[`data/title-state-machine.txt`](../re/data/title-state-machine.txt).
|
||||||
|
⚠️ **All of that is ONE PHASE.** `GamePart_Title` dispatches on an outer phase
|
||||||
|
field at `this+132` (five values) before reaching any of it: phase 0 is the
|
||||||
|
**developer splash** (`sub_821C5690`, the same function the corpus fingered
|
||||||
|
independently), phase 4 is the title/menu machine. The ten states and
|
||||||
|
eighteen edges above live inside phase 4 alone.
|
||||||
|
✅ **State 4's edge conditions are an event code** — `sub_821C6458`'s third
|
||||||
|
argument. State 4 is the input-waiting state (reached straight after the menu
|
||||||
|
is installed) and handles 6 of 26 events: **`0` → title**, **`3`, `5`, `8`,
|
||||||
|
`25` → `LOADING`**, `10` → state 5.
|
||||||
|
🟡 What the event *numbers* mean is not decoded — button id, menu row, or
|
||||||
|
message id — so the port still takes the button→destination map from
|
||||||
|
measurement. The one-to-title / four-to-loading shape matches the five-item
|
||||||
|
menu with Ⓑ, but that is a **count-match, not a mapping**. The sequence itself is fully measured:
|
||||||
|
splash → `ADV.wmv` → title + `PRESS Ⓐ` → (idle ~8–10 s → `ADV.wmv` in full →
|
||||||
|
title) → Ⓐ → main menu, with Ⓑ from the main menu returning to the title.
|
||||||
|
|
||||||
|
* **`config.ini` picks the language, and that is what picks the EN/JP build.**
|
||||||
|
The disc's **only** config file (400 bytes, at the root; one `find` over the
|
||||||
|
whole extract). Its `[LANGUAGE]` section maps the console's `XC_LANGUAGE_*`
|
||||||
|
value to `eng`/`jpn`/`deu`/`fra`/`esp`/`ita`, defaulting to `eng` — that code
|
||||||
|
selects `GP_TITLE`'s English or Japanese build and the `<lang>.pak` families.
|
||||||
|
✅ decoded.
|
||||||
|
❔ Its `[SYSTEM]` section — which the file's own comment says holds what the
|
||||||
|
game and every game part share — is **empty**, so the boot *order* is not in
|
||||||
|
disc-side configuration at all.
|
||||||
|
* **Five GameParts are named but never registered.** Pulling every
|
||||||
|
`RegisterToFactory<N, class silph::GamePart_X>` diagnostic string binds **24 of
|
||||||
|
the 29 ids to a C++ class**
|
||||||
|
([`data/gamepart-class-ids.txt`](../re/data/gamepart-class-ids.txt)). Ids `1`,
|
||||||
|
`2`, `16`, `18`, `28` have no registration site — including
|
||||||
|
**`GP_ADVERTISE_DEMO` (1)**, which agrees with the measurement that the attract
|
||||||
|
loop is the *title* replaying `ADV.wmv` rather than a separate part. 🟡 an
|
||||||
|
argument from a diagnostic string, not from the code.
|
||||||
|
Also: **`3` and `4` are both `GamePart_SaveLoad`** — one part, two ids.
|
||||||
|
|
||||||
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
|
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
|
||||||
executable's own registration strings. ✅ This is the screen vocabulary; which
|
executable's own registration strings. ✅ This is the screen vocabulary; which
|
||||||
button reaches which entry is Q4 and is *not* part of it.
|
button reaches which entry is Q4 and is *not* part of it.
|
||||||
* **The logo splash is a screen, not a video.** `logo1`–`logo4` are
|
* **The logo splash is a screen, not a video, and it renders.** `logo1`–`logo4`
|
||||||
manifest-bound with no `.wmv` on the disc. ✅
|
are manifest-bound with no `.wmv` on the disc. ✅ The screen is **two** bundles
|
||||||
|
in `GP_TITLE.pak`, each shipped twice: entries **10/13** the white
|
||||||
|
`SQUARE ENIX` publisher logo, entries **11/14** `GAME ARTS`/`SETA`/`studio
|
||||||
|
anima`. ⚠️ They are **invisible to the default `screen list`/`render`** —
|
||||||
|
`is_build` rejects them for having no `.rat` child. Pass **`--all`**, which
|
||||||
|
renumbers `--build`. So the first of the five screens does have a reference
|
||||||
|
composite. ✅ **And a capture**: both halves edge-correlate to their renders at
|
||||||
|
**0.91** and **0.98** at zero shift, each rejected by the other (0.03, −0.03).
|
||||||
|
✅ **Its timing is DECODED, not just measured.** The splash **fades both ways**
|
||||||
|
and the bundles carry the keyframes: `SQUARE ENIX` `[15 30 235 239 251 255]`,
|
||||||
|
developer logos `[15 30 190 194 206 210]`, each with an `_eff` glow child on
|
||||||
|
`[15 30 45]`. Under `1 unit = 1/60 s` that is a 0.25 s ramp in, a **3.42 s**
|
||||||
|
/ **2.67 s** hold, and a **0.33 s** fade out — and a 10 fps capture measures
|
||||||
|
≈ 3.5 s / ≈ 2.4 s holds and **≈ 0.3 s** fade-outs. The visible brightness
|
||||||
|
overshoot on the way in is the `_eff` glow ramping after the logo, not a
|
||||||
|
rendering artifact. Wall-clock: logos ≈ 0.4–4.7 s and ≈ 4.9–8.4 s, then
|
||||||
|
`ADV.wmv` from ≈ 8.9 s. ⚠️ The cyan `SQUARE ENIX` at ≈ 9.5 s is the
|
||||||
|
**movie's** opening, not a third splash screen.
|
||||||
* **Sprites carry their own labels.** No font rendering or localisation is needed
|
* **Sprites carry their own labels.** No font rendering or localisation is needed
|
||||||
for this milestone. ✅
|
for this milestone. ✅ — and the localisation is *already baked in*: the
|
||||||
|
Japanese screens are separate builds in the same pak, not a text swap.
|
||||||
|
|
||||||
## Facts the port will trip over
|
## Facts the port will trip over
|
||||||
|
|
||||||
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||||||
plays only Ogg Theora natively. How to handle that is the port's decision, not
|
plays only Ogg Theora natively. How to handle that is the port's decision, not
|
||||||
ours — but it is not optional.
|
ours — but it is not optional.
|
||||||
* **The disc holds 3.3 GB of video.** Only the boot intro and the one new-game
|
* **The disc holds 3.3 GB of video, and exactly two files are in scope:**
|
||||||
intro are in scope.
|
`dat/movie/ADV.wmv` (the boot intro *and* the attract loop — one asset) and
|
||||||
|
`dat/movie/S00A.wmv` (the new-game intro, 93.9 s). Named from the movie
|
||||||
|
manifest, ✅ decoded.
|
||||||
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
|
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
|
||||||
highest-offset entry in `sound.pak` and its size field is an allocation size. A
|
highest-offset entry in `sound.pak` and its size field is an allocation size. A
|
||||||
reader must allow a short read there and only there.
|
reader must allow a short read there and only there.
|
||||||
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
||||||
for spoken lines and discards half a music mix.
|
for spoken lines and discards half a music mix.
|
||||||
* **A music bank has several sub-waves and we glue them together.** `BGM_001`
|
* **A music bank is TWO STEMS THAT PLAY TOGETHER — do not concatenate.**
|
||||||
is 10 KB + 4.47 MB + 4.67 MB, concatenated into one 347 s track. Nobody has
|
✅ The "10 KB + 4.47 MB + 4.67 MB" reading was wrong: the 10 KB is the bank
|
||||||
established whether those are intro + loop, two variations, or two halves —
|
header, and a bank is exactly **two waves of identical duration** (32/32 banks
|
||||||
see Q10. Do not build menu looping on the concatenated track until it is
|
on the disc). `BGM_001`'s two are **sample-synchronous** — transient
|
||||||
answered.
|
correlation peaks at lag 0.00 s over ±5 s, and both stop at the same
|
||||||
|
millisecond, 167.663 s. Wave 1 is quieter, far more L/R-decorrelated and has
|
||||||
|
almost no bass, so it reads as a surround-rear pair or a second intensity
|
||||||
|
layer — 🟡 which of those is unsettled, and `ChannelMask` is `0x0002` on both,
|
||||||
|
so the file will not say. Today's 347 s concatenation plays the piece twice,
|
||||||
|
the second time as a bass-less stem.
|
||||||
|
❔ **And it is not a seamless loop**: `BGM_001` fades out at 167.663 s and is
|
||||||
|
followed by 6.15 s of silence, with no loop-point field identified. A menu loop
|
||||||
|
is authored.
|
||||||
|
✅ **The menu's music is `BGM_103`.** The cue *table* cannot say — its BGM
|
||||||
|
entries are numeric — but `GamePart_Title`'s `sub_821C5580` plays **cue 1103**,
|
||||||
|
and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are
|
||||||
|
byte-for-byte the two streams the XMA probe saw decoding at the main menu.
|
||||||
|
Static code, disc census and runtime all agree. The port does **not** have to
|
||||||
|
choose a track.
|
||||||
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
||||||
number of XMA1 packets from any known data offset.
|
number of XMA1 packets from any known data offset.
|
||||||
|
|
||||||
|
## What is still open
|
||||||
|
|
||||||
|
Every question above is answered, so this is the honest residue rather than a
|
||||||
|
work queue. **None of it blocks the five screens.** (Q1's seconds conversion was
|
||||||
|
here until 2026-08-28 and is now settled.)
|
||||||
|
|
||||||
|
| | what | why it is stuck |
|
||||||
|
|---|---|---|
|
||||||
|
| 🟡 | **cue NAME → event binding** (Q8) | event→**wave** is measured for move/confirm/back; that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still read off the authors' identifiers |
|
||||||
|
| ❔ | **the other ~319 SE cues** (Q8) | located one at a time by triggering them; only the three the menu needs have been done |
|
||||||
|
| 🟡 | **the paint-order tie-break** (Q3) | eight candidates refuted; costs one element's blend on one screen |
|
||||||
|
| 🟡 | **GamePart ids behind the buttons** (Q4) | the *screens* are measured; the ids are a name match onto the executable's class names |
|
||||||
|
| 🟡 | **the boot transitions in code** (Q6) | both levels decoded — phase at `this+132` (`entry→2`, `2→0`, `2→3`, `3→4`, `4→2`) and state at `this+136` inside phase 4. Phase 0 = splash (`LOGO`), phase 2 = title + `PRESS Ⓐ`, phase 4 = menu. Unknown: what the event *numbers* mean |
|
||||||
|
| ❔ | **builds 0/1 and 10/11**, the `DELTASABER` plates (Q2) | never seen anywhere in the boot path, the title-side screens or the attract loop. A mission load is the remaining candidate and this container kills runs before one completes |
|
||||||
|
|
||||||
|
(An earlier version of this table called the audio items blocked on "an emulator
|
||||||
|
whose audio path can be observed". That was wrong — this build already has
|
||||||
|
`--xma_param_probe`, and using it settled both.)
|
||||||
|
|
||||||
|
⚠️ **A container caveat that bounds all of the above:** the emulator has twice
|
||||||
|
been killed mid-run with no crash line in its own log (at ~50 s and ~145 s), on a
|
||||||
|
box sitting at ~1 GB free with swap exhausted. Dynamic experiments here have to
|
||||||
|
fit in roughly two minutes of guest time, which is why several of these residuals
|
||||||
|
are unfinished rather than unattempted.
|
||||||
|
|
||||||
## Reference data
|
## Reference data
|
||||||
|
|
||||||
Committed alongside the findings, so the port can be built without a disc in the
|
Committed alongside the findings, so the port can be built without a disc in the
|
||||||
|
|||||||
@@ -75,7 +75,160 @@ alongside it.
|
|||||||
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
|
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
|
||||||
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
|
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
|
||||||
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
||||||
| **S1** | **Ready Room probe.** *Gated* — one iteration, then stop | A written go/no-go (see below) |
|
| **S1** | ~~**Ready Room probe.**~~ **DONE 2026-08-28 — [no-go](../re/ready-room-probe.md).** It is 2D and enumerates fine, but the pak is briefing/tactical-map content, not the Ready Room menu | ✅ go/no-go written |
|
||||||
|
|
||||||
|
## 🔴 Emulator-side questions are blocked — the title is not reachable here
|
||||||
|
|
||||||
|
**Status 2026-08-29, instrument-verified.** Two open items need a running menu:
|
||||||
|
the gamma control behind [tone curve](../re/structures/ui-render-tone-curve.md),
|
||||||
|
and separating `8AX` from `ptbase` in
|
||||||
|
[8AX](../re/structures/ui-8ax-fullres-background.md). Both are blocked.
|
||||||
|
|
||||||
|
✅ **The negative is now solid.** A probe that restarts its capture stream every
|
||||||
|
30 s and cross-checks itself against an independent grabber every 60 s
|
||||||
|
(9 checks, 8 agreeing to within 2 %) ran **560 continuous seconds from launch at
|
||||||
|
3.30 fps**: the interactive title's green Ⓐ plate never appeared, while the game
|
||||||
|
rendered throughout. The final frame correlates 0.0145 / −0.0047 / 0.0102 with
|
||||||
|
our title / main-menu / `EXTRAS` renders — attract-movie content.
|
||||||
|
|
||||||
|
❔ **Why is unknown.** `live-title-press-a.png` (753 glyph pixels) proves it was
|
||||||
|
reachable from this container on 2026-08-28. Clearing the shader cache fixed a
|
||||||
|
*black surface* but not this.
|
||||||
|
|
||||||
|
⚠️ Neither blocked item blocks the five menu screens. See
|
||||||
|
[capture-harness-status](../re/capture-harness-status.md) for the full trail,
|
||||||
|
including three earlier "the title never appears" claims that were withdrawn
|
||||||
|
because the instrument was broken each time.
|
||||||
|
|
||||||
|
## 🟡 Needs one more run — a Japanese-locale capture
|
||||||
|
|
||||||
|
Recorded rather than worked around, per "do not improvise around a blocker".
|
||||||
|
|
||||||
|
Two questions have converged on needing **one capture we cannot take**: a running
|
||||||
|
capture of `GP_TITLE` **build 7**, the Japanese title screen.
|
||||||
|
|
||||||
|
* Q1's keyframe-time association. The shifted reading is favoured 26× by a
|
||||||
|
calibration-free measurement, and the only render it changes on the whole disc
|
||||||
|
is build 7 ([`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md)).
|
||||||
|
* What `rest()` should return for a plateau-less element. The one element that
|
||||||
|
discriminates, `ptlogo_eff3.t32`, is also in build 7
|
||||||
|
([`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)).
|
||||||
|
|
||||||
|
**🔴 The "cannot" was wrong, and is withdrawn (2026-08-29).** It is true that
|
||||||
|
`user_language` is only ever `DECLARE_int32` here, with no `DEFINE` and no entry
|
||||||
|
in `xenia-canary.config.toml`, so there is no flag to pass — and passing an
|
||||||
|
unknown one is specifically dangerous, because `run-canary`'s own header records
|
||||||
|
that xenia calls `ShowSimpleMessageBox` from `ParseLaunchArguments` *before*
|
||||||
|
logging starts. But the cvar is not the only route, and I stopped at the first
|
||||||
|
one I checked.
|
||||||
|
|
||||||
|
**The language is persisted, and canary's own file is writable.**
|
||||||
|
`kernel_state.cc` builds `XConfig` over `<storage_root>/xconfig.settings`, which
|
||||||
|
exists here at `/sylph-home/re/.local/share/Xenia/xconfig.settings` (6 680 B).
|
||||||
|
`user.language` is a **big-endian u32 at file offset `0x912`**, located by three
|
||||||
|
independent landmarks rather than guessed:
|
||||||
|
|
||||||
|
| landmark | expected | found |
|
||||||
|
|---|---|---|
|
||||||
|
| `music_volume` (`User+449`) | `0.7f` | BE float at 2727 → `User` base `0x8e6` |
|
||||||
|
| `language` (`User+44`) | `1` = `kEnglish`, the hard-coded default | **1** at `0x912` |
|
||||||
|
| `country` (`User+64`) | United States | `103` at `0x926` |
|
||||||
|
|
||||||
|
`XLanguage::kJapanese = 2` (`xbox.h:307`). Writing 2 there and restoring
|
||||||
|
afterwards is using canary's own persistence, not patching its code.
|
||||||
|
|
||||||
|
**🔴 DIAGNOSED 2026-08-29 — the harness, not the game.** `screenshot` costs
|
||||||
|
**10.8 s while xenia is running** and **0.117 s once it is killed** (92×, at a
|
||||||
|
1-minute load average of 1.80, so it is contention with the emulator). A
|
||||||
|
two-grab polling loop therefore samples every **~41 s**, against a title screen
|
||||||
|
this corpus documents as lasting *a few seconds*. Four runs — two locales, two
|
||||||
|
launch paths, two gamma settings — were all blinking slower than the event.
|
||||||
|
🔴 **…and that diagnosis is itself refuted (same day).** The faster probe was
|
||||||
|
built — 3.98 s → **0.29 s** per sample, 13.7×, control-verified at 753/327 — and
|
||||||
|
at 3.99 fps for **420 continuous seconds (1 674 samples)** the interactive title
|
||||||
|
*still* never appeared. Sampling rate was a real defect and not the cause.
|
||||||
|
✅ So "the interactive title does not appear mid-run without a pad press" is
|
||||||
|
reinstated, now as a dense measurement. ⚠️ Its reach is a **mid-run** window; it
|
||||||
|
says nothing about the boot title.
|
||||||
|
🟡 Leading hypothesis, unconfirmed: the plate appears only in the **boot** title
|
||||||
|
window and the attract loop's title has none — which is precisely what
|
||||||
|
`title_states_capture.sh` was written to test. The experiment is to start the
|
||||||
|
fast probe from t=0, not attach to a run already in progress.
|
||||||
|
See [capture-harness-status](../re/capture-harness-status.md).
|
||||||
|
|
||||||
|
**🟡 Still not settled — two runs, and the reason moved again.**
|
||||||
|
|
||||||
|
*Run 1 (2026-08-28)* reported "title not seen" in 787 s. **That was a broken
|
||||||
|
tool, not the game.** `wait_title.sh` still carried the single-pixel oracle that
|
||||||
|
`is_title.py` was written to replace — a 1280×720 coordinate sampled against the
|
||||||
|
1279×675 game surface, so it always reads the copyright line. Fixed; it now
|
||||||
|
delegates to `is_title.py`.
|
||||||
|
|
||||||
|
*Run 2 (2026-08-29)*, with the working oracle and the profile flag the English
|
||||||
|
captures use, **still did not reach the interactive title.** Not one frame in the
|
||||||
|
run showed a single green-Ⓐ glyph pixel, and content correlation against either
|
||||||
|
build-7 render never exceeded **0.22**. The game sat in the attract movie
|
||||||
|
throughout, polling `XamInputGetKeystrokeEx` (601 calls) — i.e. alive and waiting
|
||||||
|
for input, not hung.
|
||||||
|
|
||||||
|
**✅ The control was run (2026-08-29), and it removes the locale from the
|
||||||
|
picture.** Same flags, same oracle, English locale: **75 samples over 734 s, every
|
||||||
|
one glyph = 0.** The English boot does not present the interactive title either.
|
||||||
|
Canary was alive throughout and polling `XamInputGetKeystrokeEx` (1 801 calls);
|
||||||
|
the frame at 734 s has content but correlates only **0.15** with build 4's render
|
||||||
|
and 0.05 with the main menu, i.e. it is an attract-movie frame, not title art.
|
||||||
|
|
||||||
|
So the Japanese run was **not** failing because of the locale — neither locale
|
||||||
|
reaches the interactive title in ~12 minutes of no pad input. **🔴 Tried the proven path too, and I am stopping this line (2026-08-29).**
|
||||||
|
Run 3 launched exactly the way `boot_menu.sh` does — `DISPLAY=:98`, `--apu=sdl`,
|
||||||
|
`/dev/shm/xenia_*` cleared, the existing profile signed in — and drove
|
||||||
|
`skip_intro.sh`, the detector that is documented to work. It classified **every
|
||||||
|
one of 20 samples over 604 s as "movie"** and waited them all out, then timed
|
||||||
|
out. A direct check at 604 s confirms it was right: zero green-glyph pixels,
|
||||||
|
`screen_id` = other, warm mean (69,53,40), correlation **0.09** with build 7. The
|
||||||
|
game really was playing attract movies for ten minutes.
|
||||||
|
|
||||||
|
So: **three runs, two locales, two launch paths, ~35 minutes of emulator time, no
|
||||||
|
interactive title.** Either the attract loop is far longer than the 600–780 s
|
||||||
|
windows tried, or something has regressed since `live-title-press-a.png` was
|
||||||
|
captured (that one came from a pad-driven boot — commit `88b3ce9`, "booting to
|
||||||
|
the main menu and walking it").
|
||||||
|
|
||||||
|
⚠️ **Not worth more loop iterations.** This capture does not block any of the
|
||||||
|
five menu screens, and it has now cost five. It is written down so a later
|
||||||
|
session with a reason to spend an hour on the boot path can pick it up; the
|
||||||
|
locale mechanism, the fixed oracle and the launch recipe are all in place, so
|
||||||
|
what is left is patience with the attract loop, not tooling.
|
||||||
|
|
||||||
|
The locale is restored to English; `set_console_language.py ja` flips it back in
|
||||||
|
one command.
|
||||||
|
|
||||||
|
⚠️ Neither question blocks the five menu screens. Q1's *interpolation law* is
|
||||||
|
settled and only multi-keyframe absolute timing is open; `rest()` differs from
|
||||||
|
its alternative on **one** element across all five screens, and the current
|
||||||
|
answer there is the defensible one.
|
||||||
|
|
||||||
|
## 🔵 Needs a human decision — rotation (raised 2026-08-29)
|
||||||
|
|
||||||
|
The port agent asks whether it should **render** `rotation_deg` (decoded at
|
||||||
|
keyframe `+12`) when `sylpheed-cli screen render` deliberately does not. Its own
|
||||||
|
framing is the reason this is not mine to settle: if the port rotates and the
|
||||||
|
reference renderer does not, then `verify-screen` reports a large title diff that
|
||||||
|
means *"the port is right"* — a silently inverted signal.
|
||||||
|
|
||||||
|
The RE half is answered and is in HANDOFF: rotation is about the **declared
|
||||||
|
pivot** (measured against a GPU capture, not assumed), and it changes nothing on
|
||||||
|
the five screens **at rest**.
|
||||||
|
|
||||||
|
What needs a decision is which way the divergence gets closed:
|
||||||
|
|
||||||
|
* teach `ui_layout::blit` a rotating path, so the two renderers stay comparable
|
||||||
|
and the diff keeps meaning "someone is wrong" — costs work in the reference
|
||||||
|
renderer, which is otherwise not on the port's critical path; or
|
||||||
|
* let the port render rotation and mark the title as a known-divergent screen in
|
||||||
|
`verify-screen`, accepting a check that no longer guards the title.
|
||||||
|
|
||||||
|
Recorded rather than chosen, per "do not improvise around a blocker".
|
||||||
|
|
||||||
## Known unknowns — say so, do not fill them in
|
## Known unknowns — say so, do not fill them in
|
||||||
|
|
||||||
@@ -118,6 +271,35 @@ answer is no-go by definition, not "try harder".
|
|||||||
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
|
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
|
||||||
authority.
|
authority.
|
||||||
|
|
||||||
|
## Publishing a decoder state the port pins
|
||||||
|
|
||||||
|
The port's exporter depends on `sylpheed-formats` **by revision**, so a commit of
|
||||||
|
yours becomes part of its build. That creates an obligation that is easy to miss:
|
||||||
|
|
||||||
|
⚠️ **A commit reachable only from an `auto/*` branch is not safe to pin.** If the
|
||||||
|
branch is later deleted, or — worse — **squash-merged**, the object is orphaned.
|
||||||
|
Squash creates *new* commits, so `main` appears to contain the work while the
|
||||||
|
pinned sha becomes unreachable, and the port stops building for anyone doing a
|
||||||
|
fresh checkout. Silently, at their build, long after the moment of breakage.
|
||||||
|
|
||||||
|
**So when you land something the port needs, tag it:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a formats-pin-$(date +%F) -m "what the port gets from this state"
|
||||||
|
push-work # --follow-tags publishes annotated tags with the branch
|
||||||
|
```
|
||||||
|
|
||||||
|
A tag is a permanent ref. It survives branch deletion and squash-merge, it is
|
||||||
|
self-documenting in the port's `Cargo.toml`, and it fails loudly at *fetch* if it
|
||||||
|
ever goes missing rather than silently at build.
|
||||||
|
|
||||||
|
Then tell the port over the message channel that a new pin exists. It bumps
|
||||||
|
deliberately, as its own commit — that is how it stays current without floating,
|
||||||
|
which would only give it staleness it cannot see.
|
||||||
|
|
||||||
|
`formats-pin-2026-08-29` at `7eeae30` is the first, created because the port had
|
||||||
|
already pinned a commit that lived on one topic branch and nowhere else.
|
||||||
|
|
||||||
## Handing it over
|
## Handing it over
|
||||||
|
|
||||||
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it
|
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
|||||||
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
|
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
|
||||||
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
|
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
|
||||||
| IDXD nameless field keys | ✅/❌ | [idxd-unnamed-keys](structures/idxd-unnamed-keys.md) + [`tools/re-capture/idxd_unnamed_keys.py`](../../tools/re-capture/idxd_unnamed_keys.py) | Census of every field entry whose `name_off` is `0xFFFFFFFF`, disc-wide: **7 750 objects, 2 757 039 field entries, 0 parse failures**, `tag_hash` reproducing **1 271 462/1 271 462** named keys. **7 094 distinct keys are never named — and 7 052 of them are not hashes at all**, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; `tag_hash("BGM_001")` is `0xC662435B` while the key valued `BGM_001.slb` is `0x000003E9`). ⚠️ **The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names** — 42 distinct keys × 6 language copies × 2 records. All 42 are **ISL script-symbol hashes** in `<lang>\script\ID.tbl` (GP_READY_ROOM.pak), the link map built by `PrepareScript`'s "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the `.isb` bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the **trailing digits of 30 of the 42 names** (deltas of exactly `+0x01000001` across `stage01..09`, `stage10..16`, `challenge01..06`; `+0x01010000` across `tutorial0101..0601`). ❌ **No name was cracked, and the negative is quantified**: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers `"Stage01"` from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has **1 176** preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely |
|
| IDXD nameless field keys | ✅/❌ | [idxd-unnamed-keys](structures/idxd-unnamed-keys.md) + [`tools/re-capture/idxd_unnamed_keys.py`](../../tools/re-capture/idxd_unnamed_keys.py) | Census of every field entry whose `name_off` is `0xFFFFFFFF`, disc-wide: **7 750 objects, 2 757 039 field entries, 0 parse failures**, `tag_hash` reproducing **1 271 462/1 271 462** named keys. **7 094 distinct keys are never named — and 7 052 of them are not hashes at all**, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; `tag_hash("BGM_001")` is `0xC662435B` while the key valued `BGM_001.slb` is `0x000003E9`). ⚠️ **The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names** — 42 distinct keys × 6 language copies × 2 records. All 42 are **ISL script-symbol hashes** in `<lang>\script\ID.tbl` (GP_READY_ROOM.pak), the link map built by `PrepareScript`'s "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the `.isb` bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the **trailing digits of 30 of the 42 names** (deltas of exactly `+0x01000001` across `stage01..09`, `stage10..16`, `challenge01..06`; `+0x01010000` across `tutorial0101..0601`). ❌ **No name was cracked, and the negative is quantified**: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers `"Stage01"` from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has **1 176** preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely |
|
||||||
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔ |
|
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔. **2026-08-29, for the UI path only:** our composite is brighter than the emulator's frame by a gamma of **≈1.34–1.49** across three screens ([tone curve](structures/ui-render-tone-curve.md)) — 🟡 measured, not decoded, constrained only over render values ~0–60, and possibly canary's own `kernel_display_gamma_type = 2` (BT.709) output stage rather than the game's |
|
||||||
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
|
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
|
||||||
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
|
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | **✅ 2026-08-29: a child's NAME is stated by an `opt ` block** (`"opt " | BE32 len | name | NUL | 3 bytes | magic`), not by the printable bytes before it. We had been guessing it from the last printable run, which is right 17 918 times and **wrong 24**, each time because the 3-byte tail is itself printable — `8AX` (×22) and `'OX` (×2). `8AX` is **not a name**; it hid `pteff05.t32`/`pteff04.t32`, the full-resolution background of all five menu screens, which `compose` then dropped with no diagnostic ([ratc-child-names](structures/ratc-child-names.md)). ✅ 60 of 18 002 children carry no `opt ` block, and all 60 are explained: they are the ten frames of the disc's only `.tan` **frame sequence** (`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle, 6 × 10 = 60), where one `opt ` block names the whole run — so `parse` over-reports frames as children there. A census of all **18 718** `opt ` names shows a bundle names exactly six kinds of resource: `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6 ([tan-frame-sequence](structures/ratc-tan-frame-sequence.md)). Child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **✅ 10 148 of 10 148 references resolve, as of 2026-08-29.** The 4 that did not were `pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, recorded as "`pmbase.t32` is on the disc nowhere". It was on the disc all along, as the child the printable-run scan named `8AX` — the `opt ` name fix above resolves it in all four builds (3 686 767 B each). A dangling reference that closes itself when an unrelated decode lands is the corroboration that decode wanted |
|
||||||
| LSTA sprite list | ✅ | `sylpheed-formats/src/lsta.rs` | A display list of inline elements: **T8aD sprites and `PRMD` primitives**. The `count` at `0x04` is **exact and counts both** — `count == T8aD + PRMD` for **64/64** lists on the disc, which retires the old "a few entries disagree" note (it compared sprites against a total including primitives). **All 1 281 sprite frames decode** after the T8aD rectangle-list fix |
|
| LSTA sprite list | ✅ | `sylpheed-formats/src/lsta.rs` | A display list of inline elements: **T8aD sprites and `PRMD` primitives**. The `count` at `0x04` is **exact and counts both** — `count == T8aD + PRMD` for **64/64** lists on the disc, which retires the old "a few entries disagree" note (it compared sprites against a total including primitives). **All 1 281 sprite frames decode** after the T8aD rectangle-list fix |
|
||||||
| IXUD subtitle + caption text | ✅ | `sylpheed-formats/src/ixud.rs` + `movie_subtitle.rs` ([container](structures/idxd-container.md) · [movie link](movie-subtitle-link.md)) | **The IXUD record/field table is decoded and wired in (2026-08-26)** — `IxudObject` mirrors `IdxdObject`; uniform 16-byte records, 12-byte fields, every offset in **chars**, and the word at `0x08` is record 0's hash, not a schema id. Verified disc-wide: **1104/1104** objects, **1476/1476** records, **628 165/628 165** named fields reproducing their `ixud_hash` (`tests/ixud_records_disc.rs`). **Caption text: 537 → 8800 lines, which is 8800 of 8800 distinct keys.** Two steps — generalising the key parser from `MSG_DEMO_*` to all **eight** families (`ACRO ADAN ADPL BIRD DEMO RHIN TCAF` use `MSG_<FAM>_<id>_<page>_<line>`, `VOICE` alone inserts a family letter) took 537 → 8074; switching from **token adjacency to record fields** took it to 8800. ⚠️ An earlier "1.3 % of the game's text" figure of mine counted *occurrences across blocks* — the honest denominator is **8800 distinct keys**, so the real starting point was 6.1 %. The `DEMO` control shows why the field route matters: token adjacency finds 537 lines there, fields find **541** — it was dropping lines in the one family it was written for. | timed cues. **The movie↔subtitle↔voice link is solved — statically**, and as of 2026-08-25 read from the IDXD **record table** rather than scraped from the string pool: **104 cutscene slots binding 101 distinct movies**, 99 slots / 96 movies with a subtitle, 99 / 96 with a voice track, 22 / 22 with a telop. ⚠️ The previous counts (94 / 83 / 21) were the numbers of **distinct pool strings** — a repeat reference contributes no token, so 13 later `VOICE_D_450..454` references read as "no binding". **All 18 hokyu movies are bound**, not five. 93 of the 94 distinct subtitle members resolve; `SUBTITLE_S12B.tbl` resolves in none of the six languages — a dangling reference on the disc. The ~104 **script ids are no longer ❔**: they are literal positional field keys in `BASE_INFO`, each naming its record, and all 104 resolve. `movie_manifest::parse` now reads the record table; CSV regenerated by `examples/movie_map_csv.rs` |
|
| IXUD subtitle + caption text | ✅ | `sylpheed-formats/src/ixud.rs` + `movie_subtitle.rs` ([container](structures/idxd-container.md) · [movie link](movie-subtitle-link.md)) | **The IXUD record/field table is decoded and wired in (2026-08-26)** — `IxudObject` mirrors `IdxdObject`; uniform 16-byte records, 12-byte fields, every offset in **chars**, and the word at `0x08` is record 0's hash, not a schema id. Verified disc-wide: **1104/1104** objects, **1476/1476** records, **628 165/628 165** named fields reproducing their `ixud_hash` (`tests/ixud_records_disc.rs`). **Caption text: 537 → 8800 lines, which is 8800 of 8800 distinct keys.** Two steps — generalising the key parser from `MSG_DEMO_*` to all **eight** families (`ACRO ADAN ADPL BIRD DEMO RHIN TCAF` use `MSG_<FAM>_<id>_<page>_<line>`, `VOICE` alone inserts a family letter) took 537 → 8074; switching from **token adjacency to record fields** took it to 8800. ⚠️ An earlier "1.3 % of the game's text" figure of mine counted *occurrences across blocks* — the honest denominator is **8800 distinct keys**, so the real starting point was 6.1 %. The `DEMO` control shows why the field route matters: token adjacency finds 537 lines there, fields find **541** — it was dropping lines in the one family it was written for. | timed cues. **The movie↔subtitle↔voice link is solved — statically**, and as of 2026-08-25 read from the IDXD **record table** rather than scraped from the string pool: **104 cutscene slots binding 101 distinct movies**, 99 slots / 96 movies with a subtitle, 99 / 96 with a voice track, 22 / 22 with a telop. ⚠️ The previous counts (94 / 83 / 21) were the numbers of **distinct pool strings** — a repeat reference contributes no token, so 13 later `VOICE_D_450..454` references read as "no binding". **All 18 hokyu movies are bound**, not five. 93 of the 94 distinct subtitle members resolve; `SUBTITLE_S12B.tbl` resolves in none of the six languages — a dangling reference on the disc. The ~104 **script ids are no longer ❔**: they are literal positional field keys in `BASE_INFO`, each naming its record, and all 104 resolve. `movie_manifest::parse` now reads the record table; CSV regenerated by `examples/movie_map_csv.rs` |
|
||||||
| Fonts (ttf/otf/ttc) | ✅ | `sylpheed-formats/src/font.rs` | standard OpenType, parsed via ttf-parser |
|
| Fonts (ttf/otf/ttc) | ✅ | `sylpheed-formats/src/font.rs` | standard OpenType, parsed via ttf-parser |
|
||||||
@@ -30,7 +30,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
|||||||
| Weapon fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv). ⚠️ **CORRECTED 2026-08-26: the disc DOES carry a third of them.** Re-read through the [record table](structures/idxd-container.md), **1 514 of the 4 393** rows labelled `defaulted-on-disc` have a value on disc (2 879 genuinely absent) — measured here as an upper bound, since a field is counted when it appears in *any* record of the object; a per-record count gives ~1 448. The old reader could not name a record, so per-record fields read as absent. Spot-checked exactly: `wep_05`/`wep_60` `TriggerShotCount` = **4**, `wep_02` `Power` = **100.0**, `wep_60` `Power` = **1000.0** (refuting the recorded "C band ≈150…500" bracket), `wep_25` `MaximumRange` = **4000.0**, `wep_11/28/36/70` `LoadingCount` = **6/5/5/0**. The runtime capture's numbers all match the disc — what is withdrawn is the premise that it was reaching values the disc lacks. See [weapon-datasheet-runtime](weapon-datasheet-runtime.md) Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check |
|
| Weapon fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv). ⚠️ **CORRECTED 2026-08-26: the disc DOES carry a third of them.** Re-read through the [record table](structures/idxd-container.md), **1 514 of the 4 393** rows labelled `defaulted-on-disc` have a value on disc (2 879 genuinely absent) — measured here as an upper bound, since a field is counted when it appears in *any* record of the object; a per-record count gives ~1 448. The old reader could not name a record, so per-record fields read as absent. Spot-checked exactly: `wep_05`/`wep_60` `TriggerShotCount` = **4**, `wep_02` `Power` = **100.0**, `wep_60` `Power` = **1000.0** (refuting the recorded "C band ≈150…500" bracket), `wep_25` `MaximumRange` = **4000.0**, `wep_11/28/36/70` `LoadingCount` = **6/5/5/0**. The runtime capture's numbers all match the disc — what is withdrawn is the premise that it was reaching values the disc lacks. See [weapon-datasheet-runtime](weapon-datasheet-runtime.md) Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check |
|
||||||
| Unit (craft/vessel) fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. **Re-derived independently 2026-08-13 from the loader's own key strings** (`sub_82341A20`; the field name for each store is a string in the image): **159 fields**, agreeing with this solver on **25 of 25 shared offsets**, verified at **406 values matching the disc and 0 disagreeing** over 11 live objects spanning UNIT and VESSEL — landed as `data/unit_definition_layout.txt` + `sylpheed_formats::unit_layout` + a no-emulator test, with **121 defaulted fields** read out ([live-unit-definitions](live-unit-definitions.md)). ⚠️ **CORRECTED 2026-08-26:** those fields are **on the disc**. The ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the `Generic` / `Shield` / `Mass` / `SE` records — which is why a reader that could not name a record saw them as absent. And **"18 of 23 vessel records are missing at least one of `Size_X/Y/Z/HP`" is false: 0 of 114** objects with a `Generic.Type` (43 Craft + 71 Vessel) miss any of the four. See [unit-struct-runtime](structures/unit-struct-runtime.md) Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — ❌ **the sibling-default rules are WITHDRAWN (2026-08-25)** — `Size_Y` is on disc for **114/114** unit tables and **differs from `Size_X` in 90**; the old reader missed it exactly when the two were equal (a string-pool dedup artefact, cross-tab `seen+equal = 0` for all three pairs), so the rule was re-deriving the condition that hid the field. It also predicts wrong twice — `UN_e104_ADAN_Carrier.DefencePoint` and `UN_e011_ADAN_Attacker_B_HF_Wayne.FCSRange`. Read the record table instead — [values](captures/unit-runtime-fields.csv) |
|
| Unit (craft/vessel) fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. **Re-derived independently 2026-08-13 from the loader's own key strings** (`sub_82341A20`; the field name for each store is a string in the image): **159 fields**, agreeing with this solver on **25 of 25 shared offsets**, verified at **406 values matching the disc and 0 disagreeing** over 11 live objects spanning UNIT and VESSEL — landed as `data/unit_definition_layout.txt` + `sylpheed_formats::unit_layout` + a no-emulator test, with **121 defaulted fields** read out ([live-unit-definitions](live-unit-definitions.md)). ⚠️ **CORRECTED 2026-08-26:** those fields are **on the disc**. The ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the `Generic` / `Shield` / `Mass` / `SE` records — which is why a reader that could not name a record saw them as absent. And **"18 of 23 vessel records are missing at least one of `Size_X/Y/Z/HP`" is false: 0 of 114** objects with a `Generic.Type` (43 Craft + 71 Vessel) miss any of the four. See [unit-struct-runtime](structures/unit-struct-runtime.md) Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — ❌ **the sibling-default rules are WITHDRAWN (2026-08-25)** — `Size_Y` is on disc for **114/114** unit tables and **differs from `Size_X` in 90**; the old reader missed it exactly when the two were equal (a string-pool dedup artefact, cross-tab `seen+equal = 0` for all three pairs), so the rule was re-deriving the condition that hid the field. It also predicts wrong twice — `UN_e104_ADAN_Carrier.DefencePoint` and `UN_e011_ADAN_Attacker_B_HF_Wayne.FCSRange`. Read the record table instead — [values](captures/unit-runtime-fields.csv) |
|
||||||
| Arsenal develop economy | ✅/❔ | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000–350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed – / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order — the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered |
|
| Arsenal develop economy | ✅/❔ | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000–350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed – / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order — the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered |
|
||||||
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly |
|
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly. **✅ 2026-08-28: the keyframe block's `+12` is a screen-plane ROTATION in degrees**, clockwise-positive — confirmed against a GPU capture (declared 30 / −45 vs measured +30.26° / −45.28°) and non-zero in 14.50 % of 83 862 blocks disc-wide ([ui-keyframe-rotation](structures/ui-keyframe-rotation.md)). ⚠️ Every rotation on the disc is inside a **nested** `.rat` leaf record, and `ui_layout::blit` is axis-aligned, so the field is decoded but **not rendered**. 🟡 `+4`/`+8` remain unexplained |
|
||||||
| UI screen paint order | ✅ | [runtime screen object](structures/ui-screen-runtime.md) + [title capture](ui-title-paint-order-capture.md) | **SOLVED**: the game's screen object keeps a second, reordered list of its elements — the child array at `+0x30` — and that is the paint order, not the declaration table. Read live from guest memory (found by the item vtable `0x820b30b4`) and checked against the draw capture: the seven nameable elements sit at child slots 0, 6, 7, 13, 16, 17, 22, strictly ascending, exactly as captured; it also settles the one pair no static field could order. 🟡 deriving that order from the bundle — what the port needs — is still open |
|
| UI screen paint order | ✅ | [runtime screen object](structures/ui-screen-runtime.md) + [title capture](ui-title-paint-order-capture.md) | **SOLVED**: the game's screen object keeps a second, reordered list of its elements — the child array at `+0x30` — and that is the paint order, not the declaration table. Read live from guest memory (found by the item vtable `0x820b30b4`) and checked against the draw capture: the seven nameable elements sit at child slots 0, 6, 7, 13, 16, 17, 22, strictly ascending, exactly as captured; it also settles the one pair no static field could order. 🟡 deriving that order from the bundle — what the port needs — is still open |
|
||||||
| Scripted input / profile traps | ✅/🟡 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Why a scripted run appears unable to press Ⓐ: **F10 opens the emulator menu bar**, and any Xenia UI makes `XamInputGetKeystrokeEx` return SUCCESS with an empty keystroke *before* any driver is asked (Canary now logs `[RE-INPUT] … swallowed by IsUIActive`); the title needs a **signed-in profile** (hence `--create_profile_if_none`); and a **FIFO trace consumer that exits stalls the emulator**, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question |
|
| Scripted input / profile traps | ✅/🟡 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Why a scripted run appears unable to press Ⓐ: **F10 opens the emulator menu bar**, and any Xenia UI makes `XamInputGetKeystrokeEx` return SUCCESS with an empty keystroke *before* any driver is asked (Canary now logs `[RE-INPUT] … swallowed by IsUIActive`); the title needs a **signed-in profile** (hence `--create_profile_if_none`); and a **FIFO trace consumer that exits stalls the emulator**, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question |
|
||||||
| Title-screen guest crash | ✅ | [title-crash-stl-tree](title-crash-stl-tree.md) | The guest throws **`std::out_of_range`** from its cache-manager flush (`sub_823070B0`, an STL map/set erase that builds `'invalid map/set<T> iterator'`); the access violation after it is only the throw **returning**, because this build does not unwind guest EH. Trigger found and controlled: an **incomplete on-disc cache** (`~/.local/share/Xenia/cache/aab216c3`) throws ~100 s into a boot, a complete one never does — 2 runs each way. ❌ `mem_watch`, the handoff's suspect #1, is **eliminated**: cold cache + `--mem_watch=false` throws anyway |
|
| Title-screen guest crash | ✅ | [title-crash-stl-tree](title-crash-stl-tree.md) | The guest throws **`std::out_of_range`** from its cache-manager flush (`sub_823070B0`, an STL map/set erase that builds `'invalid map/set<T> iterator'`); the access violation after it is only the throw **returning**, because this build does not unwind guest EH. Trigger found and controlled: an **incomplete on-disc cache** (`~/.local/share/Xenia/cache/aab216c3`) throws ~100 s into a boot, a complete one never does — 2 runs each way. ❌ `mem_watch`, the handoff's suspect #1, is **eliminated**: cold cache + `--mem_watch=false` throws anyway |
|
||||||
@@ -103,6 +103,7 @@ files, which is how the same ground got covered twice.
|
|||||||
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` data offset = `(cumulative start of the `.pNN` segment) mod 2048`; the leading bytes are the previous bank's audio, not a header | ✅ CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier `first_riff % 2048` scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
|
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` data offset = `(cumulative start of the `.pNN` segment) mod 2048`; the leading bytes are the previous bank's audio, not a header | ✅ CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier `first_riff % 2048` scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
|
||||||
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
|
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
|
||||||
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
|
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
|
||||||
|
| [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md) | A music bank is two stems of one performance, played together | ✅ decoded structure, 32/32 banks two waves of equal duration (4 explained by the leading-region straddle); stems measured sample-synchronous; 🟡 rear-pair vs intensity-layer unsettled; ❔ no bank names a screen |
|
||||||
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
|
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
|
||||||
| [`structures/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuation*rand held in the lingering queue node; the 14 player-facing events are voiced by the player set alone (13/15 Katana tables, 0/129 others) (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue |
|
| [`structures/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuation*rand held in the lingering queue node; the 14 player-facing events are voiced by the player set alone (13/15 Katana tables, 0/129 others) (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue |
|
||||||
| [`structures/mission-scoring.md`](structures/mission-scoring.md) | Mission scoring and the S/A/B/C/D rank thresholds — 24 tables x 3 difficulties | ✅ CONFIRMED, one 22-field schema over 72 records; difficulty moves 10 of 22 fields and never the RankScore thresholds; ✅ all 24 named stage\StageParameter_S<NN>.tbl (+ _Tutorial shared by six, + _Test) |
|
| [`structures/mission-scoring.md`](structures/mission-scoring.md) | Mission scoring and the S/A/B/C/D rank thresholds — 24 tables x 3 difficulties | ✅ CONFIRMED, one 22-field schema over 72 records; difficulty moves 10 of 22 fields and never the RankScore thresholds; ✅ all 24 named stage\StageParameter_S<NN>.tbl (+ _Tutorial shared by six, + _Test) |
|
||||||
@@ -134,6 +135,7 @@ files, which is how the same ground got covered twice.
|
|||||||
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
|
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
|
||||||
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
|
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
|
||||||
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
|
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
|
||||||
|
| [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) | What a keyframe time is worth, and what shape the ramp has | ✅ CONFIRMED from the running game's own draw stream — the ramp is **linear** (a declared 15-unit fade lands on `round(255·k/15)` for all seven samples) and the animation clock advances **2 time units per submitted frame**. 🟡 the seconds conversion (`1 unit = 1/60 s`) rests on a measured 27.6 present-frames/second |
|
||||||
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
|
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
|
||||||
| [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | ✅ CONFIRMED by measurement over all 965 screen builds on the disc, |
|
| [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | ✅ CONFIRMED by measurement over all 965 screen builds on the disc, |
|
||||||
| [`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md) | The paint order comes from a layer key in the T8aD sprite header | ✅ CONFIRMED on both screens whose paint order has been measured — |
|
| [`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md) | The paint order comes from a layer key in the T8aD sprite header | ✅ CONFIRMED on both screens whose paint order has been measured — |
|
||||||
@@ -148,6 +150,13 @@ files, which is how the same ground got covered twice.
|
|||||||
| [`title-crash-stl-tree.md`](title-crash-stl-tree.md) | The title-screen crash is an STL `map`/`set` erase on a bad iterator | ✅ CONFIRMED — the guest throws std::out_of_range from an STL |
|
| [`title-crash-stl-tree.md`](title-crash-stl-tree.md) | The title-screen crash is an STL `map`/`set` erase on a bad iterator | ✅ CONFIRMED — the guest throws std::out_of_range from an STL |
|
||||||
| [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously |
|
| [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously |
|
||||||
| [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but |
|
| [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but |
|
||||||
|
| [`menu-navigation-semantics.md`](menu-navigation-semantics.md) | The title menu — how it moves, and where each button goes | ✅ measured: wraps both ends, Ⓑ restores focus, ⬅➡ inert; 4 of 5 destinations driven. 🟡 GamePart id is a name match, ❔ `NEW GAME` untested |
|
||||||
|
| [`screen-transitions.md`](screen-transitions.md) | Between two screens — a fade through black, and where its timing lives | ✅ the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; `GP_TITLE` = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc |
|
||||||
|
| [`menu-audio-cues.md`](menu-audio-cues.md) | Menu audio — the event vocabulary is on the disc, the binding is not | ✅ `SE_UI_*` cue names/ids decoded and `BANK_SE`→`Static.slb` (0/322 in FILES); 🟡 event binding is a name match; ❔ `Static.slb` has no wave boundaries, so SE audio is not extractable |
|
||||||
|
| [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) | What the game reads at boot — `config.ini`, and which GameParts exist | ✅ `config.ini` selects the language (the disc's only config); ❔ its `[SYSTEM]` is empty so the boot order is not in config; 🟡 24/29 ids bind to a class, `GP_ADVERTISE_DEMO` is never registered |
|
||||||
|
| [`movie-binding.md`](movie-binding.md) | Which movie plays where — boot intro, attract loop, new-game intro | ✅ decoded from the movie manifest (`ADVERTISE_MOVIE`→`ADV.wmv`, `MS00A`→`S00A.wmv`); attract identity confirmed independently by frame matching; 🟡 skippability unsettled |
|
||||||
|
| [`ready-room-probe.md`](ready-room-probe.md) | S1 — the Ready Room probe: no-go, and not for the reason expected | ✅ it is 2D and enumerates (60 builds), but the pak is briefing/tactical-map content; and `kind == 0x3002` finds 0 buttons there |
|
||||||
|
| [`ui-title-build-map.md`](ui-title-build-map.md) | Which `GP_TITLE` build is which screen state | ✅ CONFIRMED for title / `PRESS Ⓐ` / main menu / `EXTRAS` against live captures; the archive is 8 screens × EN/JP, and "6/8/9 are submenus" is withdrawn |
|
||||||
| [`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md) | The title screen's paint order, measured from the guest's draw submissions | ✅ CONFIRMED — the order in which the running game paints the title |
|
| [`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md) | The title screen's paint order, measured from the guest's draw submissions | ✅ CONFIRMED — the order in which the running game paints the title |
|
||||||
| [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad |
|
| [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad |
|
||||||
| [`weapon-datasheet-runtime.md`](weapon-datasheet-runtime.md) | Weapon DATA SHEET — runtime capture (Route B) | 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a |
|
| [`weapon-datasheet-runtime.md`](weapon-datasheet-runtime.md) | Weapon DATA SHEET — runtime capture (Route B) | 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a |
|
||||||
|
|||||||
@@ -103,3 +103,796 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
|||||||
out of noise.
|
out of noise.
|
||||||
* **A probe that never performs the action will "prove" the action does not
|
* **A probe that never performs the action will "prove" the action does not
|
||||||
exist.**
|
exist.**
|
||||||
|
* **The container's Canary binary can be older than the Canary source tree, and
|
||||||
|
the failure mode is a hang, not an error.** After a merge into `sylpheed-re`
|
||||||
|
the prebuilt `xenia_canary` had no `log_ui_draws`, no `mem_watch`, no
|
||||||
|
`create_profile_if_none` — and an unknown cvar makes xenia open an SDL message
|
||||||
|
box before logging is up, which headless is an unexplained freeze. Check before
|
||||||
|
trusting a harness flag: `nm -C <binary> | grep cvars::<flag>`, and
|
||||||
|
`build-canary Release` if it is missing.
|
||||||
|
* **A capture armed *at* a screen only ever sees the steady state.** Anything
|
||||||
|
about how a screen is built or animated has to be armed *before* it exists.
|
||||||
|
Re-arming every few seconds and keeping every log tiles the approach: each F10
|
||||||
|
opens a new numbered file and closes the previous one complete.
|
||||||
|
* **Measure animation in submitted frames, not in seconds.** `VdSwap` counts are
|
||||||
|
the guest's own frames, so an emulator at 80 % of real time does not move them;
|
||||||
|
a stopwatch reading does, silently and by an unknown factor.
|
||||||
|
* **~~This game's menus drop d-pad presses shorter than ~0.3 s.~~ WITHDRAWN
|
||||||
|
2026-08-28 — the menu WRAPS, and I had not measured that.** The claim came from
|
||||||
|
reading a cursor that ended up "one item short"; once wrap-around at both ends
|
||||||
|
was measured ([`menu-navigation-semantics.md`](menu-navigation-semantics.md)),
|
||||||
|
every one of those press counts is exactly right — four presses at 0.12 s moved
|
||||||
|
four steps *through the bottom*, which lands one above where a non-wrapping
|
||||||
|
menu would put it. **No press was ever dropped.** The real lesson is the
|
||||||
|
general one: *a step count is only readable once you know the topology*, and I
|
||||||
|
invented a hardware-flakiness story rather than testing the ends of the list.
|
||||||
|
Still true and worth keeping: screenshot after every step and read the cursor,
|
||||||
|
rather than trusting arithmetic over the press count.
|
||||||
|
* **A screenshot taken right after a transition can catch a screen mid-fade.**
|
||||||
|
A grab 2.5 s after Ⓑ returned the game to the title showed the title art with
|
||||||
|
no `PRESS Ⓐ BUTTON` plate; one second later the plate was there. That very
|
||||||
|
nearly went into the corpus as "the returned title has no plate". Sample a
|
||||||
|
changing screen several times before writing down what it does *not* contain.
|
||||||
|
* **Do not identify a menu cursor by label brightness.** The obvious oracle —
|
||||||
|
"the focused label is the brightest row" — fails on this game's menus, because
|
||||||
|
the background art is brighter behind some rows than the highlight is. It
|
||||||
|
confidently named the wrong item on a frame whose ring was plainly elsewhere.
|
||||||
|
Detect the **focus ring** in the gutter left of the labels instead
|
||||||
|
(`tools/re-capture/menu_focus.py`, 254 vs <82 — no threshold tuning needed),
|
||||||
|
and look at the PNG before believing either.
|
||||||
|
* **`screenshot` samples at 0.5 Hz — it cannot time an animation.** Measured:
|
||||||
|
~2 s per grab (an `import` of the root plus an ImageMagick crop). A 0.4 s fade
|
||||||
|
falls entirely between two samples, which is why a 40-frame burst across a
|
||||||
|
screen change looked like an instant cut. For anything timed, record the
|
||||||
|
display instead: `ffmpeg -f x11grab -framerate 30 -video_size <W>x<H> -i
|
||||||
|
:98+<X>,<Y> -t <secs>`, then read per-frame statistics off the file. Take the
|
||||||
|
geometry from `xwininfo -root -tree`, the same way `bin/screenshot` does.
|
||||||
|
* **A screen's brightness curve is not its fade quad.** The incoming screen's own
|
||||||
|
elements animate in *after* the transition quad has cleared, so mean luminance
|
||||||
|
keeps rising long after the fade is over — 1.47 s against a declared 0.97 s on
|
||||||
|
one screen. Time the fade from where the frame is *pure black*, and take the
|
||||||
|
ramp itself from the keyframes.
|
||||||
|
* **A `sound.pak` entry is not a bank.** Entry windows straddle bank boundaries
|
||||||
|
(the leading-region effect), so a naive "walk the RIFFs in this entry" gives
|
||||||
|
the tail of the previous bank, then one of your own, then the *next* bank's
|
||||||
|
header. It looks like a bank with the wrong number of waves. The tell is a
|
||||||
|
`BANK id=` that is not the entry's own number, and the fix is the `seek` packet
|
||||||
|
counts — they pin each wave's join exactly, so a straddle can be realigned
|
||||||
|
rather than thrown away. 4 of 32 BGM banks look like counterexamples until this
|
||||||
|
is applied.
|
||||||
|
* **Do not time a video whose start you did not see.** An attract cycle sampled
|
||||||
|
from partway through measured "≈85 s", which was used to argue it was not the
|
||||||
|
137 s `ADV.wmv`. It was `ADV.wmv`; the sampling began 39 s in. Identify a video
|
||||||
|
by **content** — match frames against the candidates and check the matched
|
||||||
|
playhead advances monotonically — before reasoning about its duration.
|
||||||
|
* **A near-black frame has no signature.** In frame-matching, low-contrast frames
|
||||||
|
drive every correlation toward zero and the ranking becomes noise; one control
|
||||||
|
frame scored 0.000 and another tied the wrong movie at 0.98. Read a dark
|
||||||
|
frame's runner-up as *no evidence*, not as evidence for the runner-up.
|
||||||
|
* **A screen that never changes is not necessarily hung — it may be a menu.** A
|
||||||
|
90-second screenshot loop that presses nothing will report a perfectly healthy
|
||||||
|
`DIFFICULTY` menu as a standing hang, and that reading survived in this corpus
|
||||||
|
for months. Before calling a static screen a hang, *look at it*, and press
|
||||||
|
something.
|
||||||
|
* **Never run `ps -ef` in this container.** Each of the three long-lived
|
||||||
|
processes carries the whole loop prompt as its argv, so the listing is tens of
|
||||||
|
kilobytes of the prompt you already have. Use `pgrep -x <name>` or
|
||||||
|
`ps -o pid=,stat= -C <name>`.
|
||||||
|
* **A file extension is not a format.** `sound.pak` is full of `.slb` banks and
|
||||||
|
names a `.xgs` project, and neither is an XACT container: the whole 1.08 GB
|
||||||
|
archive contains zero `XGSF`, `SDBK` or `WBND` magic, and the executable has no
|
||||||
|
XACT string at all. The names came from the authoring tool and survived into the
|
||||||
|
shipped data. Check for the magic before committing to a parser for the format
|
||||||
|
the extension advertises.
|
||||||
|
* **A log-polling probe must compare a COUNT, not test for presence.** Timing an
|
||||||
|
operation by waiting for its "done" line in a log gives an instant false
|
||||||
|
positive if any earlier run left one there. This produced a confident
|
||||||
|
"39.5 fps" that was really 0.1 s of matching a stale line — and it disagreed
|
||||||
|
with the true 28.5 fps by enough to have flipped a conclusion. Snapshot
|
||||||
|
`log.count(marker)` before starting and wait for it to *increase*.
|
||||||
|
* **This container kills long emulator runs.** Twice in one session the
|
||||||
|
`xenia_canary` process vanished with **no crash line in its own log** — once
|
||||||
|
~50 s into a boot, once ~145 s in at 1.15 GB RSS — while the box sat at
|
||||||
|
~1 GB free with swap exhausted. Budget dynamic experiments to fit in ~2 minutes
|
||||||
|
of guest time, take the measurement early, and never read a disappearance as a
|
||||||
|
guest crash without checking the log tail for one.
|
||||||
|
* **When a frame-match will not resolve, sample denser before doubting the
|
||||||
|
hypothesis.** Identifying a movie from 5-second screenshots gave contrast-23
|
||||||
|
frames, a playhead that would not join up, and a runner-up winning one frame —
|
||||||
|
it looked like weak evidence for the wrong film. The same question sampled every
|
||||||
|
0.5 s (`ffmpeg x11grab` at 10 fps) answered at **0.999 with a strictly monotone
|
||||||
|
playhead**. Sparse sampling does not weaken a signal gracefully; it turns it
|
||||||
|
into noise that looks like a different answer.
|
||||||
|
* **Check what instrumentation the local build already has before declaring a
|
||||||
|
question blocked on tooling.** Two Q8 residuals were written off as needing "an
|
||||||
|
emulator whose audio path can be observed". The emulator in this container
|
||||||
|
already had `xma_param_probe` — visible in the startup CONFIG DUMP the whole
|
||||||
|
time — added by this project for exactly that purpose. Read the cvar dump, or
|
||||||
|
`nm -C <binary> | grep cvars::`, before concluding you cannot measure something.
|
||||||
|
* **"Strings a function references" is not "arguments a function passes".** A
|
||||||
|
loose filter over the strings in a caller produced a tidy-looking screen
|
||||||
|
vocabulary; recovering the actual register argument at each call site removed
|
||||||
|
half of it, including two names that appeared to be corroborated by independent
|
||||||
|
measurement. If the claim is about an argument, simulate the registers — the
|
||||||
|
cheap proxy manufactures agreement.
|
||||||
|
* **A backward scan for `addi rN, …` misses arguments staged through another
|
||||||
|
register.** PPC code often builds a pointer in a scratch register and moves it
|
||||||
|
into place later (`addi r27, r11, X` … `mr r5, r27`). A backward search for the
|
||||||
|
destination register alone found 7 of 48 sites and missed the two known-good
|
||||||
|
ones. Simulate forward over a window instead, tracking `lis`/`addi`/`mr`.
|
||||||
|
* **Check a measurement against the WHOLE reference set, not the part you were
|
||||||
|
reading.** Two observed BGM stream sizes were recorded as matching "no bank's
|
||||||
|
declared waves", which spawned a wrong theory that the decoder gets a window
|
||||||
|
rather than a whole wave. They match `BGM_103` exactly — the check had covered
|
||||||
|
only the `BGM_0xx` rows of the census, because that is the block that had been
|
||||||
|
on screen.
|
||||||
|
* **`instructions.function` is unpopulated for most rows in `sylpheed.db`.** A
|
||||||
|
query keyed on it returns *nothing* rather than erroring — a `bctr` search
|
||||||
|
scoped that way reported "no jump tables" for a function with two known ones.
|
||||||
|
Scope by `address between <start> and <end_address>` instead, and gate any such
|
||||||
|
query on a function whose answer you already know.
|
||||||
|
* **Before testing "state X behaves like this", check that your route actually
|
||||||
|
reaches state X.** A test of "does Ⓐ work in phase 4 state 0" was run by
|
||||||
|
pressing Ⓑ, on the strength of a decoded `4 → state 0` edge — but the same basic
|
||||||
|
block also writes the *phase* field, sending it to phase 2. The test probed the
|
||||||
|
wrong state and produced a confident refutation of a live hypothesis. When a
|
||||||
|
transition sets more than one field, read the whole block, not the one store you
|
||||||
|
were looking for.
|
||||||
|
* **A default filter can hide a whole screen, and the corpus will record it as
|
||||||
|
absent.** `screen list`/`render` filter on `is_build`, which needs a `.rat`
|
||||||
|
child; the developer splash has none, so it never appeared — and a page ended up
|
||||||
|
stating it "cannot be rendered at all". It renders fine under `--all`. When two
|
||||||
|
pages disagree about whether something exists, run the tool both ways before
|
||||||
|
believing either.
|
||||||
|
* **A logo that appears twice may be two different things.** The boot shows
|
||||||
|
`SQUARE ENIX` at ~2 s and again at ~10 s; the first is the splash screen, the
|
||||||
|
second is the intro movie's own opening frame. Reading the capture alone gives
|
||||||
|
"three logo screens". Cross-check a repeat against what is *supposed* to be
|
||||||
|
playing at that moment before counting it as its own screen.
|
||||||
|
* **Do not hard-code an expected element count into a structure scan.** Looking
|
||||||
|
for a `.rat` record's keyframes, I required 29 strictly-increasing times because
|
||||||
|
a header word read `30`. The records hold **three** keyframes in exactly the
|
||||||
|
layout I was testing for, and the over-constrained filter reported the layout as
|
||||||
|
refuted. Scan for the *shape* (here: `0x??ffffff` fade words at a constant
|
||||||
|
stride) and let the count fall out.
|
||||||
|
* **Read the target text before writing a patch pattern for it.** Two iterations
|
||||||
|
running, a scripted edit to `HANDOFF.md` failed its assertion because the
|
||||||
|
paragraph had been rewrapped since the pattern was written — and both times the
|
||||||
|
finding landed in `docs/re/` while the page the port agent reads kept the stale
|
||||||
|
version. The commit's file count is the tell: if it is one short, an edit was
|
||||||
|
silently dropped.
|
||||||
|
* **Two renderers agreeing is not evidence the field is right.** The port's
|
||||||
|
composite and `sylpheed-cli screen render` matched on `main_menu` to 3/255 —
|
||||||
|
and *both* omitted two elements the game draws, because both read one field
|
||||||
|
through one decoder. Agreement between consumers of the same decode measures
|
||||||
|
the decode's determinism, not its truth. Only a framebuffer capture is an
|
||||||
|
independent check. (Reported by the port agent, 2026-08-28, after a capture
|
||||||
|
crop found the missing bracket.)
|
||||||
|
* **`./target/debug/<bin>` is STALE in this container — `CARGO_TARGET_DIR` is
|
||||||
|
redirected.** The Dockerfile sets `CARGO_TARGET_DIR=/sylph-home/re/target-container`
|
||||||
|
so a fresh `cargo build` writes there, while `./target/debug/sylpheed-cli`
|
||||||
|
remains whatever was built on the host, hours old. A decoder fix was verified
|
||||||
|
against the stale binary and produced byte-identical "before" and "after"
|
||||||
|
renders — which reads exactly like "the change had no effect" rather than "you
|
||||||
|
ran the old code". Run the binary cargo actually wrote, or `cargo run`.
|
||||||
|
* **A pivot-equality test conflates rounding with real breakage.** Checking
|
||||||
|
`pivot == texture/2` over `GP_TITLE` flagged 74 % as mismatched, which reads as
|
||||||
|
a systemic decode failure. Split by magnitude it is 17 exact, 54 off by ≤ 1 px
|
||||||
|
(odd dimensions rounding), and only **24 off by > 8 px** — and those 24 are the
|
||||||
|
real finding, concentrated on one element family. Bucket a mismatch by size
|
||||||
|
before reporting a rate.
|
||||||
|
* **I have now broken the "read the target text first" rule three times.** Each
|
||||||
|
time a scripted `HANDOFF.md` edit asserted on a paragraph that had been rewrapped
|
||||||
|
by an earlier edit in the same session, and each time the finding landed in
|
||||||
|
`docs/re/` while the port's page kept the stale text. Writing the rule down was
|
||||||
|
not enough; the working fix is mechanical — `grep`/`sed -n` the target paragraph
|
||||||
|
in the same command that patches it, and check the commit's file count.
|
||||||
|
* **"The nearest header after the name" is not a name→header mapping.** Looking
|
||||||
|
for each sprite's `T8aD` header by searching for its name and taking the next
|
||||||
|
`T8aD` returned *the same header for every sprite* — all the names live together
|
||||||
|
in the declaration table near the top of the bundle, so "next" is always the
|
||||||
|
first one in the file. It failed its own control instantly: different sprites
|
||||||
|
reported identical dimensions. Match on something the header itself carries
|
||||||
|
(here width × height against the decoded PNGs) rather than on proximity.
|
||||||
|
* **`cargo build` passing does not mean `cargo test` compiles.** Adding a field to
|
||||||
|
`T8adImage` built the library fine in 1.48 s — and broke two *test-only* struct
|
||||||
|
literals, so `cargo test` failed to compile with exit 101. A green build on the
|
||||||
|
binary is not a gate for a struct change; run the tests before believing a
|
||||||
|
data-structure edit is safe.
|
||||||
|
* **Detach long background work with `setsid`, or a tool-call timeout kills it.**
|
||||||
|
Three attempts to run the disc-gated test suite in the background died at exit
|
||||||
|
143 with an empty log — the timeout's SIGTERM reached the whole process group.
|
||||||
|
`setsid nohup cargo test … &` survives, and the run then completes across later
|
||||||
|
tool calls. An empty log from a "background" job usually means it was killed,
|
||||||
|
not that it produced nothing.
|
||||||
|
* **Validate a recommendation before leaving it as advice.** Having concluded a
|
||||||
|
question was undecodable from the disc, I named a per-draw GPU capture as the
|
||||||
|
next route "because it reads the blend state". It does not — checking the
|
||||||
|
capture's own source showed it records textures and vertex attributes and no
|
||||||
|
blend registers. A named next step is a claim like any other.
|
||||||
|
* **Name the element from the draw's own coordinates before building on it.** A
|
||||||
|
skewed draw in a title capture was called "the swoosh" because the swoosh is the
|
||||||
|
screen's only diagonal element — an identification by elimination, never checked
|
||||||
|
against the draw's NDC extent. It was wrong: the quads span the full screen and
|
||||||
|
beyond, the swoosh is a 234-pixel band. Five iterations of analysis pointed at
|
||||||
|
the wrong element on the strength of it. Converting the coordinates takes one
|
||||||
|
line and should come first.
|
||||||
|
* **A census over a bundle's top-level table is not a census over the bundle.**
|
||||||
|
The keyframe rotation field read "always zero on this screen" for several
|
||||||
|
iterations because every scan walked `GP_TITLE` build 4's **declaration
|
||||||
|
table**, and the rotated elements are **nested leaf records** reached through
|
||||||
|
an `opt ` link. The bytes were right; the *reach* was wrong, and a negative
|
||||||
|
stated without its reach reads like a fact about the disc. Say which region a
|
||||||
|
negative covers, and check whether the thing you are looking for lives outside
|
||||||
|
it.
|
||||||
|
* **Do not assume 4-byte alignment when scanning raw bundle bytes.** A nested
|
||||||
|
`RATC` blob starts wherever the parent's chunk stream leaves it — `ptloop01.rat`
|
||||||
|
sits at `0xbb5966` — so its 40-byte keyframe blocks are odd-aligned. A scanner
|
||||||
|
that filtered candidates on `%4 == 0` found **0/3** of its own control blocks
|
||||||
|
and under-counted the corpus by **16 341** blocks, all of them nested. It cost
|
||||||
|
nothing to catch, because the control was in the script.
|
||||||
|
* **`pkill -f <pattern>` matches the shell running it.** `pkill -f kfscan.py`
|
||||||
|
from a `bash -c` whose command line contains `kfscan.py` kills its own shell:
|
||||||
|
the tool call returns exit 143/144 and the edit that was queued behind it never
|
||||||
|
runs. The same trap makes `pgrep -f` self-report — a "still running? yes" that
|
||||||
|
was the shell seeing itself, on a job that had already finished. Use
|
||||||
|
`pgrep -x`, or match on a path the current command line does not contain.
|
||||||
|
(This is the second `ps`/`pgrep` entry in this file; the first is about
|
||||||
|
`ps -ef` dumping the loop prompt.)
|
||||||
|
* **Grep a legend and you count the legend.** A sweep for elements with a
|
||||||
|
rotation reported "1 element" in every build on the disc — the header line
|
||||||
|
`a=alpha r=rot°` matched the ` r=` pattern. A uniform count across
|
||||||
|
heterogeneous inputs is the tell. Make the pattern require the *value*
|
||||||
|
(` r=-?[0-9]+`), and sanity-check that a known-negative build reports zero.
|
||||||
|
* **A pattern over the archives you happened to check is not a negative.** I
|
||||||
|
wrote "rotation appears to live only in nested leaf records", correctly scoped
|
||||||
|
to the three paks I had swept — and the sweep refuted it on the fourth, within
|
||||||
|
the hour, because the alphabet had not reached `GP_DIALOG` yet. Scoping the
|
||||||
|
claim was what made it cheap to withdraw, but the better move is to let the
|
||||||
|
sweep finish before writing the sentence at all: a negative that is still
|
||||||
|
running is not a finding, it is a prediction.
|
||||||
|
* **A field that is implemented is not a field that is measured.** The
|
||||||
|
pivot-anchored scale term had been in `blit` for months and reasoned about in
|
||||||
|
the handoff, and no capture had ever exercised it, because every element anyone
|
||||||
|
had looked at was at 100 % scale where the term is identically zero. Look for
|
||||||
|
the case that makes a term *large* — here 600 % and 800 % scale, worth 450 px —
|
||||||
|
and check it there. A term you cannot distinguish from zero has not been
|
||||||
|
verified by any amount of agreement.
|
||||||
|
* **Printing a series' minimum and reading it as its range.** I summarised a
|
||||||
|
captured alpha series as "constant α ≈ 33" and built a contradiction on it —
|
||||||
|
the summary printed `min_alpha` and no maximum, and the series actually ramps
|
||||||
|
34 → 255 → 33. The tell was there in the same table: the column beside it said
|
||||||
|
*14 distinct colours*, which a constant series cannot have. When a summary
|
||||||
|
statistic and a distinct-value count disagree, the summary is wrong.
|
||||||
|
* **Calibrate on one element, test on another.** Fitting a declared ramp to a
|
||||||
|
capture has two free parameters (rate and offset) and will "succeed" against
|
||||||
|
almost anything — my first attempt scored RMS 128/255 and I nearly read the
|
||||||
|
numbers rather than noticing the search could not reach the ramp at all. The
|
||||||
|
version that means something: fix the clock from element A's ramp, check that
|
||||||
|
fix against A's own next landmark, then apply it to element B in the same
|
||||||
|
frames with **nothing left to tune**. That is what turned "the shapes look
|
||||||
|
different" into "still at 255 nine frames after it should read 32".
|
||||||
|
* **Two measurements can both be right and still disagree, when a heuristic sits
|
||||||
|
between them.** A change to the keyframe time association is favoured 26× by an
|
||||||
|
animation measurement and rejected by a static-render comparison. Both are
|
||||||
|
sound: `rest()` picks a pose by *longest dwell*, which is a heuristic layered on
|
||||||
|
top of the times, so moving the times moves its tie-breaks. The mistake would
|
||||||
|
be to treat one as refuting the other and drop the loser. Name what each
|
||||||
|
measurement actually constrains — here *timing* versus *pose selection* — before
|
||||||
|
deciding they conflict.
|
||||||
|
* **Before believing a render diff refutes a decode, find out which element moved.**
|
||||||
|
I reported a 13 % render change as evidence against a keyframe-time hypothesis,
|
||||||
|
on a brightness argument about language twins. One element accounted for all of
|
||||||
|
it, and that element is a transient flash with **no resting pose** — so the
|
||||||
|
difference was our own `rest()` heuristic guessing, and it would have guessed
|
||||||
|
under either reading. A whole-image statistic (pixels changed, mean luminance,
|
||||||
|
correlation) tells you *that* something moved, never *what*, and an argument
|
||||||
|
built on it can be entirely about your own code. Localise the diff to an
|
||||||
|
element and read its keyframes first.
|
||||||
|
* **A fallback that only runs when its precondition fails is not a fallback.**
|
||||||
|
`rest()` picks the longest-dwell keyframe when no plateau exists — but a dwell
|
||||||
|
gap is time spent *moving between* two poses, and the only case where an
|
||||||
|
endpoint is genuinely held is when the two poses are equal, which is exactly
|
||||||
|
the plateau the first path already caught. So the fallback is guaranteed to be
|
||||||
|
guessing every time it is reached. When a heuristic's justification is the same
|
||||||
|
condition that routes around it, it has no justification.
|
||||||
|
* **Siblings adjudicate a rule that a single element cannot.** Deciding what
|
||||||
|
`rest()` should return for an element with no held pose looked like it needed a
|
||||||
|
capture we do not have. It did not: the developer splash carries three glows
|
||||||
|
with identical structure and identical times, differing in one alpha byte, and
|
||||||
|
any rule that makes one of them behave completely differently from the other
|
||||||
|
two is producing an artefact. Parallel constructions in the same bundle are a
|
||||||
|
free control — look for them before concluding a question needs new measurement.
|
||||||
|
* **Backgrounding with `&` in a compound command loses the working directory.**
|
||||||
|
`cd X && cmd &` followed by more work in the same call left the shell reset, and
|
||||||
|
a heredoc two lines later wrote nowhere — while the `echo` after it still
|
||||||
|
printed "written". A success message after a failed redirect is not a success.
|
||||||
|
Use `(setsid nohup … &)` and keep the file-writing in its own call.
|
||||||
|
* **Read the whole function before describing what it does.** I wrote "a
|
||||||
|
zero-scale pose paints a 1-pixel sliver" into a tool's docstring, from the
|
||||||
|
`.max(1)` in the size arithmetic. Two lines above it, a guard coerced
|
||||||
|
`scale == 0` to **100 %**, so the `.max(1)` never saw a zero and the element
|
||||||
|
rendered at *full size* — a much larger bug than the one I described. I had
|
||||||
|
read the lines I went looking for and stopped.
|
||||||
|
* **Never diff a file a background job might still be writing.** A comparison
|
||||||
|
loop over an output directory reported `GP_TITLE` build 6 as changed; the pixel
|
||||||
|
diff was then all zeros, because `cmp` had read a half-written PNG. Compare
|
||||||
|
only after the job signals done, or skip the newest file. A "difference" that
|
||||||
|
vanishes on re-examination is usually a race, not a flake.
|
||||||
|
* **A `screen list` BUILD index is not a pak ENTRY index.** `GP_TITLE` happens to
|
||||||
|
map 1:1 (16 builds, entries 0–15), which is how the assumption survived;
|
||||||
|
`GP_DIALOG` does not — its build 0 is **entry 2**. Indexing a pak directly with
|
||||||
|
a build number silently reads a different bundle and reports a confident wrong
|
||||||
|
answer: a census control asked for the two rotated elements of `GP_DIALOG`
|
||||||
|
build 0 and got zero, from a bundle that genuinely has none. `screen list`
|
||||||
|
prints the mapping; use it rather than assuming identity.
|
||||||
|
* **"There is no flag" is not "there is no way".** I declared a Japanese-locale
|
||||||
|
capture impossible in this container because `user_language` is `DECLARE`d and
|
||||||
|
never `DEFINE`d, so no cvar exists to pass. That was true and not the question.
|
||||||
|
The language is *persisted*: canary builds `XConfig` over
|
||||||
|
`<storage_root>/xconfig.settings`, `SetDefaults()` only supplies the value when
|
||||||
|
the file has none, and the file is writable. One grep further — for where the
|
||||||
|
setting is **stored** rather than where it is **configured** — turned a
|
||||||
|
"blocked, needs a human decision" into a two-line edit. When you conclude a
|
||||||
|
capability is absent, check the persistence layer before writing it up.
|
||||||
|
* **Locate a binary field from struct landmarks, not from a hard-coded offset.**
|
||||||
|
`user.language` was found by scanning for `music_volume`'s `0.7f` (a known
|
||||||
|
default at a known relative offset), then cross-checking `language == 1` and
|
||||||
|
`country == 103` against the emulator's own `SetDefaults()`. Three agreeing
|
||||||
|
landmarks, no offset assumed, and the check re-runs every invocation — so the
|
||||||
|
tool fails loudly if the layout moves instead of silently patching a
|
||||||
|
neighbouring field.
|
||||||
|
* **A stale oracle reports the world, not itself.** A 787 s run concluded "the
|
||||||
|
game never reached the title". It had, repeatedly: `wait_title.sh` was still
|
||||||
|
sampling the single pixel (625,618) that `is_title.py` had already been written
|
||||||
|
to replace — a 1280×720 coordinate against a 1279×675 game surface, always
|
||||||
|
reading the copyright line. The replacement existed *in the same directory*,
|
||||||
|
with a docstring naming the bug. Before believing a negative from a probe,
|
||||||
|
check whether something in the toolkit already supersedes it, and run the probe
|
||||||
|
against a known-positive capture first — `is_title.py` scores 753 on a real
|
||||||
|
title frame and 327 on the main menu, which takes one command to confirm.
|
||||||
|
* **Run the control before theorising about the difference.** A Japanese-locale
|
||||||
|
run failed to reach the interactive title, and I wrote up the open question as
|
||||||
|
"if English reaches it, the difference is the locale". English does not reach
|
||||||
|
it either — 75 samples over 734 s, every one a miss. The locale had nothing to
|
||||||
|
do with it, and two iterations framed the problem around a variable that was
|
||||||
|
never implicated. The control cost one run and would have cost the same one
|
||||||
|
iteration earlier.
|
||||||
|
* **Log every sample, not just the hit.** The first probe printed nothing until
|
||||||
|
it succeeded, so a 787 s failure was indistinguishable from a probe that never
|
||||||
|
ran. Printing the oracle's value each tick turns "no title" into "75 samples,
|
||||||
|
max glyph 0", which is a measurement — and it is what showed the black-screen
|
||||||
|
and attract-movie phases were being sampled correctly all along.
|
||||||
|
* **Know when to stop paying for a non-blocking answer.** A capture that would
|
||||||
|
settle two 🟡 residuals — neither of which blocks the deliverable — absorbed
|
||||||
|
five iterations. Each one produced a real finding (a broken oracle, a wrong
|
||||||
|
"impossible", a locale red herring), so none was wasted, but the *question*
|
||||||
|
never moved. The signal to stop is not "this is hard", it is "the thing this
|
||||||
|
unblocks is not blocking anything". Write down the recipe you built and leave
|
||||||
|
it for a session with a reason to spend the time.
|
||||||
|
* **Audit what a renderer silently SKIPS, not only what it draws.** `compose` has
|
||||||
|
half a dozen `continue` paths, and a sprite-name miss is one of them — an
|
||||||
|
element vanishes with no error, which is exactly how the port agent's missing
|
||||||
|
bracket happened. `screen render` already prints its omissions; reading that
|
||||||
|
list for the five screens that matter took one command and turned up a
|
||||||
|
full-screen opaque element dropped on three of them. Any composer with skip
|
||||||
|
conditions should be asked to name what it skipped before it is trusted.
|
||||||
|
* **An element's declared name is not necessarily its texture's name.** The
|
||||||
|
`opt ` link on `pteff05.t32` leads to a `T8aD` registered as `8AX`, so a
|
||||||
|
lookup by declared name misses. Before concluding a texture is absent, look at
|
||||||
|
the bytes the link actually points at — the name in the declaration table and
|
||||||
|
the name the sprite table keys on can differ.
|
||||||
|
* **A fit whose residual is large for every model is a broken method, not a
|
||||||
|
close call.** Comparing our composite to a capture pixel-wise gave mean abs
|
||||||
|
errors of 10–14 for gamma, for a linear scale, for everything — and a
|
||||||
|
non-monotonic transfer curve (render 96–127 mapping *brighter* than render
|
||||||
|
128–159). The cause was edge misalignment: at correlation 0.947 a bright pixel
|
||||||
|
in one image routinely lands on a dark one in the other. Restricting to patches
|
||||||
|
that are flat in **both** images dropped the residual to 0.2–1.1. When every
|
||||||
|
candidate model fits badly, stop choosing between them and look at what the
|
||||||
|
comparison is actually measuring.
|
||||||
|
* **Say when a control failed to discriminate, rather than reporting it as a
|
||||||
|
pass.** The held-out screen for a tone-curve fit was the developer splash,
|
||||||
|
whose flat regions are pure black — every model scored ≈ 0.00 error there. That
|
||||||
|
is not corroboration; it is a test with no power, and reporting the 0.00 as
|
||||||
|
agreement would have dressed an untested claim as a verified one.
|
||||||
|
* **Time your probe against the thing you are probing for.** Four runs concluded
|
||||||
|
"the game never reaches the title". `screenshot` costs **10.8 s while xenia is
|
||||||
|
running** and **0.117 s once it is killed** — 92× — so a two-grab polling loop
|
||||||
|
samples every ~41 s, against a title screen this corpus documents as lasting a
|
||||||
|
few seconds. The harness was blinking slower than the event. Before believing a
|
||||||
|
negative from a polling loop, measure its interval and compare it to the
|
||||||
|
duration of what you are waiting for; and measure the probe's cost *under the
|
||||||
|
same load as the run*, because idle timing here was off by two orders of
|
||||||
|
magnitude.
|
||||||
|
* **Do not change a display setting and a capture behaviour in the same run.**
|
||||||
|
`kernel_display_gamma_type = 0` brightens the frame, and `skip_intro.sh`
|
||||||
|
classifies movie-vs-static on an *absolute* rmse threshold — so the gamma
|
||||||
|
change biased the very classifier the run depended on. Harness thresholds tuned
|
||||||
|
on one output configuration are not portable to another.
|
||||||
|
* **Fixing the instrument is how you test the explanation that blamed it.** I
|
||||||
|
diagnosed four failed runs as "the probe samples slower than the event",
|
||||||
|
wrote it up, and withdrew three earlier conclusions on that basis. Building the
|
||||||
|
faster probe refuted it: at 100× the sampling density, over 420 unbroken
|
||||||
|
seconds, the event still never occurred. The diagnosis was a real defect that
|
||||||
|
happened not to be the cause — the most seductive kind, because fixing it feels
|
||||||
|
like confirmation. A plausible mechanism that explains a failure is a
|
||||||
|
hypothesis, and the fix is its experiment, not its proof.
|
||||||
|
* **Ask whether the screen is drawing anything before explaining why a feature of
|
||||||
|
it is missing.** Four iterations produced four explanations for "the title is
|
||||||
|
not detected" — a stale oracle, the locale, the sampling rate, a one-shot boot
|
||||||
|
window — and three of them were refuted in turn. Splitting a raw root grab into
|
||||||
|
bands then showed the game surface at **0.08 % non-black, mean 0.07**: it was
|
||||||
|
rendering black. The check costs one command and belongs at the *front* of that
|
||||||
|
sequence, not after the fourth hypothesis. A detector reporting absence is
|
||||||
|
ambiguous between "the feature is not there" and "nothing is there".
|
||||||
|
* **A newly found fault does not retroactively explain older failures.** The
|
||||||
|
black surface is real and reproducible *in the run where it was measured*.
|
||||||
|
Earlier runs in the same session had measurable content (mean 33.1, mean 122.8,
|
||||||
|
non-zero frame-to-frame rmse), so the failure mode changed over the session.
|
||||||
|
Reaching back to re-explain earlier results with the newest cause would have
|
||||||
|
been the fourth over-reach in a row.
|
||||||
|
* **Validating a probe on static test images tests its ANALYSIS, not its
|
||||||
|
ACQUISITION.** The fast probe's counter was controlled against committed
|
||||||
|
captures and matched exactly (753 / 327), which proved the numpy expression
|
||||||
|
right and the frame source untested. The source was the broken half: a
|
||||||
|
long-lived x11grab stream degrades from 3.98 to 1.60 fps and then freezes,
|
||||||
|
repeating one stale frame. The check that catches it is to read the same screen
|
||||||
|
with an **independent grabber at the same moment** — `import` said 125.65 where
|
||||||
|
the stream said 5.21. Do that during a run, not only at design time.
|
||||||
|
* **A dense negative from one instrument is worth less than one cross-check.**
|
||||||
|
"2 391 frames, zero hits" reads as overwhelming and collapses to nothing once
|
||||||
|
the frames may all be the same frame. Sample count is not evidence of coverage
|
||||||
|
unless the samples are known to be independent.
|
||||||
|
* **A probe that cross-checks itself turns "no result" into a result.** Three
|
||||||
|
successive "the title never appears" claims were withdrawn because the
|
||||||
|
instrument was broken each time. The version that finally held prints its own
|
||||||
|
reading beside an independent grabber's every 60 s — 9 checks, 8 agreeing to
|
||||||
|
within 2 % — so the negative arrives with its own evidence that the frames were
|
||||||
|
live. Building the cross-check into the tool costs a few lines and is what
|
||||||
|
separates a measurement from a silence.
|
||||||
|
* **A hedge in a code comment is an unmeasured claim.** `compose` said the
|
||||||
|
derived paint order "reproduces both measured orders up to ties" — a sentence
|
||||||
|
that sounds like a result and was neither measured nor kept current: there were
|
||||||
|
three measured orders by then, not two. Measuring it took one example program
|
||||||
|
and turned a hedge into a bounded number (exact on 4 of 5 bundles; the 5th off
|
||||||
|
by 8 pairs, all ties). Grep your own comments for "up to", "roughly",
|
||||||
|
"essentially" — each one is a claim nobody has checked.
|
||||||
|
* **Count the cases that can actually bite, not the cases that match the
|
||||||
|
pattern.** `EXTRAS` has 15 tied pairs in its derived paint order, which reads
|
||||||
|
alarming. Only **2** of them overlap, and a tie between non-overlapping
|
||||||
|
elements paints identically either way. Reporting 15 would have overstated the
|
||||||
|
risk by 7×; the useful number is the one filtered by whether the difference can
|
||||||
|
reach a pixel.
|
||||||
|
* **An exhaustive field search needs a positive control, or "found nothing" is
|
||||||
|
worthless.** Scanning a header for a field that reproduces a measured ordering
|
||||||
|
returned zero hits — which could equally mean the field is absent or the scan
|
||||||
|
is broken. Running the same scan against a *known* ordering (declaration order)
|
||||||
|
returned **64** hits, proving the scan finds ordering fields when they exist.
|
||||||
|
Only then is the zero a finding. The control costs four extra lines and turns a
|
||||||
|
silence into a negative with reach.
|
||||||
|
* **A stale row on the handoff page is worse than a missing one.** The port was
|
||||||
|
told SE audio was "undecodable from the disc" while the linked doc had already
|
||||||
|
retracted that and located three cues decoding to PCM. The finding landed in
|
||||||
|
`docs/re/` and the page the port reads kept the superseded text — the fourth
|
||||||
|
time in this corpus. Writing the rule down has not worked, so there is now a
|
||||||
|
tool: `tools/re-capture/handoff_lint.py` flags every HANDOFF line that makes a
|
||||||
|
strong negative claim and links a doc containing retraction language. It found
|
||||||
|
this row, and one benign false positive, on its first run.
|
||||||
|
* **Narrow a risk before reporting it, then narrow it again.** "15 tied pairs on
|
||||||
|
`EXTRAS`" became 2 (only overlapping ties can paint differently), then 1 (one
|
||||||
|
of those two is a `loop*` animation the compositor skips), then "consistent
|
||||||
|
with the capture". Each step is a filter that costs one query and changes the
|
||||||
|
number by more than an order of magnitude in total. A raw count is almost never
|
||||||
|
the number a consumer needs.
|
||||||
|
* **Nothing was checking that the docs' cited evidence exists.** A sweep of every
|
||||||
|
relative link under `docs/` found **16 broken**, and two of them were the
|
||||||
|
figures backing the UI layout decode's headline claim — the port's foundation,
|
||||||
|
unreachable from its own page, because a path in `structures/` was written one
|
||||||
|
directory too shallow. Eleven were wrong relative depth with the target present;
|
||||||
|
five name files that do not exist. Evidence that cannot be opened is not
|
||||||
|
evidence, and a link is exactly the kind of thing no one re-reads.
|
||||||
|
`tools/re-capture/doc_link_check.py` now does it, and also flags targets that
|
||||||
|
resolve to a **zero-byte** file — which looks correct in every listing.
|
||||||
|
* **Repair in bulk only when the counts pair.** The fixer rewrote 11 links; the
|
||||||
|
checker went from 1 038 resolving / 16 missing to 1 049 / 5. +11 and −11
|
||||||
|
against 11 edits is the confirmation that the pass did what it said and touched
|
||||||
|
nothing else. A bulk edit without that arithmetic is a hope.
|
||||||
|
* **When two candidates carry the same content, compare their DIFFERENCE against
|
||||||
|
the oracle, not themselves.** `8AX` (1280×720) and `ptbase` (640×360 at 200 %)
|
||||||
|
are the same artwork, so no comparison of either against a capture can separate
|
||||||
|
them — every such test had been read as "inconclusive, needs a per-draw
|
||||||
|
capture". What separates them is `8AX − upscale(ptbase)`: the detail only one
|
||||||
|
of them has. Correlating the capture's residual against *that* answered it
|
||||||
|
statically, on two screens, with matched controls.
|
||||||
|
* **A ratio that saturates at 1 under noise is not a discriminator.** A pixel-pair
|
||||||
|
test cleanly separated upscales (0.00–0.72) from native (0.98) and put the
|
||||||
|
capture at 1.01 — apparently decisive. Additive noise raises both terms of the
|
||||||
|
ratio equally and drives *any* value toward 1, and fitting a noise term showed
|
||||||
|
both hypotheses reproduce the observed numbers. Before believing a ratio, ask
|
||||||
|
what it does as noise grows.
|
||||||
|
* **Read what a cvar DOES before building an experiment around it.** I planned a
|
||||||
|
run to decide whether a measured gamma was "canary's BT.709 output stage",
|
||||||
|
by setting `kernel_display_gamma_type = 0` and re-fitting. The cvar is not an
|
||||||
|
output stage: it is the value a `kStub` **getter returns to the guest**, which
|
||||||
|
the game uses to build its own ramp, and canary applies *that* ramp in the swap
|
||||||
|
path. The experiment could never have isolated a stage that does not exist —
|
||||||
|
and it would have perturbed the capture harness as a side effect. One grep for
|
||||||
|
the cvar's definition replaced a planned emulator run with a better one.
|
||||||
|
* **An absence in a log is only evidence if the log would have shown it.** No
|
||||||
|
gamma lines appear in any run log here — but kernel exports log at Debug and
|
||||||
|
this harness masks Kernel logging entirely (`log_mask = 13`), so their absence
|
||||||
|
is guaranteed regardless of what the game did. Check the logging configuration
|
||||||
|
before reading silence as a result.
|
||||||
|
* **Check what an experiment actually needs before filing it behind a blocker.**
|
||||||
|
The gamma question sat parked behind "needs the emulator to reach a menu" for
|
||||||
|
several iterations. It needed the emulator only to **boot** — video init, and
|
||||||
|
the `VdGetCurrentDisplayGamma` call, happen in the first seconds, long before
|
||||||
|
any title screen. A blocker that stops one experiment does not stop every
|
||||||
|
experiment in the same area, and it is worth re-reading the parked list against
|
||||||
|
what each item really requires rather than against the area it belongs to.
|
||||||
|
* **A default value is evidence.** Whether the game writes a gamma ramp looked
|
||||||
|
like it needed a GPU trace. It mostly did not: canary initialises the ramp
|
||||||
|
table to **identity** and applies it through a shader that is a pure LUT lookup
|
||||||
|
with no other transfer. An unwritten ramp is therefore a no-op, and any
|
||||||
|
non-identity transfer in the output implies a write. Reading what a field holds
|
||||||
|
when nobody has touched it turns "I cannot observe the write" into "the write
|
||||||
|
must have happened" — cheaper than instrumenting, though it stays an inference
|
||||||
|
and should be labelled one.
|
||||||
|
* **Name the weak joint of an inference in the same breath as the conclusion.**
|
||||||
|
The chain above assumes our composite reproduces the pre-ramp framebuffer,
|
||||||
|
which is the one step that could be wrong. Writing that down beside the
|
||||||
|
conclusion — rather than only the supporting facts — is what lets a later
|
||||||
|
reader attack it instead of inheriting it.
|
||||||
|
* **The startup config dump is the config FILE, not the effective flags.** A run
|
||||||
|
passed `--log_mask=12 --log_level=3` and its dump printed `log_mask = 0,
|
||||||
|
log_level = 2` — while Kernel Debug logging was demonstrably on, which is how
|
||||||
|
that run's finding was obtained. So a dump can neither confirm nor refute a
|
||||||
|
command-line override, and reading one as "my flag was ignored" is a mistake I
|
||||||
|
nearly made with `trace_gpu_stream`. Verify a flag by its *effect*, not by the
|
||||||
|
dump. (This does not undo the earlier `user_language` conclusion: absence of a
|
||||||
|
*name* from the dump still shows the cvar is unregistered.)
|
||||||
|
* **`kill -9` destroys anything that finalises on shutdown.** Canary starts a GPU
|
||||||
|
trace at init but only closes it in `GraphicsSystem::Shutdown()`, so the hard
|
||||||
|
kills this session used routinely could never have produced a trace. Before
|
||||||
|
concluding a feature is broken, check whether the way you stop the program is
|
||||||
|
what discards its output.
|
||||||
|
* **Bound a risky experiment from inside, not by watching it.** A boot-time GPU
|
||||||
|
trace on a disk at 95 % could have filled it between two tool calls. The runner
|
||||||
|
carried its own watchdog that killed the emulator the moment the output passed
|
||||||
|
a 2 GiB cap, so the experiment was safe regardless of how coarsely I polled.
|
||||||
|
The watchdog never fired, which is the point — it cost nothing and removed the
|
||||||
|
need to gamble on timing.
|
||||||
|
* **A cvar existing does not mean the feature is compiled in.** `trace_gpu_stream`
|
||||||
|
parses, is documented, sets state, and does nothing: the code that opens the
|
||||||
|
trace sits behind `#if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1`, which
|
||||||
|
`trace_writer.h` ties to `#ifdef NDEBUG` — off in release. Two runs and two
|
||||||
|
wrong hypotheses before reading the `#if`. When a switch produces no effect,
|
||||||
|
follow the code from the switch to the output and look for a compile-time gate
|
||||||
|
before theorising about runtime causes.
|
||||||
|
* **Test a compile-time gate against the binary, with a control.** A string that
|
||||||
|
exists only inside the guarded block (`_stream.xtr`) settles it in one command:
|
||||||
|
0 occurrences in the release binary, **1** in the debug binary. The debug build
|
||||||
|
is the control that proves the test can find the string when it is there —
|
||||||
|
without it, "0 occurrences" is just as consistent with a bad grep.
|
||||||
|
* **`T8aD` headers sit in RATC child order — use that, not the size, to name
|
||||||
|
one.** Attributing a sprite header to a sprite name by matching decoded
|
||||||
|
dimensions fails whenever two sprites share a size, and `GP_TITLE` build 4 has
|
||||||
|
such a pair (`ptlogo_back2eff` / `ptlogo_back2eff5`, both 1133×280) that differ
|
||||||
|
in exactly the flag being studied. Header order matches child order — verified
|
||||||
|
18/18 on that build against the dimensions as an independent check — so
|
||||||
|
ordering resolves what size cannot.
|
||||||
|
* **Identical summary statistics are not identical data.** Two same-sized sprites
|
||||||
|
reported the same %opaque, %clear and mean alpha to one decimal, which reads as
|
||||||
|
"the bundle stores this texture twice". Pixel-comparing them gives max abs diff
|
||||||
|
**21**: two different renditions of one image. Three matching summaries over
|
||||||
|
300 000 pixels is weak evidence of identity; the comparison costs one line.
|
||||||
|
* **A pattern that is perfect on one screen can be near-chance on the disc.** The
|
||||||
|
`0x02` bit matched `eff` names 10/10 on `GP_TITLE` build 4, which is exactly
|
||||||
|
the sample size that makes a rule feel found. Disc-wide it holds 46.8 % of the
|
||||||
|
time — the build's artists simply named their effect sprites consistently. An
|
||||||
|
18-element bundle cannot distinguish a format rule from a local naming habit;
|
||||||
|
before reporting an implication, run it over the corpus, not the example that
|
||||||
|
suggested it.
|
||||||
|
* **When an association survives a refuted implication, the counterexamples are
|
||||||
|
the finding.** The bit is 3.3× enriched for `eff` names and the sprites that
|
||||||
|
break the rule are `pv_loading_ring0`, `pv_loading_light0`–`3`,
|
||||||
|
`px_bunk_line` — rings, glows, lights, thin lines. That the exceptions are all
|
||||||
|
effect-like artwork *without* the naming convention says more about the field
|
||||||
|
than the rule it broke did.
|
||||||
|
* **Report a classifier's lift over its base rate, not its accuracy.** The
|
||||||
|
`0x02` bit's association with bright-RGB/low-alpha art gives a best single
|
||||||
|
threshold of 76.5 % accuracy — which sounds like a finding until you notice
|
||||||
|
that always guessing the majority class scores 64.1 %. The lift is 12 points,
|
||||||
|
the distributions overlap, and it is a tendency rather than a rule. An accuracy
|
||||||
|
quoted without its base rate is not interpretable.
|
||||||
|
* **Park a field after N failed hypotheses, and say what was eliminated.** Four
|
||||||
|
candidate meanings for one flag bit were each refuted at a cost of roughly an
|
||||||
|
iteration apiece, and the bit blocks nothing measurable. Stopping is the right
|
||||||
|
call, but only if the negative space is written down — additive blend, name,
|
||||||
|
lifetime, premultiplied alpha — so the next attempt starts where this one ended
|
||||||
|
rather than at the beginning.
|
||||||
|
* **Nothing was checking that a doc's figures match its committed data.** A
|
||||||
|
number is written once from a run and then lives in prose that gets edited
|
||||||
|
around it; the data file beside it is regenerated independently. All 19
|
||||||
|
headline figures across four censuses do currently agree
|
||||||
|
(`tools/re-capture/doc_figure_check.py`), which is worth knowing rather than
|
||||||
|
assuming — but the checker had to be written **numerically**, because the first
|
||||||
|
version grepped for the doc's formatting (`14 709` with a thin space, `33.7`
|
||||||
|
rounded from `33.66`) and reported almost every figure as a mismatch. A
|
||||||
|
consistency check that fails on formatting will train you to ignore it.
|
||||||
|
* **A detached job you never check can outlive many iterations.** Two
|
||||||
|
`setsid nohup cargo test … &` runs from earlier iterations were still alive
|
||||||
|
**four hours** later, one child at 89 % CPU for 3 h 26 m, holding the load
|
||||||
|
average at 14 on 12 cores. `setsid` was added precisely so a tool-call timeout
|
||||||
|
could not kill them — which also means nothing kills them. Check that a
|
||||||
|
backgrounded run actually exited before launching the next one, and prefer
|
||||||
|
`ps -o etime=` over assuming.
|
||||||
|
* **Know whether your verification gate can terminate.** `build-reborn test`
|
||||||
|
includes a disc test that decodes 166 `.xpr` files (1.4 GB) and had not
|
||||||
|
finished after 3 h 26 m of CPU. Every "green run" from it is partial unless the
|
||||||
|
suite is stated to have terminated — so report the suite count and elapsed
|
||||||
|
state rather than the word "green", which is what I should have been doing all
|
||||||
|
session.
|
||||||
|
* **"Cannot finish" and "takes an hour" are different claims — measure before
|
||||||
|
choosing.** I wrote that the verification gate *cannot terminate*, from having
|
||||||
|
watched it run 3 h 26 m. Timing the work directly: 19 of 166 containers exceed
|
||||||
|
25 s, `Stage_S02` completes in **144 s**, and one full pass is ~45–60 minutes.
|
||||||
|
Nothing hangs. The 3 h 26 m was that hour of work at a load average of 9–14,
|
||||||
|
inflated by my own duplicate runs. A slow thing observed under contention looks
|
||||||
|
like a stuck thing, and the correction matters: an hour-scale gate can be run
|
||||||
|
deliberately, a hung one cannot be run at all.
|
||||||
|
* **Separate the question with evidence from the question tangled with it.** The
|
||||||
|
keyframe-time reading was held back for several iterations by one objection:
|
||||||
|
adopting it changes `rest()` on a single element. But `rest()` reaches the times
|
||||||
|
only through a fallback that is unsound whenever it runs, and the shift does not
|
||||||
|
fix that fallback either — so the objection was never about the times. Timing
|
||||||
|
had three discriminating measurements; pose-selection had a heuristic guessing.
|
||||||
|
When a conclusion is blocked, check whether the blocker is actually evidence
|
||||||
|
about the same thing.
|
||||||
|
* **A predicate over adjacent PAIRS silently misclassifies a one-element list.**
|
||||||
|
"Has a plateau" was implemented as *any two adjacent keyframes share a pose* —
|
||||||
|
which is false for a single-keyframe element, so 1 502 static elements were
|
||||||
|
counted as having a *guessed* rest pose and the published defect rate was 65 %
|
||||||
|
too high. The error only surfaced when acting on it: suppressing those elements
|
||||||
|
dropped the title's correlation by 0.27, because they include the backgrounds.
|
||||||
|
Whenever a rule quantifies over pairs, ask what it says about a list of one.
|
||||||
|
* **Acting on a claim is a better test of it than re-reading it.** The
|
||||||
|
single-keyframe flaw survived a disc-wide census, a write-up and a handoff row.
|
||||||
|
It died the moment the rule was used to change a rendering, because the result
|
||||||
|
was visibly worse. If a measurement implies an action, take the action on
|
||||||
|
something you can score.
|
||||||
|
* **Score a rule where it can actually differ, or you will measure nothing.**
|
||||||
|
Three rest-pose rules rendered builds 4, 5 and 6 to *identical* correlations —
|
||||||
|
not because they agree, but because the code they change is unreachable on
|
||||||
|
those screens. The signal was on the two splashes, the only builds whose
|
||||||
|
elements reach the fallback at all. Identical results across variants is a
|
||||||
|
clue that the variant is not being exercised, not evidence that the choice does
|
||||||
|
not matter.
|
||||||
|
* **An argument from symmetry is a prediction, not a refutation.** I killed the
|
||||||
|
last-keyframe rule because it treats one of three sibling glows differently,
|
||||||
|
which felt like an artefact. Measured, it is the better rule on both screens
|
||||||
|
where it applies. Aesthetic expectations about how authored data "should" look
|
||||||
|
are worth stating as hypotheses and worth nothing as verdicts.
|
||||||
|
* **A blank render is a correlation of NaN, not a low score — notice which.**
|
||||||
|
Applying "rest = last keyframe" to every element made both splashes render with
|
||||||
|
zero variance, so the correlation was undefined rather than poor. Reading the
|
||||||
|
NaN as "the tool failed" would have hidden the result; it *was* the result, and
|
||||||
|
the strongest form of it. Check for degenerate output before treating a missing
|
||||||
|
number as a broken measurement.
|
||||||
|
* **When a model predicts something and the measurement refuses, the model is
|
||||||
|
usually incomplete rather than wrong.** The shifted time reading says when each
|
||||||
|
pose is reached, which looked like it implied the final pose is the resting
|
||||||
|
one. It does not: it says nothing about whether the group is *played to
|
||||||
|
completion* while the screen is still up. The exit is the dismissal, so a
|
||||||
|
displayed screen never reaches its last keyframe. Both claims survive; what was
|
||||||
|
wrong was the step between them that I supplied.
|
||||||
|
* **The control is what turns a helpful edit into a finding.** Suppressing the
|
||||||
|
`_eff` glows raised both splashes from ≈0.96 to ≈0.998 — on its own that reads
|
||||||
|
as "we over-draw glows". Running the same edit on the three screens that settle
|
||||||
|
made them **worse** (−0.002, −0.092, −0.107), which is what makes the result
|
||||||
|
mean something specific: the over-draw is confined to transient screens, where
|
||||||
|
a plateau mid-animation is not evidence the element is on screen at rest. An
|
||||||
|
edit that improves one set of cases is only interesting once you have shown it
|
||||||
|
damages the cases where it should.
|
||||||
|
* **When a model reproduces durations but not positions, the missing piece is an
|
||||||
|
origin, not a rate.** Playing the splash timeline matched every element's
|
||||||
|
on-screen *length* to within 2 % while placing the elements in the wrong part
|
||||||
|
of the run. That pattern names the gap precisely — the rate and the shape are
|
||||||
|
right, so what is absent is a per-group start — and it pointed straight at the
|
||||||
|
one undecoded word in the group header. (Which was zero, but the diagnosis was
|
||||||
|
still what made the next step obvious rather than a search.)
|
||||||
|
* **Two elements with identical data and different outcomes is the strongest
|
||||||
|
possible evidence that the decision is elsewhere.** `palogo_anima` and
|
||||||
|
`palogo_gamearts` carry byte-identical keyframe times; one is drawn for 95
|
||||||
|
frames and the other not at all. No amount of re-reading the placement data can
|
||||||
|
explain that, and the pair is worth more than a survey — when a hypothesis says
|
||||||
|
"the data decides", look for two records that agree and behave differently.
|
||||||
|
* **A shared resource address does not identify the resource's owner.** The draw
|
||||||
|
log's `tex[base=…]` looked like it would say whether two animation phases came
|
||||||
|
from the same bundle. All three splash phases report `0x11C30000` — including
|
||||||
|
the publisher splash, which is certainly a *different* bundle. The address is a
|
||||||
|
reused upload slot. The control was free and sitting in the same table; without
|
||||||
|
it the shared base would have read as proof of a shared bundle.
|
||||||
|
* **State the mechanism as a separate claim from the observation.** "Declared
|
||||||
|
elements are not what gets drawn" is measured and holds. "Because one bundle
|
||||||
|
activates its elements selectively" was an explanation I attached to it, and it
|
||||||
|
is not established. Bundling the two let the weaker claim inherit the stronger
|
||||||
|
one's evidence.
|
||||||
|
* **Ask what the competing hypothesis would REQUIRE on the disc.** Two
|
||||||
|
explanations for the splash's disjoint phases looked separable only by a
|
||||||
|
capture I could not take, and the capture-side test failed its control. The
|
||||||
|
alternative needed a bundle declaring the glows without the logos — a
|
||||||
|
four-entry enumeration showed none exists, and the question closed statically.
|
||||||
|
A hypothesis that predicts an artefact can be killed by looking for the
|
||||||
|
artefact, which is often far cheaper than measuring the behaviour.
|
||||||
|
* **Check what a "measured" value was measured *from* before reasoning about
|
||||||
|
its limits.** Two things that never co-occur have no observable relative order
|
||||||
|
— true, and a real limit on any order read from a *draw capture*. I applied it
|
||||||
|
to the splash's paint order and withdrew it hours later: that vector is a read
|
||||||
|
of the runtime **child array**, which has a definite order whether or not its
|
||||||
|
children are ever drawn together, and the two halves in question also carry
|
||||||
|
distinct static layer keys. Both the provenance and an independent static field
|
||||||
|
said the order was real. The general trap is not the co-occurrence rule, it is
|
||||||
|
applying a source-specific limit to a value from a different source: the word
|
||||||
|
"measured" covers several kinds of evidence, and the page that recorded it
|
||||||
|
usually says which — `ui-screen-runtime.md` said "child slots" in as many
|
||||||
|
words. Read that line before building an argument on top of it.
|
||||||
|
|
||||||
|
* **A heuristic that is right 99.9 % of the time still has a shape to its
|
||||||
|
failures — find it before trusting the field.** RATC child names were read by
|
||||||
|
scanning backwards for the last printable run of bytes. That agrees with the
|
||||||
|
format's own `opt ` declaration on 17 918 of 17 942 children, which is the kind
|
||||||
|
of agreement that stops people looking. The 24 exceptions were not random: all
|
||||||
|
24 are the *same* case, a 3-byte binary tail that happens to be printable ASCII
|
||||||
|
(`8AX`), and one of them was the full-resolution background of every menu
|
||||||
|
screen we care about. Ask what the format *states* before settling for what a
|
||||||
|
scan *infers*, especially when the stated version is already decoded elsewhere
|
||||||
|
in the same file — `opt ` was being read for button focus links the whole time.
|
||||||
|
|
||||||
|
* **A `continue` that silently skips is a defect even when the skip is correct.**
|
||||||
|
`compose` drops an element whose sprite does not resolve. Two arms above it
|
||||||
|
record the name into `missing` first; the `el.sprite.is_none()` arm does not.
|
||||||
|
So a screen lost its background and `screen render` still reported "sprites
|
||||||
|
that did not resolve: none" — the diagnostic was structurally unable to see it.
|
||||||
|
When adding an early-out to a loop that already reports what it discards, make
|
||||||
|
it report through the same channel, or it becomes a place findings go to die.
|
||||||
|
|
||||||
|
* **"It has no name" can mean "it is not a thing that gets named."** Sixty RATC
|
||||||
|
children had no `opt ` name block and the open question was whether the block
|
||||||
|
was absent or merely outside our search window. It was neither: the sixty are
|
||||||
|
*frames*, ten each of six copies of one `.tan` animation, and one `opt ` block
|
||||||
|
names the whole run. The give-away was in the data before any hypothesis was —
|
||||||
|
the distances back to the nearest tag were an exact arithmetic progression
|
||||||
|
(`213 + n·60600`), i.e. ten different records finding the *same* tag. When a
|
||||||
|
negative result's measurements come out evenly spaced, the thing you are
|
||||||
|
counting is probably not the thing the format counts.
|
||||||
|
|
||||||
|
* **This container OOM-kills `slb_leading_segment_disc` under default test
|
||||||
|
parallelism.** It dies with `signal: 9, SIGKILL` and no assertion — eight
|
||||||
|
threads each holding a slice of a ~1.1 GB bank. It is not a regression and not
|
||||||
|
a flake, and it reproduces when run alone. `-- --test-threads=1` passes 8/8 in
|
||||||
|
20 s. Before believing a SIGKILL in this repo, re-run the suite serially;
|
||||||
|
before believing a *pass*, check nothing else heavy was sharing the box.
|
||||||
|
⚠️ And budget for `mesh_consistency_disc`: it takes **22 minutes** (1 318 s
|
||||||
|
measured, serial) and produces no output while it runs, so `build-reborn test`
|
||||||
|
looks hung for a third of an hour. It is not. Two runs were killed for looking
|
||||||
|
stuck before it was timed. If the change under test is not in the mesh path,
|
||||||
|
`build-reborn t -p <crate>` scopes around it — note that `build-reborn test`
|
||||||
|
itself passes `--workspace` and will ignore a `-p`.
|
||||||
|
|
||||||
|
* **Never measure a file another process is still writing.** `ffprobe` on a
|
||||||
|
half-written transcode reported 33 s against a 137 s source — the shape of
|
||||||
|
catastrophic truncation, with no error and no warning, and it nearly became a
|
||||||
|
filed bug. The tell was the mtime and a climbing packet count, not anything in
|
||||||
|
the tool's output. Before comparing an artifact you did not produce, check that
|
||||||
|
whoever produces it has finished; across two agents sharing a repo this is a
|
||||||
|
race, not an edge case.
|
||||||
|
|
||||||
|
* **A difference-signal RMS means nothing until the two are aligned.** Comparing a
|
||||||
|
transcode against its source gave source RMS −25.3 dB and difference RMS
|
||||||
|
−34.2 dB — only ~9 dB down, which looks like a failure. It is inconclusive: a
|
||||||
|
one-sample offset makes the difference nearly as loud as the source. Any such
|
||||||
|
test needs cross-correlation to align first and an agreed downmix, and only then
|
||||||
|
is a pass mark like ">40 dB down" meaningful. Reporting the 9 dB as a result
|
||||||
|
would have been a confident wrong number.
|
||||||
|
|||||||
@@ -35,6 +35,159 @@ neighbourhood, not just the line.
|
|||||||
|
|
||||||
## Screens, classes and RTTI
|
## Screens, classes and RTTI
|
||||||
|
|
||||||
|
* "the rotated draw in the title's capture is the `Z` swoosh
|
||||||
|
(`ptlogo_back2*`)" → **mine, and refuted.** Its quads span y −209…925 and
|
||||||
|
−292…1012 in screen space; the swoosh is a band at y 126…360.
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* "the keyframe words at `+4`/`+8`/`+12` are always zero" → they are non-zero in
|
||||||
|
**4.76 %, 4.62 % and 14.50 % of 83 862** blocks disc-wide, reading as
|
||||||
|
**degrees** (±180, ±90, 120, 360). The original note was a sample artefact.
|
||||||
|
(Superseded figures: an earlier count of 72 287 blocks missed every nested
|
||||||
|
record — see the alignment entry below.)
|
||||||
|
* ~~"those angle fields are where the title's rotated quads come from" → **no** —
|
||||||
|
every `GP_TITLE` build 4 element has all three at zero.~~ → **that refutation
|
||||||
|
was itself wrong, and is withdrawn (2026-08-28).** `+12` *is* exactly where
|
||||||
|
they come from. Build 4's **top-level** elements do all read zero; the rotated
|
||||||
|
quads belong to its two **nested** leaf records, `ptloop01.rat` (`+12` = 30)
|
||||||
|
and `ptloop02.rat` (`+12` = −45), which the census never opened. Measured
|
||||||
|
off the GPU: **+30.26°** and **−45.28°**.
|
||||||
|
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||||
|
* "the rotated draw's element cannot be named from the capture" → refuted; its
|
||||||
|
quads' **edge lengths** name it. 400 × 1076 and 400 × 1444 match `pteff03`
|
||||||
|
399×180 at 600 % and `pteff03a` 399×180 at 800 % — two different heights, both
|
||||||
|
landing. [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||||
|
* "a keyframe-block scanner may assume 4-byte alignment" → refuted; it found
|
||||||
|
**0/3** of its own control blocks and under-counted the corpus by 16 341
|
||||||
|
blocks. Nested `RATC` blobs start at odd offsets (`0xbb5966`).
|
||||||
|
* "keyframe rotation lives only in **nested** `.rat` leaf records" → **mine, and
|
||||||
|
refuted within the hour by my own sweep.** It held for the three archives I had
|
||||||
|
checked (`GP_TITLE`, `GP_BUNK`, `GP_CHALLENGE`) and failed on the next:
|
||||||
|
`GP_DIALOG` and `GP_DEBRIEFING_PILOTLOG` rotate **top-level** elements, and
|
||||||
|
those are the clearest examples on the disc.
|
||||||
|
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||||
|
* "the pivot-anchored scale formula `kf.x − pivot·(scale−100)/100` is our
|
||||||
|
renderer's reading, not a measurement" → **now measured.** At the `ptloop`
|
||||||
|
pair's 600 %/800 % scale it predicts both quad centres at y = 360.0 against a
|
||||||
|
captured 359.1/360.0, where top-left anchoring predicts 810/990.
|
||||||
|
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||||
|
* "the game passes a pink per-vertex colour for the title swoosh" → **refuted by
|
||||||
|
draw capture.** Every vertex colour in the capture is `<alpha>FFFFFF`, white RGB.
|
||||||
|
* "the swoosh discrepancy is undecodable" → **solved**: the game submits it as two
|
||||||
|
**rotated parallelograms**; `ui_layout::blit` only does axis-aligned rectangles.
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* "the `--log_ui_draws` per-draw capture reads the guest's blend state" →
|
||||||
|
**mine, and wrong.** It records primitive type, index count, index-buffer
|
||||||
|
address, VS/PS ucode hashes, texture bindings and vertex attribute 0 — no
|
||||||
|
`RB_BLENDCONTROL`. It can test vertex colour as-is; blend state needs a Canary
|
||||||
|
change. [`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* "the plate-free title capture (t ≈ 4.0 s) may be too early to be settled" →
|
||||||
|
**mine, and refuted.** The swoosh band correlates 0.7342 at t = 4.0 s and
|
||||||
|
0.7353 at t = 21.5 s — identical to 0.001 over 17.5 s.
|
||||||
|
* "`T8aD +0x04` bit `0x02` selects an additive blend" → **mine, and refuted.**
|
||||||
|
Blending those sprites additively worsens every measure against the capture.
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* "the title logo elements' wrong pivots (off by up to 59 px, authored against the
|
||||||
|
other language's sprite) explain the swoosh rendering too thick" → **mine, and
|
||||||
|
refuted.** `blit` sizes from the texture and applies the pivot only as
|
||||||
|
`pivot·(scale−100)/100`; all seven swoosh elements are scale `(100,100)` at every
|
||||||
|
keyframe, so the term is zero. The mismatch is real but inert here — it would
|
||||||
|
bite `ptlogo1`/`ptlogo2`, which scale to 150 during the build-in.
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* "the title screen loops at ≈ 2.2 s" → **mine, and doubly wrong.** The title
|
||||||
|
*art* is near-static (wordmark sd 0.06); the 2.3 s pulse is the
|
||||||
|
**`PRESS Ⓐ BUTTON` plate**, which is a *different build* (2, not 4). Localised
|
||||||
|
by a per-tile amplitude map and decoded in `ptbtn00f.rat`.
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* ~~"a `.rat` leaf record's keyframes use the build's 40-byte layout" → they do
|
||||||
|
not~~ — **withdrawn.** They do. The scan that "found nothing" required 29
|
||||||
|
increasing times; the records hold **three**. Decoded in
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md).
|
||||||
|
* "the title's measured ≈ 2.2 s oscillation is the `ptloop01`/`ptloop02`
|
||||||
|
elements" → **mine, and refuted by decoding them.** Their sweeps run 7.5 s and
|
||||||
|
9.5 s; a 22 s capture showed 8 peaks, not 3. The period's source is unidentified.
|
||||||
|
* "the developer splash cannot be rendered by `screen render` at all" → it can,
|
||||||
|
with **`--all`**. It is only invisible to the *default* listing, which filters on
|
||||||
|
`is_build`. [`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
* "the four `GP_TITLE` entries that are not screen builds are unidentified" → they
|
||||||
|
are the **splash**: 10/13 the `SQUARE ENIX` logo, 11/14 the developer logos.
|
||||||
|
* ~~"the boot title is phase 2 and the attract title is phase 4 state 0, and only
|
||||||
|
phase 2 handles Ⓐ" → refuted~~ — **the REFUTATION is withdrawn.** The test
|
||||||
|
assumed Ⓑ lands in phase 4 state 0; the event-0 block actually sets
|
||||||
|
**phase = 2** and state = 0 together, so it probed phase 2. The hypothesis is
|
||||||
|
untested, not dead.
|
||||||
|
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||||
|
* "any title after the first one refuses input" → too broad. The **Ⓑ-returned
|
||||||
|
title accepts Ⓐ**; only the **attract**-returned title is inert.
|
||||||
|
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md)
|
||||||
|
* "`sub_821C6458` is `GamePart_Title`'s state machine" → **mine, imprecise.** It
|
||||||
|
is the machine for **phase 4** of a five-way outer dispatch at `this+132`;
|
||||||
|
phase 0 is the splash. The ten states and eighteen edges are phase 4 only.
|
||||||
|
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||||
|
* "`sub_821CC860` is the game's by-name screen factory" → **mine, and wrong.** Its
|
||||||
|
decoded arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
|
||||||
|
`GAMMA_RGB` — it is a **generic name-keyed lookup**, mostly config.
|
||||||
|
* "`DIFFICULTY` and `EXTRA_MENU` are corroborated screen names" → **mine, and
|
||||||
|
wrong.** Neither appears in `r5` at any of the 48 call sites; they were strings
|
||||||
|
merely referenced by the same functions. Only `TUTORIAL_MENU` survives.
|
||||||
|
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||||
|
* "the `{func, func, ptr}` triples at `0x820a3b48` are a GamePart state table" →
|
||||||
|
**static-initialiser records trailing the `RegisterToFactory` strings.** The
|
||||||
|
bytes before them are the tail of a diagnostic string and the data column is
|
||||||
|
zero-filled descriptors.
|
||||||
|
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||||
|
* "the boot sequence is driven by a table the game reads" → it is **not
|
||||||
|
data-driven**; four search spaces closed, transitions are calls with an id
|
||||||
|
argument. [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||||
|
* "`config.ini` is a GamePart settings table, so the boot order is in it" → its
|
||||||
|
`[SYSTEM]` section is **empty**; the only populated section is `[LANGUAGE]`.
|
||||||
|
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||||
|
* "the attract loop is `GP_ADVERTISE_DEMO` (GamePart 1)" → 🟡 id 1 has **no
|
||||||
|
registration site** in the shipped build; the attract is the title replaying
|
||||||
|
`ADV.wmv`.
|
||||||
|
* "the 29 id-table names are 29 distinct GameParts" → 24 register, and `3`/`4`
|
||||||
|
are the **same class** (`GamePart_SaveLoad`).
|
||||||
|
* "Ⓐ on `NEW GAME` leads to a standing black-screen hang" → it opens
|
||||||
|
**`DIFFICULTY`**, then **`SELECT DATA`**. What looked like a hang was a menu
|
||||||
|
waiting for input that nobody pressed; the crash that follows is the already
|
||||||
|
documented `sub_823070B0` cache throw.
|
||||||
|
[`menu-navigation-semantics.md`](menu-navigation-semantics.md)
|
||||||
|
* "tapping Ⓐ during the boot movie breaks the title" → **one** tap skips the
|
||||||
|
movie cleanly and the title works normally. It is *hammering* (88 presses) that
|
||||||
|
breaks it. [`movie-binding.md`](movie-binding.md)
|
||||||
|
* "the attract movie runs ~85 s, so it is not `ADV.wmv` (137 s)" → **mine, and
|
||||||
|
wrong.** Sampling began 39 s into the movie, so what was timed was its tail.
|
||||||
|
The attract movie **is** `ADV.wmv`, played in full.
|
||||||
|
[`movie-binding.md`](movie-binding.md)
|
||||||
|
* "the boot intro and the attract movie are different videos" → one asset, one
|
||||||
|
manifest slot (`ADVERTISE_MOVIE`).
|
||||||
|
* "the new-game intro is unidentified" → `MS00A` → `S00A.wmv`, decoded from the
|
||||||
|
movie manifest.
|
||||||
|
* "`GP_READY_ROOM.pak` holds the Ready Room screen" → its 317 distinct element
|
||||||
|
names contain **none** of the six visible labels; it is the briefing /
|
||||||
|
tactical-map content behind the `BRIEFINGS` item.
|
||||||
|
[`ready-room-probe.md`](ready-room-probe.md)
|
||||||
|
* "the Ready Room might be 3D with a UI overlay" → it is **2D**; the pak carries
|
||||||
|
zero 3D containers.
|
||||||
|
* "element kind `0x3002` is *the* button kind" → title-side only. All 902
|
||||||
|
`GP_READY_ROOM` bundles have **zero** `0x3002`; that pak uses `0x3000`,
|
||||||
|
`0x3004`, `0x300c`, `0x3008`. `0x3002` is one member of a `0x3000` family.
|
||||||
|
* "the transition between menu screens is a cut" → it is a **fade through
|
||||||
|
black**; a 0.5 Hz screenshot burst simply samples too slowly to see it.
|
||||||
|
[`screen-transitions.md`](screen-transitions.md)
|
||||||
|
* "the main menu's initial focus is fixed" → three boots of one script gave
|
||||||
|
`TUTORIAL`, `TUTORIAL`, `NEW GAME`.
|
||||||
|
* "the title menus drop d-pad presses shorter than ~0.3 s" → **refuted by my
|
||||||
|
own data.** The menu **wraps at both ends**; every press registered, and the
|
||||||
|
"missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md).
|
||||||
|
* "the main menu opens with `NEW GAME` focused" → it opens on **`TUTORIAL`**,
|
||||||
|
2/2 boots (🟡 a third recorded run implies `NEW GAME`, so this is reproducible,
|
||||||
|
not invariant).
|
||||||
|
* "`GP_TITLE` builds 6/8/9 are three submenus" → **8 is the JAPANESE main
|
||||||
|
menu**, and 6/9 are the English and Japanese `EXTRAS` submenu. `GP_TITLE`
|
||||||
|
holds eight screens shipped twice (EN/JP), and exactly one submenu. See
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md).
|
||||||
|
* "the `PRESS Ⓐ BUTTON` plate is a state of the title build" → it is **its own
|
||||||
|
build** (2/3), composited over build 4 and faded in a beat later.
|
||||||
* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150
|
* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150
|
||||||
`ANON_`, 0 `rtti_present`, 0 base classes.
|
`ANON_`, 0 `rtti_present`, 0 base classes.
|
||||||
* "the sibling vtable methods name the class" → they cannot.
|
* "the sibling vtable methods name the class" → they cannot.
|
||||||
@@ -102,6 +255,46 @@ neighbourhood, not just the line.
|
|||||||
* "a set-difference over file names can see reuse" → it cannot; **join per
|
* "a set-difference over file names can see reuse" → it cannot; **join per
|
||||||
USER**. Per-pak copies are ×6.
|
USER**. Per-pak copies are ×6.
|
||||||
|
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
* "which bank the menu plays is not on the disc" → the **cue table** cannot say
|
||||||
|
(all BGM cues are numeric), but the **code** can: `sub_821C5580` plays cue
|
||||||
|
**1103 = `BGM_103`**, and its two declared waves match the two streams the XMA
|
||||||
|
probe saw byte-for-byte.
|
||||||
|
* "the observed BGM stream sizes match no bank's declared waves, so the game hands
|
||||||
|
the decoder a window" → **mine, and wrong** — I checked only the `BGM_0xx` rows.
|
||||||
|
They are `BGM_103`'s two waves exactly; the game hands over the whole wave.
|
||||||
|
|
||||||
|
* "an individual SE cue's audio cannot be extracted" → **mine, and wrong.**
|
||||||
|
`--xma_param_probe=true` logs each stream's head bytes; searching them in
|
||||||
|
`Static.slb` locates the wave exactly. [`menu-audio-cues.md`](menu-audio-cues.md)
|
||||||
|
* "`Static.slb` has no wave boundaries, so its layout is unknown" → it is a
|
||||||
|
**packed run** of whole 2 048-byte XMA packets with no delimiters — the two
|
||||||
|
located cues are contiguous. There is nothing to scan for, by design.
|
||||||
|
|
||||||
|
* "`Pj_Silph.xgs` holds the cue→wave index, so parse XACT" → **no XACT container
|
||||||
|
exists on this disc**: 0 × `XGSF`/`SDBK`/`WBND` in all 1.08 GB of `sound.pak`,
|
||||||
|
and no `XACT`/`.xgs` string in the executable. The extensions are the authoring
|
||||||
|
tool's, not the format's. [`menu-audio-cues.md`](menu-audio-cues.md)
|
||||||
|
|
||||||
|
* "every sound cue resolves to its own `.slb` bank" → the 322 `SE_*` cues do not;
|
||||||
|
**0 of 322** are in `FILES`, and `BANK_SE` puts them all in `Static.slb`.
|
||||||
|
* "`Static.slb` can be split into waves like any other bank" → it holds **0
|
||||||
|
`RIFF`, 0 `seek`, 0 `WAVE`** across all 8 353 472 readable bytes.
|
||||||
|
[`menu-audio-cues.md`](menu-audio-cues.md)
|
||||||
|
|
||||||
|
* "`BGM_001.slb` is three sub-waves (10 KB + 4.47 MB + 4.67 MB)" → the 10 KB is
|
||||||
|
the **bank header**; a bank is **two** waves.
|
||||||
|
* "a music bank's two waves might be intro + loop, two variations, or two halves"
|
||||||
|
→ they are **two stems of one performance, played together** — equal duration
|
||||||
|
in 32/32 banks, and sample-synchronous.
|
||||||
|
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md)
|
||||||
|
* "`BGM_106`–`BGM_109` break the two-wave rule" → they are the leading-region
|
||||||
|
straddle; realigned across entry boundaries they obey it.
|
||||||
|
* "the cue table names which BGM belongs to which screen" → all 32 BGM cues are
|
||||||
|
numeric (`BGM_001`…`BGM_109`).
|
||||||
|
|
||||||
## Units, weapons, effects and assets
|
## Units, weapons, effects and assets
|
||||||
|
|
||||||
* "`Generic` (394) is the unit datasheet" → refuted.
|
* "`Generic` (394) is the unit datasheet" → refuted.
|
||||||
@@ -183,3 +376,244 @@ neighbourhood, not just the line.
|
|||||||
|
|
||||||
* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328.
|
* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328.
|
||||||
* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.
|
* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.
|
||||||
|
* "the splash `_eff` glows hold a constant α ≈ 33, contradicting their declared
|
||||||
|
255 plateau" → **mine, and refuted within the iteration.** They ramp 34 → 255
|
||||||
|
in exact steps of 34. I had printed the series' minimum and read it as its
|
||||||
|
range. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||||
|
* "the declared keyframe timeline reproduces the captured splash" → **refuted for
|
||||||
|
multi-keyframe elements.** `palogo_gamearts` is still at `a=255` nine frames
|
||||||
|
after its declared `a=32`, and its declared 80-frame fade-in is never drawn.
|
||||||
|
The `_eff` glows do reproduce, exactly — so this is about the group timeline,
|
||||||
|
not about the interpolation law. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||||
|
* "the `_eff` elements' agreement is the whole case for the keyframe-time shift,
|
||||||
|
so it stays a shape argument" → superseded. The **hold duration** is
|
||||||
|
calibration-free and decides it: observed 83 frames of full alpha against a
|
||||||
|
predicted **2.0** as decoded and **80.0** shifted. The shift is nonetheless
|
||||||
|
**not adopted** — it moves `GP_TITLE` build 7 by 13 % of pixels, away from its
|
||||||
|
verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||||
|
* "the `GP_TITLE` build 7 render difference is evidence against the keyframe-time
|
||||||
|
shift" → **mine, and withdrawn.** It is one element, `ptlogo_eff3.t32`, a
|
||||||
|
transient bloom with no resting pose; `rest()`'s dwell fallback returns a
|
||||||
|
different endpoint of the same movement under each reading. The brightness
|
||||||
|
comparison measured our heuristic, not the decode.
|
||||||
|
[`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "`rest()`'s longest-dwell fallback picks the pose the element rests at" →
|
||||||
|
**refuted structurally.** A dwell gap is time spent interpolating *between*
|
||||||
|
poses; an endpoint is only held when the two poses are equal, which is a
|
||||||
|
plateau, which the earlier path already returned for. Every element that
|
||||||
|
reaches the fallback has a guessed rest pose.
|
||||||
|
* "`rest()` for a plateau-less element should be the **last keyframe**" → **mine,
|
||||||
|
and refuted.** The developer splash's three sibling glows are structurally
|
||||||
|
identical and differ by one byte (`a=212` vs `a=255` at `t=45`); that rule
|
||||||
|
makes `palogo_anima_eff` alone invisible while `gamearts_eff` and `seta_eff`
|
||||||
|
stay lit. Capture box-mean ratios (0.717 / 0.723 / **0.772**) go the other way
|
||||||
|
too. [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "a keyframe `scale` of 0 means *unset*, so render at 100 %" → **refuted by a
|
||||||
|
disc-wide control.** 2 166 elements have a zero-scale keyframe and **not one is
|
||||||
|
zero on every keyframe**, while 1 762 grow back out of zero (`ptlogo_eff3.t32`
|
||||||
|
runs 0 % → 200 %). Zero means collapsed; the renderer now draws nothing.
|
||||||
|
[`structures/ui-rat-layout.md`](structures/ui-rat-layout.md)
|
||||||
|
* "a Japanese-locale capture is impossible in this container, because canary has
|
||||||
|
no `user_language` cvar" → **mine, and refuted the next iteration.** The cvar
|
||||||
|
really is absent, but the language is persisted in
|
||||||
|
`<storage_root>/xconfig.settings` (`user.language`, BE u32 at file offset
|
||||||
|
`0x912`, located from three struct landmarks) and that file is writable. The
|
||||||
|
capture is still not *taken* — a Japanese run never reached the title in 787 s
|
||||||
|
— but it needs a longer run, not a rebuilt emulator.
|
||||||
|
[`tools/re-capture/set_console_language.py`](../../tools/re-capture/set_console_language.py)
|
||||||
|
* "the Japanese-locale run never reached the title in 787 s" → **the measurement
|
||||||
|
was broken, not the run.** `wait_title.sh` carried the superseded single-pixel
|
||||||
|
oracle. Re-run with `is_title.py`: the game still did not present the
|
||||||
|
interactive title, but that is now a measured statement (zero green-glyph
|
||||||
|
pixels, correlation ≤ 0.22 to either build-7 render) rather than an artefact.
|
||||||
|
* "the Japanese-locale run fails to reach the interactive title *because of the
|
||||||
|
locale*" → **refuted by the English control.** 75 samples over 734 s with the
|
||||||
|
same flags and oracle, every one glyph = 0. Neither locale presents the
|
||||||
|
interactive title without a pad press.
|
||||||
|
* "neither locale reaches the interactive title without a pad press" /
|
||||||
|
"the game sat in the attract loop for 604 s" → **withdrawn as causes.** Both
|
||||||
|
rest on runs whose polling loop sampled every ~41 s, because `screenshot`
|
||||||
|
costs 10.8 s while the emulator runs (0.117 s idle, 92×). A title lasting a few
|
||||||
|
seconds would be missed. The observations stand; the conclusions drawn from
|
||||||
|
them do not. [`capture-harness-status.md`](capture-harness-status.md)
|
||||||
|
* "the boot harness fails because its polling loop samples every ~41 s, slower
|
||||||
|
than the title screen lasts" → **mine, and refuted by my own fix.** The
|
||||||
|
sampling defect was real (3.98 s → 0.29 s per sample, 13.7×, control-verified
|
||||||
|
at 753/327), but a probe running at 3.99 fps for **420 continuous seconds —
|
||||||
|
1 674 samples — still saw zero green-Ⓐ pixels.** Sampling rate was not the
|
||||||
|
cause. [`capture-harness-status.md`](capture-harness-status.md)
|
||||||
|
* ~~"neither locale reaches the interactive title without a pad press"~~ →
|
||||||
|
withdrawn last iteration for want of evidence, now **reinstated as a
|
||||||
|
measurement**: 1 674 dense samples over 420 s, English, zero glyph frames.
|
||||||
|
⚠️ Reach: a mid-run window only; it says nothing about the boot title.
|
||||||
|
* "the PRESS Ⓐ plate appears only in the **boot** title window, which mid-run
|
||||||
|
sampling could never catch" → **mine, and refuted.** The fast probe was
|
||||||
|
attached at t=0: **2 391 frames over 600 s at 3.98 fps from launch**, max glyph
|
||||||
|
0. The plate did not appear at any point in the first ten minutes.
|
||||||
|
[`capture-harness-status.md`](capture-harness-status.md)
|
||||||
|
* "2 391 frames over 600 s from t=0, max glyph 0, therefore the title never
|
||||||
|
appears in the first ten minutes" → **withdrawn: the instrument stalls.** A
|
||||||
|
single long-lived x11grab stream degrades 3.98 → 1.60 fps and then freezes,
|
||||||
|
repeating one stale frame; cross-checked, it read surface mean 5.21 where
|
||||||
|
`import` read 125.65 at the same moment. A dense negative from a frozen stream
|
||||||
|
is not a negative. [`capture-harness-status.md`](capture-harness-status.md)
|
||||||
|
* "the `T8aD` layer key fully determines a screen's paint order" → **refuted, and
|
||||||
|
the remainder is undecodable.** Elements sharing a key are tied; on the title
|
||||||
|
the game paints the five tied `ptlogo_back2eff` glows `1,2,5,3,4` while the
|
||||||
|
declaration table, the RATC child order and **every** field in the `T8aD`
|
||||||
|
header (exhaustive 0x00–0x7f, u8/u16/u32, both directions — 0 matches against
|
||||||
|
64 for the declaration-order control) all give `1,2,3,4,5`.
|
||||||
|
[`ui-paint-order-derived-check.md`](structures/ui-paint-order-derived-check.md)
|
||||||
|
* "SE audio is undecodable from the disc — no XACT container exists anywhere"
|
||||||
|
(as it stood on the **handoff page**) → **stale**: `menu-audio-cues.md` had
|
||||||
|
already retracted it and located three cues in `Static.slb` that decode to PCM.
|
||||||
|
The retraction never reached the row the port agent reads. Handoff row fixed;
|
||||||
|
`tools/re-capture/handoff_lint.py` now checks for this class.
|
||||||
|
* "which of `8AX` and `ptbase` the game draws needs a per-draw capture recording
|
||||||
|
texture base addresses" → **mine, and refuted — it is settled statically.** The
|
||||||
|
two carry the same art at two resolutions, so neither compares usefully against
|
||||||
|
a capture; their *difference* does. Correlating the capture's
|
||||||
|
departure-from-upscale against the 8AX-only detail gives +0.0475 (main menu)
|
||||||
|
and +0.0634 (title), both 68 % of ceiling against matched controls of ≤0.0095.
|
||||||
|
The game draws the full-res `8AX`.
|
||||||
|
[`ui-8ax-fullres-background.md`](structures/ui-8ax-fullres-background.md)
|
||||||
|
* "the pixel-pair ratio shows the capture is native, not an upscale" → **mine,
|
||||||
|
and withdrawn as evidence.** Upscales give 0.00–0.72, native 0.98, capture 1.01
|
||||||
|
— but additive noise pushes any such ratio toward 1, and both "native + noise"
|
||||||
|
and "bilinear upscale + noise" fit the observed values. The conclusion happens
|
||||||
|
to be right; this test does not establish it.
|
||||||
|
* "the render-vs-capture gamma may be canary's own BT.709 output transform, since
|
||||||
|
`kernel_display_gamma_type = 2`" → **mine, and refuted from the source.** That
|
||||||
|
cvar is the value a `kStub` getter (`VdGetCurrentDisplayGamma`) hands the
|
||||||
|
**guest**; the game builds its own ramp from it and canary applies the guest's
|
||||||
|
`DC_LUT` ramp in the swap path. No emulator-side gamma post-process exists to
|
||||||
|
subtract. 🟡 Whether this game installs a ramp at all is still unestablished.
|
||||||
|
[`ui-render-tone-curve.md`](structures/ui-render-tone-curve.md)
|
||||||
|
* "the GPU trace produced nothing because either the CLI flag did not reach the
|
||||||
|
cvar or `BeginTracing()` failed silently" → **both wrong.** The trace writer is
|
||||||
|
**compiled out**: `trace_writer.h` gates it on `#ifdef NDEBUG`, so a release
|
||||||
|
build has no writer at all. Confirmed with a control — the format string
|
||||||
|
`_stream.xtr` appears **once** in the Debug binary and **zero** times in the
|
||||||
|
Release binary `run-canary` actually uses.
|
||||||
|
[`capture-harness-status.md`](capture-harness-status.md)
|
||||||
|
* "the `T8aD` `+0x04` bit `0x02` means the sprite's name contains `eff`" →
|
||||||
|
**refuted, now on evidence.** `ptlogo_back2eff` is an `eff` name with the bit
|
||||||
|
clear; the attribution is confirmed by header-order pairing (18/18 on build 4)
|
||||||
|
rather than by a size match, which cannot separate it from the same-sized
|
||||||
|
`ptlogo_back2eff5`. All 10 bit-set sprites *are* `eff` names, so the
|
||||||
|
implication runs one way only.
|
||||||
|
* "the bit `0x02` marks a transient element" → **refuted.** `pteff03` and
|
||||||
|
`pteff03a` carry the bit and run to `t=250`, ramping to `a=255` and holding.
|
||||||
|
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
|
||||||
|
* "bit `0x02` set ⇒ the sprite's name contains `eff`" (the one-way reading that
|
||||||
|
survived the biconditional's refutation) → **mine, and refuted disc-wide the
|
||||||
|
next iteration.** True 10/10 on `GP_TITLE` build 4; over 14 709 sprites it
|
||||||
|
fails on **2 657 of 4 995** bit-set ones. `P(eff|set) = 0.468` against
|
||||||
|
`P(eff|clear) = 0.144` — an association, not an implication.
|
||||||
|
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
|
||||||
|
* "`T8aD +0x04` bit `0x02` selects premultiplied alpha" → **refuted.**
|
||||||
|
Premultiplied requires `RGB ≤ A` everywhere; over 170 decoded textures the
|
||||||
|
flagged group violates it on a median **52.5 %** of pixels against **30.2 %**
|
||||||
|
unflagged — both far from premultiplied, and the flagged group *further*.
|
||||||
|
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
|
||||||
|
* "`build-reborn test` cannot terminate" → **mine, and too strong; corrected the
|
||||||
|
next iteration.** It is heavy, not hung: 19 of 166 `.xpr` containers exceed
|
||||||
|
25 s, `Stage_S02` completes in **144 s** with `rc = 0`, and one full pass is
|
||||||
|
~45–60 minutes. The 3 h 26 m observed was that work at a load average of 9–14,
|
||||||
|
inflated by my own two duplicate runs.
|
||||||
|
[`test-suite-runtime.md`](test-suite-runtime.md)
|
||||||
|
* "the case for the keyframe-time shift rests on a single element" → **no longer
|
||||||
|
true.** Three elements across two screens discriminate and all favour it:
|
||||||
|
`palogo_gamearts` and `palogo_seta` hold full alpha 83 frames, `palogo_sqex`
|
||||||
|
≥77, against 6–8 predicted by the current reading and 80–102 by the shifted
|
||||||
|
one. The `_eff` glows fit both and argue against neither.
|
||||||
|
[`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||||
|
* "an element with no held pose should be drawn as NOTHING rather than at a
|
||||||
|
guessed endpoint" → **mine, and refuted.** Suppressing every plateau-less
|
||||||
|
element and re-correlating against the live captures: title +0.9500 → +0.6839,
|
||||||
|
main menu +0.9460 → +0.9037, `EXTRAS` +0.9440 → +0.9094. Worse on all three.
|
||||||
|
* "`rest()` guesses for 24.57 % of elements (3 807 of 15 493)" → **mine, and
|
||||||
|
overstated by 65 %.** The plateau test marks a **single-keyframe** element as
|
||||||
|
plateau-less because it has no adjacent pair — but its one pose is
|
||||||
|
unambiguously its rest. 1 502 of the 3 807 are those; the genuinely ambiguous
|
||||||
|
population is **2 305 (14.88 %)**.
|
||||||
|
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "`rest()` for a plateau-less element should be the last keyframe → refuted by
|
||||||
|
the sibling argument" → **that refutation is itself refuted, this time by
|
||||||
|
measurement.** Rendering under the rule and correlating against the live
|
||||||
|
captures: publisher splash +0.9600 → **+0.9982**, developer splash +0.9643 →
|
||||||
|
**+0.9758**. Making `palogo_anima_eff` invisible *improves* the match; the
|
||||||
|
sibling symmetry was my expectation, not evidence.
|
||||||
|
* "the port's exposure to the rest-guessing defect is 14 elements" → **two.** The
|
||||||
|
fallback needs an element to be plateau-less **and** multi-keyframe; title,
|
||||||
|
main menu and `EXTRAS` reach it **zero** times, the two splashes once each.
|
||||||
|
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "the shifted time reading implies rest = the last keyframe, so the plateau rule
|
||||||
|
can be dropped" → **mine, and refuted by measurement.** Applying it to every
|
||||||
|
element collapses all five screens (title 0.9500→0.6819, main menu
|
||||||
|
0.9460→0.6416, `EXTRAS` 0.9440→0.5745) and renders both splashes **blank**. A
|
||||||
|
group is entry → hold → exit and the exit is the screen's *dismissal*: a
|
||||||
|
displayed screen sits at the hold, not at its final pose.
|
||||||
|
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "`rest_plateau` renders elements the game has already finished with" (as a
|
||||||
|
general claim) → **narrowed by its control.** It holds on **transient** screens
|
||||||
|
only: suppressing the finished glows takes the two splashes from 0.9604/0.9659
|
||||||
|
to **0.9982/0.9980**, while the same edit costs the title 0.002, the main menu
|
||||||
|
**0.092** and `EXTRAS` **0.107**. A plateau mid-animation is evidence the
|
||||||
|
element is held at that point in the timeline, not that it is on screen once the
|
||||||
|
screen has settled — and where a screen does settle, the rule is right.
|
||||||
|
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||||
|
* "the group header's undecoded lead-in word carries a per-element start offset"
|
||||||
|
→ **refuted immediately.** It is `0x00000000` for all seven elements of the
|
||||||
|
developer splash — glows and logos alike — while those two families are
|
||||||
|
observed to run sequentially (frames 94–115 and 116–211) despite declaring
|
||||||
|
overlapping times. [`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||||
|
* "the splash elements share one clock origin" → **refuted.** Fitting a single
|
||||||
|
origin needs f₀ ≈ 93.5 for `palogo_gamearts_eff` and f₀ ≈ 103 for
|
||||||
|
`palogo_gamearts`, ~19 units apart, and aligning one throws the other off by
|
||||||
|
~9 frames at both ends. Durations match (97.8 %, 98.5 %); starts do not.
|
||||||
|
* "the glows and logos might overlap and my size-grouping merged them" → **tested
|
||||||
|
and refuted.** Across all 235 captured frames, **zero** contain both a glow and
|
||||||
|
a logo; f110–115 draw two glows and f116 onward two logos, with no transition
|
||||||
|
frame. The sequencing is real.
|
||||||
|
* "a bundle's declared elements are what the screen shows" → **refuted.** Entry 11
|
||||||
|
declares three logo/glow pairs and only two are ever drawn — `palogo_anima`
|
||||||
|
gets 0 frames against `palogo_gamearts`'s 95, from byte-identical keyframe
|
||||||
|
times. (Reach: within the capture's frames 1–214.)
|
||||||
|
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||||
|
* "the glow and logo phases are one bundle with elements selectively activated"
|
||||||
|
→ **mine, and withdrawn as unestablished.** The alternative — two compositions
|
||||||
|
shown in sequence — fits equally. The texture-base test fails its control: the
|
||||||
|
publisher splash is a different bundle and shares the base `0x11C30000`, so
|
||||||
|
that address is a reused upload slot, not a bundle identity. What survives is
|
||||||
|
that declared elements ≠ drawn elements.
|
||||||
|
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||||
|
* "two compositions shown in sequence" (as the alternative to selective
|
||||||
|
activation) → **refuted statically.** It requires a bundle declaring the glows
|
||||||
|
without the logos; no such bundle exists. Only four `GP_TITLE` entries carry
|
||||||
|
`palogo` elements, and both developer entries declare **all six** logos and
|
||||||
|
glows — so whichever was active, a subset of its elements was drawn at a time.
|
||||||
|
Selective activation is reinstated on evidence.
|
||||||
|
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||||
|
* ~~"the splash's `measured_paint_order` records a front-to-back depth order" →
|
||||||
|
mis-typed; between its glow and logo halves it records only the temporal order
|
||||||
|
they were seen in."~~ → **that refutation is itself refuted (same day).** The
|
||||||
|
vector is a read of the runtime **child array** (`ui-screen-runtime.md`:
|
||||||
|
"paint order (child slots)"), not of the draw capture, so co-occurrence does
|
||||||
|
not bear on it; and the halves carry *distinct* T8aD layer keys
|
||||||
|
(`0xa100` < `0xa110`, `paint_order_audit`: 0 same-key ties), so the file orders
|
||||||
|
them regardless. The no-overlap measurement was correct; the inference from it
|
||||||
|
was not. What survives: a capture of this screen can only cross-check the order
|
||||||
|
*within* each half. [`ui-prm-primitives.md`](structures/ui-prm-primitives.md)
|
||||||
|
|
||||||
|
* "`8AX` is the name a `T8aD` is registered under" → **it is not a name at all.**
|
||||||
|
It is three bytes of the preceding record's payload (`38 41 58`) that happen to
|
||||||
|
be printable ASCII, which our backwards printable-run scan preferred over the
|
||||||
|
name the format actually states in its `opt ` block. The claim sat in
|
||||||
|
`HANDOFF.md` and `ui-8ax-fullres-background.md` as though `8AX` were a real
|
||||||
|
identifier, and cost every menu screen its full-resolution background.
|
||||||
|
[`ratc-child-names.md`](structures/ratc-child-names.md)
|
||||||
|
* "`pmbase.t32` is on the disc nowhere" (the one dangling asset behind
|
||||||
|
`10 144 of 10 148 references resolve`) → **withdrawn; it is on the disc.** It is
|
||||||
|
the `GP_STAGE_CLEAR` child the same scan named `8AX`. With the name decoded the
|
||||||
|
count is **10 148 of 10 148**. [`ratc-child-names.md`](structures/ratc-child-names.md)
|
||||||
|
|||||||
583
docs/re/boot-config-and-gamepart-registry.md
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
# What the game reads at boot — `config.ini`, and which GameParts actually exist
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED` and **decoded** for the language selection. ❔ a bounded
|
||||||
|
**negative** for the boot *order*. 🟡 for what the registration strings imply
|
||||||
|
about the attract loop.
|
||||||
|
|
||||||
|
Contributes to [MISSION Q6](../port/MISSION.md), and firms up Q4's ids.
|
||||||
|
|
||||||
|
## ✅ `config.ini` — the disc's only config, and it selects the language
|
||||||
|
|
||||||
|
`config.ini` sits at the **disc root**, is 400 bytes, and is the **only** `.ini`,
|
||||||
|
`.cfg` or `.txt` anywhere on the disc. Its own header comment (Shift-JIS) names
|
||||||
|
it:
|
||||||
|
|
||||||
|
> `アプリケーション/ゲームパート初期設定テーブル`
|
||||||
|
> — *"Application / GamePart initial settings table"*
|
||||||
|
>
|
||||||
|
> `SYSTEM セクションには、ゲーム及び各ゲームパートで共通にアクセスする情報を記述する`
|
||||||
|
> — *"the SYSTEM section describes information accessed in common by the game and
|
||||||
|
> each game part"*
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[SYSTEM]
|
||||||
|
|
||||||
|
[LANGUAGE]
|
||||||
|
= eng ; default
|
||||||
|
#0x01 = eng ; XC_LANGUAGE_ENGLISH
|
||||||
|
#0x02 = jpn ; XC_LANGUAGE_JAPANESE
|
||||||
|
#0x03 = deu ; XC_LANGUAGE_GERMAN
|
||||||
|
#0x04 = fra ; XC_LANGUAGE_FRENCH
|
||||||
|
#0x05 = esp ; XC_LANGUAGE_SPANISH
|
||||||
|
#0x06 = ita ; XC_LANGUAGE_ITALIAN
|
||||||
|
```
|
||||||
|
|
||||||
|
**This is the mechanism behind the EN/JP screen pairs.** The console's
|
||||||
|
`XC_LANGUAGE_*` setting selects a three-letter code, and that code is what picks
|
||||||
|
`GP_TITLE`'s English or Japanese build ([`ui-title-build-map.md`](ui-title-build-map.md))
|
||||||
|
and the `<lang>.pak` families. Default is `eng`, keyed by the empty line — an
|
||||||
|
unlisted language falls back to English. ✅ decoded. The executable does read the
|
||||||
|
file: the string `config.ini` is at `.rdata 0x82062b44`.
|
||||||
|
|
||||||
|
## ❔ But the boot ORDER is not in it — and this is the whole search space
|
||||||
|
|
||||||
|
`[SYSTEM]` — the section the file's own comment says holds what the game and every
|
||||||
|
game part share — is **empty**. So the file the game itself calls the *GamePart
|
||||||
|
initial settings table* says nothing about which part runs first or what follows
|
||||||
|
what.
|
||||||
|
|
||||||
|
**Reach of the negative:** this is the only config file on the disc (one `find`
|
||||||
|
over the whole extract). The boot order is therefore not in disc-side
|
||||||
|
configuration at all; it is in code, or in a table inside the executable that has
|
||||||
|
not been located.
|
||||||
|
|
||||||
|
## 🟡 `GP_ADVERTISE_DEMO` is never registered — the attract loop is not its own part
|
||||||
|
|
||||||
|
The executable carries one diagnostic string per GamePart registration site
|
||||||
|
(`silph::GamePartTask::RegisterToFactory<N, class silph::GamePart_X>::RegisterToFactory is failed!`)
|
||||||
|
— the same evidence the corpus used to pin the id table
|
||||||
|
([`challenge-mission-gate.md`](challenge-mission-gate.md) §3). Extracting all of
|
||||||
|
them gives **24 of the 29 ids bound to a C++ class**; full list in
|
||||||
|
[`data/gamepart-class-ids.txt`](data/gamepart-class-ids.txt).
|
||||||
|
|
||||||
|
**The five ids with no registration site are `1`, `2`, `16`, `18`, `28`** — in the
|
||||||
|
id table's naming, `GP_ADVERTISE_DEMO`, `GP_SELECT_STORAGE`, `GP_DEMO`,
|
||||||
|
`GP_SELECTOR`, `GP_TEST`.
|
||||||
|
|
||||||
|
That `GP_ADVERTISE_DEMO` (1) is among them matters for Q6, and it agrees with
|
||||||
|
what was measured: the attract loop is **the title screen replaying `ADV.wmv`**
|
||||||
|
([`movie-binding.md`](movie-binding.md)), not a transition into a separate
|
||||||
|
advertise part. The id table names a part the shipped build never registers.
|
||||||
|
|
||||||
|
🟡 **not ✅**, because this is an argument from a *diagnostic string*: no error
|
||||||
|
message for id 1 implies no registration site for id 1. That is how the corpus
|
||||||
|
already reads these strings, and it is sound, but it is not the code.
|
||||||
|
|
||||||
|
### Two bonuses for Q4
|
||||||
|
|
||||||
|
* **`3` and `4` are the same class** — `GamePart_SaveLoad` is registered twice.
|
||||||
|
The id table's separate `GP_LOAD` / `GP_SAVE` names are two ids on one part.
|
||||||
|
* The ids behind the menu buttons now match the executable's own **class** names,
|
||||||
|
not just a list of table names: `GamePart_Options` = 8, `GamePart_Tutorial` =
|
||||||
|
25, `GamePart_Extras` = 5, `GamePart_MissionSelect` = 7,
|
||||||
|
`GamePart_MovieTheater` = 6, `GamePart_Title` = 0. Still a **name match** —
|
||||||
|
screen title ↔ class name — but anchored one level closer to the code.
|
||||||
|
|
||||||
|
## What is still missing for Q6
|
||||||
|
|
||||||
|
The *transitions*. Nothing found so far says "title, then movie, then title" — the
|
||||||
|
manifest gives the boot-side **assets** in play order
|
||||||
|
([`movie-binding.md`](movie-binding.md)), `config.ini` gives the **language**, and
|
||||||
|
the registry gives **which parts exist**. What decides to advance is in
|
||||||
|
`GamePart_Title`'s own code, and reading it is a static PPC job that has not been
|
||||||
|
started.
|
||||||
|
|
||||||
|
## ❔ The transitions are code, not data — the reach of that negative
|
||||||
|
|
||||||
|
Q6's remaining half asked what the game *reads* to decide the boot order. The
|
||||||
|
answer is: **nothing. It is not data-driven.** Four independent places were
|
||||||
|
checked, and the sequence is in none of them.
|
||||||
|
|
||||||
|
| looked in | result |
|
||||||
|
|---|---|
|
||||||
|
| **disc configuration** | `config.ini` is the only config file on the disc, its `[SYSTEM]` section is empty (above) |
|
||||||
|
| **the movie manifest** | carries the boot-side *assets* in play order, and no transitions — [`movie-binding.md`](movie-binding.md) |
|
||||||
|
| **a persistent part-id field** | already refuted: the requested GamePart id exists **only as a stack argument in flight**, with no literal store anywhere — [`challenge-mission-gate.md`](challenge-mission-gate.md) §5 |
|
||||||
|
| **the id table's attract entry** | the string `GP_ADVERTISE_DEMO` at `0x820a1fe8` has **zero xrefs of any kind**; nothing in the code reads it |
|
||||||
|
|
||||||
|
So a transition is a **call with an id argument**, chosen by code. There is no
|
||||||
|
table to read and nothing to poke.
|
||||||
|
|
||||||
|
**Where that code is, as far as it was traced.** The `RegisterToFactory<0, class
|
||||||
|
silph::GamePart_Title>` diagnostic string at `0x820a3d60` is referenced from
|
||||||
|
exactly one place, `sub_8280E148` — the registration site — which also takes the
|
||||||
|
address of **`sub_821C7D98`** (`addi`), the position a factory template puts its
|
||||||
|
creator. 🟡 That identification is by position and convention, not proven;
|
||||||
|
`sub_821C7D98` itself has **0 `.rdata` references**, consistent with a small
|
||||||
|
`new`+ctor thunk rather than the state machine. The substantial function in the
|
||||||
|
same class neighbourhood is `sub_821C6458` (4 460 bytes, has EH, 15 `.rdata`
|
||||||
|
refs), and **reading it has not been attempted**.
|
||||||
|
|
||||||
|
### What this means for the port
|
||||||
|
|
||||||
|
The boot sequence is **authored**, not transcribed — and that is fine, because the
|
||||||
|
sequence itself is measured end to end:
|
||||||
|
|
||||||
|
```
|
||||||
|
developer splash (a RATC screen, not a video)
|
||||||
|
→ ADV.wmv
|
||||||
|
→ title + PRESS Ⓐ ──idle ~8–10 s──> fade to black → ADV.wmv in full → title
|
||||||
|
→ Ⓐ → main menu
|
||||||
|
```
|
||||||
|
|
||||||
|
with the fade-through-black timings in [`screen-transitions.md`](screen-transitions.md)
|
||||||
|
and Ⓑ from the main menu returning to the title.
|
||||||
|
|
||||||
|
|
||||||
|
## 🟡 `sub_821C6458` read — the title's states are NAMED in the executable
|
||||||
|
|
||||||
|
The page above left this function as the named next step: "the substantial
|
||||||
|
function in `GamePart_Title`'s neighbourhood, and **reading it has not been
|
||||||
|
attempted**". It has now been looked at — not disassembled line by line, but
|
||||||
|
characterised, which is enough to sharpen Q6.
|
||||||
|
|
||||||
|
**It is the title part's screen-state function.** 4 460 bytes, has EH, and called
|
||||||
|
from **exactly one** place — `sub_821C7850`, which sits in the same neighbourhood
|
||||||
|
as the creator the registration site points at (`sub_821C7D98`). It makes 33
|
||||||
|
distinct calls.
|
||||||
|
|
||||||
|
**What it names.** Its `.rdata` string references are:
|
||||||
|
|
||||||
|
| string | at |
|
||||||
|
|---|---|
|
||||||
|
| **`TITLE_SCREEN`** | `0x820a3d3c` |
|
||||||
|
| **`TITLE_MENU`** | `0x820a3d30` |
|
||||||
|
| **`LOADING`** | `0x820a214c` |
|
||||||
|
| `BASE_INFO` (×2) | `0x820a20ec` |
|
||||||
|
|
||||||
|
Those are the states measured off the running game, in the game's own words: the
|
||||||
|
title carrying `PRESS Ⓐ BUTTON`, the five-button menu, and a loading state. And
|
||||||
|
`BASE_INFO` is this corpus's own marker for a **screen-config lookup** rather than
|
||||||
|
a table read ([`REFUTED.md`](REFUTED.md), "`BASE_INFO` discriminates
|
||||||
|
screen-config from table-read, 9/9 vs 10/10").
|
||||||
|
|
||||||
|
### The sharper negative
|
||||||
|
|
||||||
|
So the title part **does ask for configuration keyed by `TITLE_SCREEN` and
|
||||||
|
`TITLE_MENU`** — and [`config.ini`](#-configini--the-discs-only-config-and-it-selects-the-language),
|
||||||
|
the disc's only config file, contains **no such sections**. Its only sections are
|
||||||
|
an empty `[SYSTEM]` and `[LANGUAGE]`. The lookups find nothing, and the values are
|
||||||
|
whatever the code defaults to.
|
||||||
|
|
||||||
|
That is a better answer than "the order is not in config": the game *asks the
|
||||||
|
question*, the shipped disc *does not answer it*, and the defaults live in code.
|
||||||
|
For the port it means the state **names** are transcribable even though their
|
||||||
|
contents are not.
|
||||||
|
|
||||||
|
🟡 **Not ✅.** This is a characterisation from string references and call counts,
|
||||||
|
not a read of the control flow. Two self-references (`0x821c6498`, `0x821c6b7c`)
|
||||||
|
inside the function look like jump tables — a switch, which is what a state
|
||||||
|
machine compiles to — but that was **not confirmed**, and nothing here shows which
|
||||||
|
state leads to which.
|
||||||
|
|
||||||
|
### 🔴 Refuted on the way, because it looked like a find
|
||||||
|
|
||||||
|
The words at `0x820a3b48`… resolve as neat `{func, func, ptr}` triples and read
|
||||||
|
convincingly as a state/handler table. **They are not.** The bytes immediately
|
||||||
|
before them are the tail of
|
||||||
|
`…SaveLoad>::RegisterToFactory is failed!`, and the `0x8210c1xx` targets are
|
||||||
|
zero-filled descriptors — static-initialiser records trailing the registration
|
||||||
|
strings, not a dispatch table. A plausible-looking array of function pointers next
|
||||||
|
to relevant strings is not evidence of anything until its neighbours are read.
|
||||||
|
|
||||||
|
|
||||||
|
## 🟡 The title's transition is a screen lookup BY NAME — `sub_821CC860`
|
||||||
|
|
||||||
|
Reading the code around each of the three state names shows the **same four
|
||||||
|
instructions** at all three sites:
|
||||||
|
|
||||||
|
```
|
||||||
|
lwz r29, 20(r30) ; an object off the part
|
||||||
|
lwz r3, 4(...) ; ...
|
||||||
|
bl 0x822F2328
|
||||||
|
lis r11, 0x820A
|
||||||
|
li r6, 0
|
||||||
|
lwz r4, 24(r30)
|
||||||
|
addi r5, r11, 15676 ; "TITLE_SCREEN" (15664 = "TITLE_MENU",
|
||||||
|
lwz r3, 88(r30) ; 8524 = "LOADING")
|
||||||
|
bl 0x821CC860 ; <- lookup(this+88, this+24, NAME, 0)
|
||||||
|
mr r4, r3
|
||||||
|
bl 0x82187B78 ; <- install the result
|
||||||
|
```
|
||||||
|
|
||||||
|
So a title-side transition is **`sub_821CC860(…, "<NAME>", 0)` followed by
|
||||||
|
`sub_82187B78(result)`** — a *string-keyed* screen lookup, then an install. Not a
|
||||||
|
numeric id, not a table index. That also fits `GP_ADVERTISE_DEMO` having zero
|
||||||
|
xrefs: at this level the screen graph is keyed by **name**, not by GamePart id.
|
||||||
|
|
||||||
|
### The candidate name vocabulary
|
||||||
|
|
||||||
|
`sub_821CC860` is called from **28 distinct functions**. Collecting the
|
||||||
|
upper-case identifier strings those callers reference gives 35 names, and they
|
||||||
|
split into two obvious families:
|
||||||
|
|
||||||
|
* **screen/state names** — `TITLE_SCREEN`, `TITLE_MENU`, `LOADING`,
|
||||||
|
**`DIFFICULTY`**, `EXTRA_MENU`, `TUTORIAL_MENU`, `STANDARD_MENU`, `DEBRIEFING`,
|
||||||
|
`CHALLENGE`, `MISSIONS`, `LIVE_BOARD`, `LOCAL_BOARD`, `EXTRA_MENU`;
|
||||||
|
* **config keys** — `TEXT_FONT`, `TEXT_HEIGHT`, `LINE_SPACE`, `GAMMA_RGB`,
|
||||||
|
`GAMMA_WB`, `TEXT_SPEED_PER_LETTER`, `EXIT_VALUE`, `INPUT_DIR`,
|
||||||
|
`MENU_ENABLE_SKIP`/`_DISABLE_SKIP`, `PAUSE_SE`, `JINGLE`, …
|
||||||
|
|
||||||
|
plus `BASE_INFO` in **19 of the 28** callers, which is this corpus's existing
|
||||||
|
marker for a screen-config function.
|
||||||
|
|
||||||
|
**Three of the screen names are independently corroborated by measurement**:
|
||||||
|
`DIFFICULTY` is exactly the screen `NEW GAME` opens, `EXTRA_MENU` matches the
|
||||||
|
`EXTRAS` submenu and `TUTORIAL_MENU` the lesson list — all three measured off the
|
||||||
|
running game in [`menu-navigation-semantics.md`](menu-navigation-semantics.md)
|
||||||
|
before this function was ever looked at.
|
||||||
|
|
||||||
|
⚠️ **Why this is 🟡 and not ✅.** The 35 strings are what those callers
|
||||||
|
*reference*, **not** proven arguments to `sub_821CC860` — the list plainly mixes
|
||||||
|
screen names with config keys, so it is a **candidate vocabulary**, not a decoded
|
||||||
|
one. Confirming it means checking, per call site, which string actually lands in
|
||||||
|
`r5`. That was not done.
|
||||||
|
|
||||||
|
### What is still unread
|
||||||
|
|
||||||
|
Which state leads to which. The three lookups sit in different branches of one
|
||||||
|
function and at least one (`TITLE_MENU`) is guarded by a `cmplwi`/`bne`, but the
|
||||||
|
branch structure was not traced, so the *order* still comes from measurement, not
|
||||||
|
from the code.
|
||||||
|
|
||||||
|
|
||||||
|
## ✅ The argument is now decoded — and it refutes my own corroboration
|
||||||
|
|
||||||
|
The section above listed a **candidate** vocabulary from "upper-case strings the
|
||||||
|
callers reference", flagged 🟡 because those are not proven arguments, and named
|
||||||
|
the check: *confirm per call site which string actually lands in `r5`.* That check
|
||||||
|
has been run, and it fired.
|
||||||
|
|
||||||
|
**Method.** Forward register simulation over the 100 instructions before each of
|
||||||
|
the **48** call sites of `sub_821CC860`, tracking `lis` / `addi` / `subi` / `mr`.
|
||||||
|
Full table in [`data/name-lookup-callsites.txt`](data/name-lookup-callsites.txt).
|
||||||
|
|
||||||
|
**Gated on a control, and the first version failed it.** A backward scan for
|
||||||
|
`addi r5, …` recovered only 7/48 and **missed both title sites I had read by
|
||||||
|
eye** — because there the name goes into `r27` first and reaches `r5` via a later
|
||||||
|
`mr`. The rewritten forward simulation reproduces all three known sites
|
||||||
|
(`TITLE_SCREEN`, `TITLE_MENU`, `LOADING`) and then recovers **46 of 48**.
|
||||||
|
|
||||||
|
### 🔴 `DIFFICULTY` and `EXTRA_MENU` are *not* arguments
|
||||||
|
|
||||||
|
Last section I wrote that three names were "independently corroborated by
|
||||||
|
measurement": `DIFFICULTY`, `EXTRA_MENU`, `TUTORIAL_MENU`. **Only
|
||||||
|
`TUTORIAL_MENU` survives.** `DIFFICULTY` and `EXTRA_MENU` never appear in `r5` at
|
||||||
|
any of the 48 sites — they are strings that merely live in the same functions.
|
||||||
|
The corroboration I claimed was an artifact of the loose filter, and the 🟡 I put
|
||||||
|
on it is exactly what caught it.
|
||||||
|
|
||||||
|
### The 28 names that ARE passed
|
||||||
|
|
||||||
|
```
|
||||||
|
BASE BASE(x3) BG(x3) BLACK(x2) BUTTON DEBRIEFING DETAIL EXTRA FADE(x3)
|
||||||
|
FILE(x5) GAME GAMMA_RGB GAMMA_TITLE INFO KEY LIVE_BOARD(x2) LOADING(x2)
|
||||||
|
LOCAL_BOARD(x2) MENU(x2) MENU_DISABLE_SKIP MESSAGE NEW_ITEM(x2) PAD SOUND
|
||||||
|
TITLE(x2) TITLE_MENU TITLE_SCREEN(x2) TUTORIAL_MENU WINDOW
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 So `sub_821CC860` is not a screen factory
|
||||||
|
|
||||||
|
The previous section called it one. The real argument list says otherwise:
|
||||||
|
`BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`, `WINDOW`, `GAMMA_RGB`,
|
||||||
|
`MENU_DISABLE_SKIP` are **not screens**. It is a **generic name-keyed lookup** —
|
||||||
|
a named-entry getter used for config and resources throughout the executable, of
|
||||||
|
which the title part happens to call it with its three state names.
|
||||||
|
|
||||||
|
That weakens nothing about the title finding itself — `sub_821C6458` really does
|
||||||
|
transition by calling this with `TITLE_SCREEN` / `TITLE_MENU` / `LOADING` — but it
|
||||||
|
removes the inference that the 28 names are a screen vocabulary. They are a
|
||||||
|
**lookup-key vocabulary**, mostly config.
|
||||||
|
|
||||||
|
|
||||||
|
## ✅ The title's state machine — decoded
|
||||||
|
|
||||||
|
The last open piece of Q6 was *which state leads to which*. It is a plain
|
||||||
|
`switch` and it is now read.
|
||||||
|
|
||||||
|
```
|
||||||
|
821c6474 lwz r11, 136(r30) ; state = this+0x88
|
||||||
|
821c6478 cmplwi cr6, r11, 0x9 ; ten states, 0..9
|
||||||
|
821c647c bgt cr6, <default>
|
||||||
|
821c6480 lis/addi r12, 0x821C6498 ; jump table
|
||||||
|
821c648c lwzx r0, r12, r0
|
||||||
|
821c6494 bctr
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ The table at `0x821C6498` disassembles as ten `lwz r16, N(r28)` instructions.
|
||||||
|
**It is data.** That is the "self-reference" this page flagged two sections ago as
|
||||||
|
*looking* like a jump table — it is one, and the disassembler was decoding its
|
||||||
|
words as code.
|
||||||
|
|
||||||
|
**Three of the ten states install a named screen**: **0 → `TITLE_SCREEN`**,
|
||||||
|
**2 → `TITLE_MENU`**, **8 → `LOADING`**.
|
||||||
|
|
||||||
|
**Eighteen transitions**, every one a literal `li rX, N ; stw rX, 136(r30)`:
|
||||||
|
|
||||||
|
```
|
||||||
|
0 → 1, 2 2 → 4 4 → 0, 5, 8×4 6 → 7, 9, 2 8 → 2
|
||||||
|
1 → 2, 2 3 → 4 5 → 6 7 → 9 9 → (none)
|
||||||
|
```
|
||||||
|
|
||||||
|
Full table with addresses in
|
||||||
|
[`data/title-state-machine.txt`](data/title-state-machine.txt).
|
||||||
|
|
||||||
|
### 🟡 It lines up with what was measured — read as corroboration, not proof
|
||||||
|
|
||||||
|
The boot reaches the title (state 0) and Ⓐ opens the main menu (state 2); the
|
||||||
|
graph has `0 → 2` directly and `0 → 1 → 2`. Ⓑ at the main menu returns to the
|
||||||
|
title, and `4 → 0` is the **only** edge back to state 0, reached from `2 → 4`.
|
||||||
|
Entering a submenu goes through `LOADING` and comes back, and `4 → 8` (four
|
||||||
|
separate sites) then `8 → 2` is exactly that shape.
|
||||||
|
|
||||||
|
Those readings are **mine, matching a graph to observed behaviour** — the
|
||||||
|
conditions on the edges are not decoded, so nothing here proves which input picks
|
||||||
|
which branch.
|
||||||
|
|
||||||
|
### What is still not decoded
|
||||||
|
|
||||||
|
* **the condition on each edge** — which input or event selects it;
|
||||||
|
* **states 1, 3, 5, 6, 7, 9** install no named screen, so what they do is unknown;
|
||||||
|
* **state 3 is never a destination** in this function, so something outside sets
|
||||||
|
it.
|
||||||
|
|
||||||
|
|
||||||
|
## ✅ The edge conditions, for the state that has them — an EVENT CODE
|
||||||
|
|
||||||
|
The previous section left "the condition on each edge" open. For the state that
|
||||||
|
matters it is now read.
|
||||||
|
|
||||||
|
**`sub_821C6458`'s third argument is an event code.** State 4 — reached from
|
||||||
|
`2 → 4`, i.e. immediately after the main menu is installed — is the
|
||||||
|
**input-waiting state**, and it dispatches on that argument:
|
||||||
|
|
||||||
|
```
|
||||||
|
821c6b5c cmplwi cr6, r27, 0x19 ; 26 events, 0..25
|
||||||
|
821c6b60 bgt cr6, <default>
|
||||||
|
821c6b68 addi r12, r12, 27516 ; second jump table, at 0x821c6b7c
|
||||||
|
821c6b78 bctr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Six of the 26 are handled**; the other twenty fall through and change nothing.
|
||||||
|
|
||||||
|
| event | → state | |
|
||||||
|
|---|---|---|
|
||||||
|
| **0** | **0** | `TITLE_SCREEN` — back to the title |
|
||||||
|
| 3 | 8 | `LOADING` |
|
||||||
|
| 5 | 8 | `LOADING` |
|
||||||
|
| 8 | 8 | `LOADING` |
|
||||||
|
| 10 | 5 | — |
|
||||||
|
| 25 | 8 | `LOADING` |
|
||||||
|
|
||||||
|
So last section's edges `4 → 0, 5, 8×4` are each now attributed to a specific
|
||||||
|
event, and the shape of the state graph is complete for the input state.
|
||||||
|
|
||||||
|
### 🟡 A correspondence worth noticing, and not more than that
|
||||||
|
|
||||||
|
The main menu has five items, Ⓑ returns to the title, and this table has **one
|
||||||
|
event to state 0, four to `LOADING`, one elsewhere**. It is tempting to read that
|
||||||
|
as *Ⓑ = event 0, four items load an external archive, `EXTRAS` stays inside
|
||||||
|
`GP_TITLE`* — which would match everything measured.
|
||||||
|
|
||||||
|
**That is a count-match, not a decode.** The event numbers are not named, nothing
|
||||||
|
here shows event 3 is a particular menu row, and state 5 installs no named screen,
|
||||||
|
so the `EXTRAS` half of the story has no support at all. Recorded as an
|
||||||
|
observation so the next person sees the shape; it is **not** a button→event map.
|
||||||
|
|
||||||
|
### What is still not decoded
|
||||||
|
|
||||||
|
* **what the event numbers mean** — button id, menu-item index, or message id;
|
||||||
|
* **conditions on edges out of the other states** (`0 → 1` vs `0 → 2`, `6 → 7/9/2`);
|
||||||
|
* states 1, 3, 5, 6, 7, 9, which install no named screen.
|
||||||
|
|
||||||
|
|
||||||
|
## 🔴 Correcting my own framing: `sub_821C6458` is ONE PHASE, not the whole part
|
||||||
|
|
||||||
|
Chasing where the event code comes from turned up the level above, and it revises
|
||||||
|
what the previous three sections called "the title part's state machine".
|
||||||
|
|
||||||
|
`sub_821C6458` has exactly one caller, `sub_821C7850`, and that caller is itself a
|
||||||
|
dispatcher — on a **second, outer state field**:
|
||||||
|
|
||||||
|
```
|
||||||
|
821c786c lwz r11, 16(r30) ; only runs at all when this+16 == 3
|
||||||
|
821c7874 bne cr6, <exit>
|
||||||
|
821c787c lwz r11, 132(r30) ; phase = this+0x84
|
||||||
|
821c7880 cmplwi cr6, r11, 0x4 ; FIVE phases, 0..4
|
||||||
|
821c789c bctr ; table at 0x821C78A0
|
||||||
|
```
|
||||||
|
|
||||||
|
| phase | handler | |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | `sub_821C5690` | **the splash** — independently identified as the splash mechanics in the `iterate3E` notes, before any of this |
|
||||||
|
| 1 | inline at `0x821c790c` | |
|
||||||
|
| 2 | `sub_821C5818` | |
|
||||||
|
| 3 | `sub_821C5EC0` | |
|
||||||
|
| **4** | **`sub_821C6458`** | the title/menu machine — `TITLE_SCREEN` / `TITLE_MENU` / `LOADING` |
|
||||||
|
|
||||||
|
So `GamePart_Title` has **two nested state fields**: a phase at `this+132` (five
|
||||||
|
values) selecting which sub-machine runs, and the state at `this+136` (ten values)
|
||||||
|
*inside phase 4*. Everything the previous sections decoded — the ten states, the
|
||||||
|
eighteen edges, the event dispatch — is **phase 4 only**. Phases 0–3 are
|
||||||
|
untouched, and one of them is the developer splash.
|
||||||
|
|
||||||
|
That phase 0 lands on the function the corpus had already fingered as the splash,
|
||||||
|
from a completely different direction, is the useful check here.
|
||||||
|
|
||||||
|
### And the event code is forwarded, not created
|
||||||
|
|
||||||
|
All five phase handlers are called as `(this, r29, r28)` with the *same*
|
||||||
|
arguments, and `r28` is `sub_821C7850`'s own third argument, passed through
|
||||||
|
untouched. So the event vocabulary is defined **at least one level above** this
|
||||||
|
function. Finding what `3`, `5`, `8`, `10`, `25` mean means going up again, and
|
||||||
|
that was not done.
|
||||||
|
|
||||||
|
|
||||||
|
## Going up one more level — three of four callers pass a constant event
|
||||||
|
|
||||||
|
`sub_821C7850` has **four** direct callers. Recovering the `r5` argument at each:
|
||||||
|
|
||||||
|
| caller | event passed |
|
||||||
|
|---|---|
|
||||||
|
| `sub_821C7CB8` | **0** (→ back to the title) |
|
||||||
|
| `sub_821C7BA0` | **0** |
|
||||||
|
| `sub_821C47A0` | **5** (→ `LOADING`) |
|
||||||
|
| `sub_821C5580` | **not constant** — `lwz r5, 4(r27)`, read out of a structure |
|
||||||
|
|
||||||
|
So the vocabulary is still not enumerable from here: the interesting caller reads
|
||||||
|
its event from a field. ❔ What `3`, `8`, `10` and `25` mean remains open.
|
||||||
|
|
||||||
|
**`sub_821C5580` is also where the outer gate is set:**
|
||||||
|
|
||||||
|
```
|
||||||
|
821c5640 li r11, 3
|
||||||
|
821c5644 stw r11, 16(r28) ; this+16 = 3 — the exact value sub_821C7850 tests
|
||||||
|
821c5650 lwz r5, 4(r27) ; event from a struct field
|
||||||
|
821c5658 bl 0x821C7850
|
||||||
|
```
|
||||||
|
|
||||||
|
That answers the "what does `this+16 == 3` gate on" question from the previous
|
||||||
|
section: this function arms it.
|
||||||
|
|
||||||
|
### 🎁 And it names the title's music
|
||||||
|
|
||||||
|
The same function, a few instructions earlier:
|
||||||
|
|
||||||
|
```
|
||||||
|
821c560c li r5, 1103
|
||||||
|
821c5610 li r4, 4
|
||||||
|
821c561c lwz r3, 0(r29)
|
||||||
|
821c5620 bl 0x8217ACF8 ; a sound-play call
|
||||||
|
```
|
||||||
|
|
||||||
|
**1103 is a BGM cue id** — `BGM_103`. That closes an open residual from Q8/Q10
|
||||||
|
("which bank is the menu's music is not on the disc"), and it checks out three
|
||||||
|
ways; see [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md).
|
||||||
|
|
||||||
|
|
||||||
|
## The other phases, characterised — and phase 0 confirms the splash twice over
|
||||||
|
|
||||||
|
Same treatment for the phase handlers that were still unread. Strings each one
|
||||||
|
references, and whether it carries a jump table of its own:
|
||||||
|
|
||||||
|
| phase | handler | size | strings referenced | own switch |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 0 | `sub_821C5690` | 380 | **`LOGO`** | none |
|
||||||
|
| 2 | `sub_821C5818` | 1 576 | `BASE_INFO`, **`BUTTON`**, **`TITLE_SCREEN`** | none |
|
||||||
|
| 3 | `sub_821C5EC0` | 1 220 | *(none)* | one, `bctr` at `0x821c5ef8` |
|
||||||
|
| 4 | `sub_821C6458` | 4 460 | `BASE_INFO`, `LOADING`, `TITLE_MENU`, `TITLE_SCREEN` | two (known) |
|
||||||
|
|
||||||
|
**Phase 0 references `LOGO`** — a second, independent confirmation that it is the
|
||||||
|
developer splash. The `iterate3E` notes reached `sub_821C5690` from the guest side
|
||||||
|
and named the splash's `LOGO` items; this reaches the same function from the
|
||||||
|
registration site and finds the same string.
|
||||||
|
|
||||||
|
**Phase 2 draws the title *with* the `PRESS Ⓐ BUTTON` plate** — it references
|
||||||
|
`TITLE_SCREEN` and `BUTTON`, and [`ui-title-build-map.md`](ui-title-build-map.md)
|
||||||
|
established from the archive that the plate is its own build (2/3), composited
|
||||||
|
over the title art.
|
||||||
|
|
||||||
|
### 🟡 A hypothesis for a puzzle this corpus has had open for months
|
||||||
|
|
||||||
|
**The title is installed from two different places**: phase 2, and phase 4's
|
||||||
|
state 0. Same screen, different code.
|
||||||
|
|
||||||
|
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) has long
|
||||||
|
recorded, and never explained, that *the title which ends the boot accepts Ⓐ while
|
||||||
|
the title the attract loop returns to accepts nothing* — with the giveaway that a
|
||||||
|
draw capture in each state is identical, 13 quads at the same rects. Two code
|
||||||
|
paths installing the same screen is exactly the shape that would produce that.
|
||||||
|
|
||||||
|
**Candidate:** the boot title is **phase 2**; the attract-returned title is
|
||||||
|
**phase 4, state 0**. Only phase 2 wires up the Ⓐ handling.
|
||||||
|
|
||||||
|
⚠️ **Tested, and dead.** Ⓑ from the main menu is the decoded edge `4 → 0`, so the
|
||||||
|
Ⓑ-returned title *is* phase 4 state 0 — and **Ⓐ works there**, opening the main
|
||||||
|
menu, with Ⓐ on the boot title as the control in the same run. Phase 4 state 0
|
||||||
|
handles Ⓐ, so the two-code-paths explanation is refuted. See
|
||||||
|
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md), which the
|
||||||
|
test did narrow usefully: only the **attract**-returned title is inert, not every
|
||||||
|
title after the first.
|
||||||
|
|
||||||
|
### ⚠️ One query in this section failed its own control
|
||||||
|
|
||||||
|
Counting `stw rX, 136(r30)` per phase returned **0 for phase 4**, which is known
|
||||||
|
to have 18 — the operand text is `r11, 136(r30)` and the pattern did not allow the
|
||||||
|
space. The per-phase store counts were discarded. The `bctr` half *does* pass its
|
||||||
|
control (phase 4 shows both known tables) and is reported above.
|
||||||
|
|
||||||
|
|
||||||
|
## ✅ The phase transitions — and they invalidate yesterday's test
|
||||||
|
|
||||||
|
Nine stores to the phase field `this+132` across the `GamePart_Title`
|
||||||
|
neighbourhood, attributed to the handler each lives in (control: the same query
|
||||||
|
shape finds 30 stores to `this+136`, ≥ the 18 known in phase 4):
|
||||||
|
|
||||||
|
| store | sets phase | lives in |
|
||||||
|
|---|---|---|
|
||||||
|
| `0x821c4fbc` | 2 | outside the handlers (entry/setup) |
|
||||||
|
| `0x821c4fe0`, `0x821c4fec` | 4 | outside the handlers |
|
||||||
|
| `0x821c5010` | *(not an immediate)* | outside the handlers |
|
||||||
|
| `0x821c592c` | **0** | phase 2 |
|
||||||
|
| `0x821c5b00` | **3** | phase 2 |
|
||||||
|
| `0x821c6370` | **4** | phase 3 |
|
||||||
|
| **`0x821c6e04`** | **2** | **phase 4, in the event-0 block** |
|
||||||
|
| `0x821c7950` | 2 | the dispatcher |
|
||||||
|
|
||||||
|
```
|
||||||
|
entry → 2 2 → 0 (splash) 2 → 3 3 → 4 4 → 2 (event 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔴 Withdrawing the refutation from the previous iteration
|
||||||
|
|
||||||
|
The event-0 block sets **both** fields, one instruction apart:
|
||||||
|
|
||||||
|
```
|
||||||
|
821c6df8 li r11, 2
|
||||||
|
821c6e00 stw r28, 136(r30) ; state = 0
|
||||||
|
821c6e04 stw r11, 132(r30) ; phase = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
So **Ⓑ from the menu lands in phase 2**, not phase 4 state 0. The previous section
|
||||||
|
tested "Ⓐ on the Ⓑ-returned title" believing it was probing phase 4 state 0 —
|
||||||
|
**it was probing phase 2**, the same phase as the boot title.
|
||||||
|
|
||||||
|
The test therefore says nothing about phase 4 state 0, and the refutation it
|
||||||
|
produced is **withdrawn**. Worse for me and better for the idea: Ⓐ working there
|
||||||
|
is exactly what the hypothesis predicts, since phase 2 is the phase that
|
||||||
|
references `BUTTON`.
|
||||||
|
|
||||||
|
**Status of the hypothesis: untested, and now consistent with two observations**
|
||||||
|
(Ⓐ works on the boot title and on the Ⓑ-returned title, both phase 2). What it
|
||||||
|
still needs is the **attract**-returned title's phase — the one thing no test so
|
||||||
|
far has read.
|
||||||
@@ -643,3 +643,58 @@ Two lessons, both cheap:
|
|||||||
The evidence recovered cleanly once the interference stopped — the interrupted
|
The evidence recovered cleanly once the interference stopped — the interrupted
|
||||||
batch's own run 3 had already reached the menu with the same 40 calls / 6
|
batch's own run 3 had already reached the menu with the same 40 calls / 6
|
||||||
`ResolvePath` signature.
|
`ResolvePath` signature.
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-08-28 — a candidate explanation, from the static side
|
||||||
|
|
||||||
|
This page's oldest open question is **why the attract-returned title accepts no
|
||||||
|
input while the boot title does**, given that a draw capture in each state is
|
||||||
|
identical (13 quads, same rects).
|
||||||
|
|
||||||
|
`GamePart_Title` turns out to dispatch on an outer **phase** field at `this+132`,
|
||||||
|
and **the title screen is installed from two different phases** — phase 2
|
||||||
|
(`sub_821C5818`, which references `TITLE_SCREEN` *and* `BUTTON`) and phase 4's
|
||||||
|
state 0 (`sub_821C6458`). Same screen, different code. See
|
||||||
|
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md).
|
||||||
|
|
||||||
|
**Candidate:** the boot title is phase 2; the attract-returned title is phase 4
|
||||||
|
state 0; only one of them wires up Ⓐ.
|
||||||
|
|
||||||
|
### ⚠️ Tested the same day, and the TEST was invalid — see the withdrawal below
|
||||||
|
|
||||||
|
There was a cheaper test than reading `this+132`: the state graph says Ⓑ from the
|
||||||
|
main menu goes `4 → 0`, so **the Ⓑ-returned title *is* phase 4 state 0**. If phase
|
||||||
|
4 state 0 were the inert one, Ⓐ would not work there.
|
||||||
|
|
||||||
|
**It works.** Measured:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| *control* — Ⓐ on the boot title | → main menu ✅ |
|
||||||
|
| Ⓐ on the **Ⓑ-returned** title | → **main menu** ✅ |
|
||||||
|
|
||||||
|
**🔴 Withdrawn.** Reading the event-0 block in full shows it sets **two** fields,
|
||||||
|
one instruction apart: `stw r28, 136(r30)` (state = 0) *and* `stw r11, 132(r30)`
|
||||||
|
with `r11 = 2` — **phase = 2**. So Ⓑ lands in **phase 2**, the same phase as the
|
||||||
|
boot title, and this test never probed phase 4 state 0 at all.
|
||||||
|
|
||||||
|
The refutation is withdrawn. Ⓐ working on the Ⓑ-returned title is exactly what
|
||||||
|
the hypothesis predicts — phase 2 is the phase that references `BUTTON`.
|
||||||
|
|
||||||
|
### ✅ What the test DID establish — the puzzle is narrower than this page says
|
||||||
|
|
||||||
|
(This part stands: it is a measurement, independent of which phase the returned
|
||||||
|
title turns out to be.)
|
||||||
|
|
||||||
|
This page frames the problem as *the boot title accepts Ⓐ, a later title does
|
||||||
|
not*. That is too broad. **The Ⓑ-returned title accepts Ⓐ perfectly well** — a
|
||||||
|
case nobody had tried. Only the **attract-returned** title is inert.
|
||||||
|
|
||||||
|
So whatever makes the title stop responding is tied to the attract cycle
|
||||||
|
specifically — the movie playing and coming back — not to "the title has been
|
||||||
|
shown before". That is a much smaller haystack than the one this page has been
|
||||||
|
pointing at.
|
||||||
|
|
||||||
|
⚠️ Also reproduced in passing: 4 s after Ⓑ the plate is **absent** (169 plate
|
||||||
|
pixels), and it fades in after. A test that samples too early will read the
|
||||||
|
returned title as plate-less, which this corpus has already been caught by once.
|
||||||
|
|||||||
364
docs/re/capture-harness-status.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# 🔴 Why the boot harness stopped reaching the title — `screenshot` costs 10.8 s
|
||||||
|
|
||||||
|
**Status:** ✅ **diagnosed, with a control.** Four consecutive runs on
|
||||||
|
2026-08-28/29 failed to reach the interactive title, across two locales, two
|
||||||
|
launch paths and two display-gamma settings. The cause is none of those.
|
||||||
|
|
||||||
|
## The measurement
|
||||||
|
|
||||||
|
| condition | one `screenshot` call |
|
||||||
|
|---|---|
|
||||||
|
| while Xenia Canary is running | **10.8 s** |
|
||||||
|
| immediately after killing it | **0.117 s** |
|
||||||
|
|
||||||
|
**92×.** The 1-minute load average at the slow measurement was 1.80, so this is
|
||||||
|
contention with the emulator (both go through the same X server), not general
|
||||||
|
system load.
|
||||||
|
|
||||||
|
## Why that breaks the harness
|
||||||
|
|
||||||
|
`skip_intro.sh` takes **two** grabs 0.6 s apart per iteration, plus an
|
||||||
|
`is_title.py` numpy load. Its actual sample timestamps in the last run:
|
||||||
|
|
||||||
|
```
|
||||||
|
57, 95, 177, 218, 238, 298, 333, 380, 426, 471, 515 → intervals
|
||||||
|
38, 82, 41, 20, 30, 30, 35, 47, 46, 45, 44, 43, 21, 19 median 41 s
|
||||||
|
```
|
||||||
|
|
||||||
|
A **41-second** sampling interval against a title screen that the corpus already
|
||||||
|
documents as lasting *"a few seconds"* before auto-returning to the attract loop
|
||||||
|
(`wait_title.sh`'s own header). The harness is not seeing a stuck game; it is
|
||||||
|
blinking slower than the thing it is looking for.
|
||||||
|
|
||||||
|
That is also why runs at 16:43–18:05 the same day succeeded and later ones did
|
||||||
|
not — nothing about the game changed.
|
||||||
|
|
||||||
|
## 🔴 What this retracts
|
||||||
|
|
||||||
|
Three earlier conclusions were built on these runs and are **withdrawn as
|
||||||
|
causes**, though the observations stand:
|
||||||
|
|
||||||
|
* *"the Japanese-locale run never reaches the interactive title"* — it may well
|
||||||
|
have appeared, unsampled.
|
||||||
|
* *"neither locale reaches the interactive title without a pad press"* — the
|
||||||
|
English control shared the same defect, so it controlled for locale but not for
|
||||||
|
the sampling rate.
|
||||||
|
* *"the game sat in the attract loop for 604 s"* — what was observed is that
|
||||||
|
**every one of ~15 samples** landed on movie content, which at a 41 s interval
|
||||||
|
is a much weaker statement than it reads as.
|
||||||
|
|
||||||
|
## ⚠️ A confound I introduced
|
||||||
|
|
||||||
|
Setting `kernel_display_gamma_type = 0` makes the frame substantially brighter
|
||||||
|
(a mid-attract frame measured mean **122.8** against **52.5** and **82.8** on
|
||||||
|
comparable phases at type 2). `skip_intro.sh` classifies movie-vs-static on an
|
||||||
|
**absolute** rmse threshold of 1500 between two grabs, so a brighter output
|
||||||
|
inflates that difference and biases every frame toward "movie". **The capture
|
||||||
|
harness's tuning is coupled to the display settings** — changing gamma and
|
||||||
|
capture behaviour in one run confounds both.
|
||||||
|
|
||||||
|
## 🔴 The fix works — and it REFUTES the diagnosis above
|
||||||
|
|
||||||
|
Built and measured (`tools/re-capture/fast_title_probe.py`): one long-lived
|
||||||
|
`ffmpeg` x11grab stream, raw RGB frames, glyph counted in numpy. No per-sample
|
||||||
|
process startup, no PNG encode, no `convert -crop`.
|
||||||
|
|
||||||
|
| probe | seconds per sample, emulator running |
|
||||||
|
|---|---|
|
||||||
|
| the wrapper `screenshot` | **3.98** |
|
||||||
|
| `import -window root` → PPM | 1.20 |
|
||||||
|
| **long-lived x11grab stream** | **0.29** |
|
||||||
|
|
||||||
|
**13.7× faster**, and the counter is control-verified against the committed
|
||||||
|
frames — it returns **753** on `live-title-press-a.png` and **327** on
|
||||||
|
`live-main-menu.png`, byte-identical to `is_title.py`.
|
||||||
|
|
||||||
|
Then it was pointed at a running game:
|
||||||
|
|
||||||
|
```
|
||||||
|
332 frames in 85.3 s = 3.89 fps; max glyph 0
|
||||||
|
1674 frames in 420.0 s = 3.99 fps; max glyph 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**1 674 consecutive samples over seven unbroken minutes, four per second, and the
|
||||||
|
interactive title never appeared.** So the sampling rate was a real defect and
|
||||||
|
*not* the cause. The hypothesis on this page — that the harness was blinking
|
||||||
|
slower than the event — is **mine, and refuted by my own fix**.
|
||||||
|
|
||||||
|
## What that restores
|
||||||
|
|
||||||
|
Last iteration I withdrew three conclusions on the strength of that hypothesis.
|
||||||
|
The withdrawal was right at the time (15 samples at 41 s intervals cannot support
|
||||||
|
them) and is now **superseded by better evidence**: dense sampling says the
|
||||||
|
interactive title genuinely does not appear in a mid-run window. Reinstated as a
|
||||||
|
**measurement**, with its reach:
|
||||||
|
|
||||||
|
* ✅ over **420 continuous seconds**, English, `gamma_type = 2`, ~13 minutes into
|
||||||
|
a run with no pad input, **zero** frames carried the green Ⓐ glyph.
|
||||||
|
* ⚠️ Reach: this covers a **mid-run** window only. It says nothing about the
|
||||||
|
first minutes of boot.
|
||||||
|
|
||||||
|
## 🟡 The leading hypothesis, not confirmed
|
||||||
|
|
||||||
|
The corpus already suspects the answer. `title_states_capture.sh` exists to test
|
||||||
|
*"whether the interactive one draws `ptbtn00` (the PRESS Ⓐ plate) and the other
|
||||||
|
does not"* — i.e. the title appears **twice**: once at the end of the boot
|
||||||
|
sequence, and again from the attract loop, and only the first may carry the
|
||||||
|
plate. If so, the plate's window is early and one-shot, and no amount of
|
||||||
|
mid-run sampling will ever find it.
|
||||||
|
|
||||||
|
That is consistent with everything measured, and it is **not confirmed**. The
|
||||||
|
test is to start the fast probe *before* the boot title — from t=0 rather than
|
||||||
|
attaching to a run already in progress.
|
||||||
|
|
||||||
|
## The original fix note, kept
|
||||||
|
|
||||||
|
Make the probe cheap enough to sample faster than the title window: grab a small
|
||||||
|
region rather than the full surface, drop the ImageMagick `convert` round trip,
|
||||||
|
or keep the glyph test in one long-lived process instead of re-importing numpy
|
||||||
|
per sample. None of that is done — this page is the diagnosis, and it is what
|
||||||
|
every remaining emulator-side question is waiting on.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 The boot-window hypothesis is refuted too — and the surface is BLACK
|
||||||
|
|
||||||
|
**2026-08-29, third explanation and third refutation.** The named experiment was
|
||||||
|
to attach the fast probe at **t = 0** so the boot title could not be missed.
|
||||||
|
Done, on the default config, English:
|
||||||
|
|
||||||
|
```
|
||||||
|
2391 frames in 600.4 s = 3.98 fps; max glyph 0; hits 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Ten minutes, sampled four times a second **from launch**, and the green Ⓐ glyph
|
||||||
|
never appeared. So "the plate only shows in an early boot window I keep missing"
|
||||||
|
is **mine, and refuted**.
|
||||||
|
|
||||||
|
Then the thing I should have checked first. Splitting the raw root grab by band:
|
||||||
|
|
||||||
|
| band | non-black | mean |
|
||||||
|
|---|---|---|
|
||||||
|
| y 0–44 (the GTK menu bar) | **100 %** | 210.5 |
|
||||||
|
| **y 45–719 (the game surface)** | **0.08 %** | **0.07** |
|
||||||
|
|
||||||
|
**The game is rendering black**, reproducibly across back-to-back samples, while
|
||||||
|
the guest is alive and polling input (`XamInputGetKeystrokeEx` past 1 201 calls)
|
||||||
|
and `MEM-WATCH` keeps reporting. The crop and every pixel oracle were correct;
|
||||||
|
there was nothing on the surface to detect.
|
||||||
|
|
||||||
|
### ⚠️ What this does and does not explain
|
||||||
|
|
||||||
|
**It does not retroactively explain the earlier failures**, and saying so would
|
||||||
|
be the fourth over-reach in a row. Those runs had *content*: run 2 sampled frames
|
||||||
|
at mean 33.1 (warm), run 3's classifier measured real frame-to-frame rmse, and
|
||||||
|
the `gamma_type = 0` run measured mean 122.8. A black surface is **this run's**
|
||||||
|
symptom.
|
||||||
|
|
||||||
|
So the honest reading is that the failure mode **changed** over the session, and
|
||||||
|
the black screen is a new and worse one.
|
||||||
|
|
||||||
|
### 🟡 Hypothesis for the regression, untested
|
||||||
|
|
||||||
|
Canary's shader/pipeline cache at `~/.local/share/Xenia/cache` is **47 MB** and
|
||||||
|
was last written **23:49 on 2026-08-28** — during the failed runs. This session
|
||||||
|
has `kill -9`'d the emulator repeatedly, which can leave a partially written
|
||||||
|
cache. A corrupt pipeline cache is a plausible route to a guest that runs while
|
||||||
|
nothing reaches the screen.
|
||||||
|
|
||||||
|
**The test is one line and one run:** move `cache*` aside and boot again. Not
|
||||||
|
done — it needs a fresh 10-minute run, and this iteration had spent its emulator
|
||||||
|
budget proving the negative above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 The fast probe STALLS — and that invalidates its own dense negatives
|
||||||
|
|
||||||
|
**2026-08-29.** The instrument built last iteration is unfit for long runs, found
|
||||||
|
by cross-checking it against an independent grabber *while both watched the same
|
||||||
|
screen*.
|
||||||
|
|
||||||
|
A single long-lived `ffmpeg` x11grab stream degrades and then freezes:
|
||||||
|
|
||||||
|
```
|
||||||
|
862 frames in 540.1 s = 1.60 fps (it starts at 3.98 fps)
|
||||||
|
t=450s surface mean 5.21 nonblack 10.1%
|
||||||
|
t=480s surface mean 5.21 nonblack 10.1%
|
||||||
|
t=510s surface mean 5.21 nonblack 10.1%
|
||||||
|
t=540s surface mean 5.21 nonblack 10.1%
|
||||||
|
```
|
||||||
|
|
||||||
|
Four consecutive 30-second marks reporting an identical value. At that same
|
||||||
|
moment an `import` grab of the same display read **surface mean 125.65**, and a
|
||||||
|
*freshly started* ffmpeg stream read **122.43** — agreeing with `import` to 3 %.
|
||||||
|
So the acquisition, not the analysis, was broken: the stream was replaying a
|
||||||
|
stale frame while the screen was 24× brighter.
|
||||||
|
|
||||||
|
🔴 **Therefore the previous section's headline is withdrawn.** "2 391 frames over
|
||||||
|
600 s from t=0, max glyph 0" cannot distinguish *the title never appeared* from
|
||||||
|
*the stream froze early and repeated one frame 2 391 times*. Its 3.98 fps was
|
||||||
|
measured over the first 20 s, before the degradation.
|
||||||
|
|
||||||
|
**Fixed** in `tools/re-capture/fast_title_probe.py`: the stream is torn down and
|
||||||
|
restarted every 30 s. Startup costs ~0.3 s, cheap against the title's window, and
|
||||||
|
it buys a guarantee the frames are live.
|
||||||
|
|
||||||
|
## ✅ Clearing the shader cache restored rendering
|
||||||
|
|
||||||
|
The cache hypothesis from the previous section was tested: `cache`, `cache0`,
|
||||||
|
`cache1` and `cache_host` moved aside (to `/tmp/xenia-cache-aside`, not deleted),
|
||||||
|
then a fresh boot.
|
||||||
|
|
||||||
|
The surface **renders again**, confirmed with the independent grabber:
|
||||||
|
`import` reads surface mean **54.8** and **68.6** on successive samples with 100 %
|
||||||
|
non-black warm content, against **0.07** and 0.08 % non-black in the black run.
|
||||||
|
The probe's own summary agrees where it can be trusted — **773 of 862 frames had
|
||||||
|
> 2 % non-black**.
|
||||||
|
|
||||||
|
🟡 **Supported, not proven.** One run each side, and the emulator was `kill -9`'d
|
||||||
|
many times before the black run, so a corrupt pipeline cache is plausible but a
|
||||||
|
single A/B does not exclude run-to-run variation. The old caches are kept in
|
||||||
|
`/tmp/xenia-cache-aside` if anyone wants to reproduce the failure.
|
||||||
|
|
||||||
|
❔ **Still no title.** Even rendering, `max glyph 0` — but that number now comes
|
||||||
|
from a stalling probe, so it establishes nothing either way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ A negative I can finally stand behind — measured with a self-validating probe
|
||||||
|
|
||||||
|
**2026-08-29.** Three earlier "the title never appears" claims came from
|
||||||
|
instruments that were later found broken (a stale pixel oracle, a 41 s sampling
|
||||||
|
interval, a freezing stream). This run used
|
||||||
|
[`tools/re-capture/title_probe_xchecked.py`](../../tools/re-capture/title_probe_xchecked.py),
|
||||||
|
which restarts its stream every 30 s **and cross-checks itself against an
|
||||||
|
independent `import` grab every 60 s**, printing both numbers.
|
||||||
|
|
||||||
|
```
|
||||||
|
1851 frames in 560.2 s = 3.30 fps
|
||||||
|
cross-checks 9, disagreements 1
|
||||||
|
max glyph 0
|
||||||
|
```
|
||||||
|
|
||||||
|
| t | stream | import | |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 62 s | 6.05 | 0.07 | disagree — a fade, the boot logos are mid-transition |
|
||||||
|
| 123 s | 7.40 | 7.49 | agree |
|
||||||
|
| 183 s | 8.18 | 8.29 | agree |
|
||||||
|
| 243 s | 0.23 | 0.10 | agree |
|
||||||
|
| 311 s | 89.68 | 89.51 | agree |
|
||||||
|
| 371 s | 80.97 | 81.58 | agree |
|
||||||
|
| 426 s | 117.43 | 117.72 | agree |
|
||||||
|
| 487 s | 77.71 | 76.25 | agree |
|
||||||
|
| 546 s | 70.43 | 70.55 | agree |
|
||||||
|
|
||||||
|
Eight of nine agree to within 2 %, the fps held at 3.30 (no collapse to 1.60),
|
||||||
|
and the surface plainly moved through dark and bright phases. **The frames were
|
||||||
|
live and the negative is real:**
|
||||||
|
|
||||||
|
> ✅ **Measured — over 560 continuous seconds from launch, sampled 3.3 times a
|
||||||
|
> second by a cross-validated probe, the interactive title's green Ⓐ plate never
|
||||||
|
> appears, while the game renders throughout.**
|
||||||
|
|
||||||
|
The final frame correlates **0.0145** with our title render, **−0.0047** with the
|
||||||
|
main menu and **0.0102** with `EXTRAS` — it is attract-movie content, not a UI
|
||||||
|
screen at all.
|
||||||
|
|
||||||
|
## ❔ Why, still unknown
|
||||||
|
|
||||||
|
`live-title-press-a.png` is committed and carries 753 glyph pixels, so the title
|
||||||
|
*was* reachable from this container on 2026-08-28. Nothing found so far explains
|
||||||
|
the change. Clearing the shader cache fixed the *black surface* but not this.
|
||||||
|
|
||||||
|
## Where this leaves the emulator-side questions
|
||||||
|
|
||||||
|
The two open items that need a running menu — the gamma control
|
||||||
|
([tone curve](structures/ui-render-tone-curve.md)) and separating `8AX` from
|
||||||
|
`ptbase` ([8AX](structures/ui-8ax-fullres-background.md)) — remain blocked, now
|
||||||
|
on a well-characterised and instrument-verified failure rather than on a
|
||||||
|
suspicion. Neither blocks the five menu screens. **Returning to static work**;
|
||||||
|
the probe is committed for whoever picks this up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❔ The GPU trace route: attempted, produced nothing, characterised
|
||||||
|
|
||||||
|
**2026-08-29.** To turn the gamma-ramp *inference* into an observation, canary's
|
||||||
|
`trace_gpu_stream` should work — it records gamma ramps as their own command type
|
||||||
|
(`kGammaRamp`, index 11 in `TraceCommandType`). Two bounded runs produced **no
|
||||||
|
trace file at all**: nothing under the prefix, no `.xtr` anywhere, no
|
||||||
|
`scratch/gpu/`.
|
||||||
|
|
||||||
|
Bounded deliberately: the container's disk is at **95 % (50 GiB free)** and a
|
||||||
|
boot-time trace of all GPU packets includes video decode, so the runner carried a
|
||||||
|
watchdog killing the emulator the moment the trace passed a 2 GiB cap. It never
|
||||||
|
fired — there was nothing to cap. Disk was unchanged at 95 % throughout.
|
||||||
|
|
||||||
|
What the attempt did establish:
|
||||||
|
|
||||||
|
* `BeginTracing()` is called at GPU init when the cvar is set
|
||||||
|
(`graphics_system.cc:237`), but `EndTracing()` runs only from
|
||||||
|
`GraphicsSystem::Shutdown()`. **A `kill -9` — which this session has used
|
||||||
|
routinely — can never finalise a trace.** The second run was therefore stopped
|
||||||
|
with `SIGTERM` and exited cleanly. Still no file, so that is not the whole
|
||||||
|
story.
|
||||||
|
* ~~Two candidates remain and were **not** separated: the CLI flag not reaching
|
||||||
|
the cvar, or `BeginTracing()` failing silently.~~
|
||||||
|
|
||||||
|
### ✅ Explained — and it was neither candidate
|
||||||
|
|
||||||
|
**The trace writer is compiled out of the build in use.** Following the code:
|
||||||
|
`BeginTracing()` only sets `trace_state_ = kStreaming` — *"Streaming starts on
|
||||||
|
the next primary buffer execute"* — and the file is opened later, in
|
||||||
|
`ExecutePrimaryBuffer`, inside
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1
|
||||||
|
```
|
||||||
|
|
||||||
|
which `trace_writer.h` defines as:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#ifdef NDEBUG
|
||||||
|
#define XE_ENABLE_TRACE_WRITER_INSTRUMENTATION 0 // release
|
||||||
|
#else
|
||||||
|
#define XE_ENABLE_TRACE_WRITER_INSTRUMENTATION 1 // debug
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirmed in the binaries themselves, with a control. The format string
|
||||||
|
`"{:08X}_stream.xtr"` exists only inside that guard:
|
||||||
|
|
||||||
|
| binary | `_stream.xtr` occurrences |
|
||||||
|
|---|---|
|
||||||
|
| `build/bin/Linux/Release/xenia_canary` | **0** |
|
||||||
|
| `build/bin/Linux/Debug/xenia_canary` | **1** |
|
||||||
|
| `/sylph-home/re/canary-build/.../Release/xenia_canary` — **the one `run-canary` uses** | **0** |
|
||||||
|
|
||||||
|
So `trace_gpu_stream` is a **no-op in this container's emulator**: the cvar
|
||||||
|
parses, `BeginTracing` runs, and nothing can ever open a file. Neither the CLI
|
||||||
|
flag nor `BeginTracing` was at fault, and neither was the `kill -9` — though that
|
||||||
|
would have destroyed the trace too, had one existed.
|
||||||
|
|
||||||
|
🟡 **The route exists but is not cheap.** A Debug build with the writer compiled
|
||||||
|
in is present at `build/bin/Linux/Debug/xenia_canary` (253 MB against Release's
|
||||||
|
18 MB). Running it means a much slower boot and a trace of every GPU packet on a
|
||||||
|
disk at 95 %. Recorded as available rather than attempted — the thing it would
|
||||||
|
confirm (the `DC_LUT` write) is already a well-supported inference, so the cost
|
||||||
|
is out of proportion to the gain.
|
||||||
|
|
||||||
|
## ⚠️ The config dump in a log is the FILE, not the effective command line
|
||||||
|
|
||||||
|
Nearly a wrong conclusion here. The dump printed at startup showed
|
||||||
|
`trace_gpu_stream = false` after I had passed `--trace_gpu_stream=true`, which
|
||||||
|
reads as "the flag was ignored". It is not evidence either way:
|
||||||
|
|
||||||
|
| | passed on the CLI | shown in the dump | actual behaviour |
|
||||||
|
|---|---|---|---|
|
||||||
|
| gamma run | `--log_mask=12 --log_level=3` | `log_mask = 0`, `log_level = 2` | **Kernel Debug logging demonstrably ON** |
|
||||||
|
|
||||||
|
The gamma run's flags plainly took effect — that run is where
|
||||||
|
`VdGetCurrentDisplayGamma` was captured — while its dump showed the file's
|
||||||
|
values. So the dump reflects the config file and cannot confirm or refute a
|
||||||
|
command-line override.
|
||||||
53
docs/re/captures/ORACLE-CAPTURES.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# The oracle frames — what to verify a render against
|
||||||
|
|
||||||
|
**These are framebuffer captures of the real game running under Xenia Canary.**
|
||||||
|
They are the reference. `sylpheed-cli screen render` is **not** — Reborn is a GUI
|
||||||
|
explorer and extraction CLI built to check that our *decoding* is right, and it
|
||||||
|
may very well be wrong. Where a render and a capture disagree, the capture wins,
|
||||||
|
and the render is the thing to go and fix.
|
||||||
|
|
||||||
|
⚠️ Two renderers agreeing proves nothing: they share our assumptions. This corpus
|
||||||
|
has been bitten by exactly that three times — the dropped `pteff05` background,
|
||||||
|
the scale-0 rect, and `rest()`. Each was invisible to any render-vs-render diff
|
||||||
|
and visible immediately against a capture.
|
||||||
|
|
||||||
|
## The frames
|
||||||
|
|
||||||
|
All are **1279×675**, top-left aligned, cropped to the game surface by the
|
||||||
|
screenshot tool (the guest renders 1280×720; the missing row/column is the crop,
|
||||||
|
not a scale).
|
||||||
|
|
||||||
|
| screen | capture |
|
||||||
|
|---|---|
|
||||||
|
| publisher splash (SQUARE ENIX) | [`title-builds/live-splash-publisher.png`](title-builds/live-splash-publisher.png) |
|
||||||
|
| developer splash (GAME ARTS / SETA / anima) | [`title-builds/live-splash-developer.png`](title-builds/live-splash-developer.png) |
|
||||||
|
| title, **without** the `PRESS Ⓐ` plate | [`title-builds/live-title-build4-no-plate.png`](title-builds/live-title-build4-no-plate.png) |
|
||||||
|
| title, **with** the plate | [`title-builds/live-title-press-a.png`](title-builds/live-title-press-a.png) |
|
||||||
|
| main menu | [`title-builds/live-main-menu.png`](title-builds/live-main-menu.png) · [`main-menu-oracle.png`](main-menu-oracle.png) |
|
||||||
|
| main menu, **`OPTIONS` focused** | [`title-builds/live-main-menu-options-focused.png`](title-builds/live-main-menu-options-focused.png) |
|
||||||
|
| `EXTRAS` | [`title-builds/live-extras.png`](title-builds/live-extras.png) |
|
||||||
|
| title (alternate) | [`title-screen-oracle.png`](title-screen-oracle.png) |
|
||||||
|
| a screen transition, 13 frames | [`transitions/transition-filmstrip.png`](transitions/transition-filmstrip.png) + [`transition-luminance.csv`](transitions/transition-luminance.csv) |
|
||||||
|
|
||||||
|
The **focused** pair is the useful one for button states: the same screen with a
|
||||||
|
different button highlighted, so the difference isolates what focus changes.
|
||||||
|
|
||||||
|
## ⚠️ Before you compute an RMSE against one
|
||||||
|
|
||||||
|
* **They are not gamma-neutral.** `capture ≈ 255·(render/255)^γ` with γ ≈ 1.34–1.49,
|
||||||
|
and that is a ramp **the game installed**, not a capture-path artefact. So RMSE
|
||||||
|
against these has a floor and chasing it below that floor is chasing the ramp.
|
||||||
|
[`../structures/ui-render-tone-curve.md`](../structures/ui-render-tone-curve.md)
|
||||||
|
* **Geometry is sound**: cross-correlating a render against `live-main-menu.png`
|
||||||
|
over ±6 px puts the best alignment at exactly (0,0), correlation 0.9466. So a
|
||||||
|
positional disagreement is real, not a crop artefact.
|
||||||
|
* **A capture is one moment.** Several of these screens are still animating; the
|
||||||
|
title's two `ptloop` sweeps move continuously. Compare settled poses, or
|
||||||
|
compare regions you know are at rest.
|
||||||
|
|
||||||
|
## What is NOT here
|
||||||
|
|
||||||
|
No capture of the interactive title reached mid-run without a pad press — three
|
||||||
|
runs across two locales and two launch paths never reached it in ~35 minutes.
|
||||||
|
See [`../capture-harness-status.md`](../capture-harness-status.md). And no
|
||||||
|
`GP_READY_ROOM` capture; S1 ruled it out of scope.
|
||||||
BIN
docs/re/captures/menu-nav/extras-wrap.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/re/captures/menu-nav/q4-destinations.png
Normal file
|
After Width: | Height: | Size: 634 KiB |
BIN
docs/re/captures/menu-nav/wrap-montage.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/re/captures/newgame-path/newgame-difficulty.png
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
docs/re/captures/newgame-path/newgame-selectdata-crash.png
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
docs/re/captures/newgame-path/s00a-frame-at-capture-6s.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/re/captures/newgame-path/title-after-single-A-skip.png
Normal file
|
After Width: | Height: | Size: 994 KiB |
BIN
docs/re/captures/ready-room-probe/ready-room-pak-builds.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 143 KiB |
BIN
docs/re/captures/title-builds/live-extras.png
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
docs/re/captures/title-builds/live-main-menu-options-focused.png
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
docs/re/captures/title-builds/live-main-menu.png
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
docs/re/captures/title-builds/live-splash-developer.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/re/captures/title-builds/live-splash-publisher.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/re/captures/title-builds/live-title-build4-no-plate.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/re/captures/title-builds/live-title-press-a.png
Normal file
|
After Width: | Height: | Size: 996 KiB |
BIN
docs/re/captures/title-builds/splash-both-halves-rendered.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/re/captures/title-builds/splash-entries-rendered.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/re/captures/title-builds/title-build-contact-sheet.png
Normal file
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 664 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
# every draw in SUBMISSION ORDER, undeduplicated, frames 1..3
|
||||||
|
# tex dimensions identify the sprite; base is the guest address
|
||||||
|
0 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
|
||||||
|
vb=0x15036638 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [-0.50,-0.50,z=0.00000] [1279.50,-0.50,z=0.00000] [1279.50,719.50,z=0.00000]
|
||||||
|
1 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
|
||||||
|
vb=0x15036690 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||||
|
2 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||||
|
vb=0x150366F0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [0.70,1.58,z=0.00000,col=C3FFFFFF] [1.24,1.02,z=0.00000,col=C3FFFFFF] [0.40,-1.57,z=0.00000,col=C3FFFFFF] [-0.14,-1.02,z=0.00000,col=C3FFFFFF] [-1.29,1.02,z=0.00000,col=B6FFFFFF] [-0.85,1.81,z=0.00000,col=B6FFFFFF] [0.75,-1.02,z=0.00000,col=B6FFFFFF] [0.31,-1.81,z=0.00000,col=B6FFFFFF]
|
||||||
|
3 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
|
||||||
|
vb=0x150367B0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||||
|
4 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||||
|
vb=0x15036810 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.90,0.68,z=0.00000,col=FFFFFFFF] [0.87,0.68,z=0.00000,col=FFFFFFFF] [0.87,-0.10,z=0.00000,col=FFFFFFFF] [-0.90,-0.10,z=0.00000,col=FFFFFFFF] [-0.89,0.65,z=0.00000,col=FFFFFFFF] [0.86,0.65,z=0.00000,col=FFFFFFFF] [0.86,-0.08,z=0.00000,col=FFFFFFFF] [-0.89,-0.08,z=0.00000,col=FFFFFFFF]
|
||||||
|
5 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||||
|
vb=0x150368D0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.71,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.15,z=0.00000,col=FFFFFFFF] [-0.71,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.10,z=0.00000,col=FFFFFFFF] [0.76,-0.10,z=0.00000,col=FFFFFFFF]
|
||||||
|
6 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
|
||||||
|
vb=0x15036990 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.79,0.15,z=0.00000,col=FFFFFFFF] [0.76,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.14,z=0.00000,col=FFFFFFFF] [-0.79,-0.14,z=0.00000,col=FFFFFFFF]
|
||||||
|
7 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||||
|
vb=0x150369F0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.67,z=0.00000,col=FFFFFFFF] [-0.40,-0.67,z=0.00000,col=FFFFFFFF]
|
||||||
|
8 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||||
|
vb=0x15036AB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.42,-0.49,z=0.00000,col=31FFFFFF] [0.42,-0.49,z=0.00000,col=31FFFFFF] [0.42,-0.70,z=0.00000,col=31FFFFFF] [-0.42,-0.70,z=0.00000,col=31FFFFFF]
|
||||||
|
9 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||||
|
10 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||||
|
--- frame 3 ---
|
||||||
|
11 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
|
||||||
|
vb=0x15076C38 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [-0.50,-0.50,z=0.00000] [1279.50,-0.50,z=0.00000] [1279.50,719.50,z=0.00000]
|
||||||
|
12 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
|
||||||
|
vb=0x15076C90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||||
|
13 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||||
|
vb=0x15076CF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [0.73,1.58,z=0.00000,col=C5FFFFFF] [1.27,1.02,z=0.00000,col=C5FFFFFF] [0.43,-1.57,z=0.00000,col=C5FFFFFF] [-0.11,-1.02,z=0.00000,col=C5FFFFFF] [-1.32,1.02,z=0.00000,col=B8FFFFFF] [-0.88,1.81,z=0.00000,col=B8FFFFFF] [0.71,-1.02,z=0.00000,col=B8FFFFFF] [0.27,-1.81,z=0.00000,col=B8FFFFFF]
|
||||||
|
14 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
|
||||||
|
vb=0x15076DB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||||
|
15 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||||
|
vb=0x15076E10 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.90,0.68,z=0.00000,col=FFFFFFFF] [0.87,0.68,z=0.00000,col=FFFFFFFF] [0.87,-0.10,z=0.00000,col=FFFFFFFF] [-0.90,-0.10,z=0.00000,col=FFFFFFFF] [-0.89,0.65,z=0.00000,col=FFFFFFFF] [0.86,0.65,z=0.00000,col=FFFFFFFF] [0.86,-0.08,z=0.00000,col=FFFFFFFF] [-0.89,-0.08,z=0.00000,col=FFFFFFFF]
|
||||||
|
16 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||||
|
vb=0x15076ED0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.71,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.15,z=0.00000,col=FFFFFFFF] [-0.71,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.10,z=0.00000,col=FFFFFFFF] [0.76,-0.10,z=0.00000,col=FFFFFFFF]
|
||||||
|
17 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
|
||||||
|
vb=0x15076F90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.79,0.15,z=0.00000,col=FFFFFFFF] [0.76,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.14,z=0.00000,col=FFFFFFFF] [-0.79,-0.14,z=0.00000,col=FFFFFFFF]
|
||||||
|
18 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||||
|
vb=0x15076FF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.67,z=0.00000,col=FFFFFFFF] [-0.40,-0.67,z=0.00000,col=FFFFFFFF]
|
||||||
|
19 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||||
|
vb=0x150770B0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.42,-0.49,z=0.00000,col=1EFFFFFF] [0.42,-0.49,z=0.00000,col=1EFFFFFF] [0.42,-0.70,z=0.00000,col=1EFFFFFF] [-0.42,-0.70,z=0.00000,col=1EFFFFFF]
|
||||||
|
20 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||||
|
21 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||||
BIN
docs/re/captures/title-builds/title-swoosh-capture-vs-render.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
docs/re/captures/transitions/transition-filmstrip.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
421
docs/re/captures/transitions/transition-luminance.csv
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
frame,seconds,mean_luminance
|
||||||
|
0,0.0000,26.045
|
||||||
|
1,0.0333,26.054
|
||||||
|
2,0.0667,26.051
|
||||||
|
3,0.1000,26.054
|
||||||
|
4,0.1333,26.049
|
||||||
|
5,0.1667,26.054
|
||||||
|
6,0.2000,26.052
|
||||||
|
7,0.2333,26.053
|
||||||
|
8,0.2667,26.046
|
||||||
|
9,0.3000,26.053
|
||||||
|
10,0.3333,26.049
|
||||||
|
11,0.3667,26.052
|
||||||
|
12,0.4000,26.047
|
||||||
|
13,0.4333,26.050
|
||||||
|
14,0.4667,26.048
|
||||||
|
15,0.5000,26.049
|
||||||
|
16,0.5333,26.047
|
||||||
|
17,0.5667,26.049
|
||||||
|
18,0.6000,26.048
|
||||||
|
19,0.6333,26.048
|
||||||
|
20,0.6667,26.045
|
||||||
|
21,0.7000,26.048
|
||||||
|
22,0.7333,26.049
|
||||||
|
23,0.7667,26.048
|
||||||
|
24,0.8000,26.046
|
||||||
|
25,0.8333,26.048
|
||||||
|
26,0.8667,26.048
|
||||||
|
27,0.9000,26.049
|
||||||
|
28,0.9333,26.047
|
||||||
|
29,0.9667,26.048
|
||||||
|
30,1.0000,26.048
|
||||||
|
31,1.0333,26.048
|
||||||
|
32,1.0667,26.047
|
||||||
|
33,1.1000,26.048
|
||||||
|
34,1.1333,26.047
|
||||||
|
35,1.1667,26.047
|
||||||
|
36,1.2000,26.046
|
||||||
|
37,1.2333,26.046
|
||||||
|
38,1.2667,26.046
|
||||||
|
39,1.3000,26.047
|
||||||
|
40,1.3333,26.046
|
||||||
|
41,1.3667,26.047
|
||||||
|
42,1.4000,26.047
|
||||||
|
43,1.4333,26.048
|
||||||
|
44,1.4667,26.047
|
||||||
|
45,1.5000,26.049
|
||||||
|
46,1.5333,26.050
|
||||||
|
47,1.5667,26.052
|
||||||
|
48,1.6000,26.051
|
||||||
|
49,1.6333,26.052
|
||||||
|
50,1.6667,26.052
|
||||||
|
51,1.7000,26.052
|
||||||
|
52,1.7333,26.052
|
||||||
|
53,1.7667,26.055
|
||||||
|
54,1.8000,26.056
|
||||||
|
55,1.8333,26.056
|
||||||
|
56,1.8667,26.055
|
||||||
|
57,1.9000,26.058
|
||||||
|
58,1.9333,26.059
|
||||||
|
59,1.9667,26.059
|
||||||
|
60,2.0000,26.057
|
||||||
|
61,2.0333,26.062
|
||||||
|
62,2.0667,26.022
|
||||||
|
63,2.1000,22.032
|
||||||
|
64,2.1333,21.744
|
||||||
|
65,2.1667,17.909
|
||||||
|
66,2.2000,13.996
|
||||||
|
67,2.2333,13.966
|
||||||
|
68,2.2667,11.113
|
||||||
|
69,2.3000,8.072
|
||||||
|
70,2.3333,7.862
|
||||||
|
71,2.3667,6.270
|
||||||
|
72,2.4000,3.375
|
||||||
|
73,2.4333,1.771
|
||||||
|
74,2.4667,0.020
|
||||||
|
75,2.5000,0.020
|
||||||
|
76,2.5333,0.020
|
||||||
|
77,2.5667,0.020
|
||||||
|
78,2.6000,0.020
|
||||||
|
79,2.6333,0.020
|
||||||
|
80,2.6667,0.020
|
||||||
|
81,2.7000,5.985
|
||||||
|
82,2.7333,5.991
|
||||||
|
83,2.7667,5.990
|
||||||
|
84,2.8000,5.991
|
||||||
|
85,2.8333,5.986
|
||||||
|
86,2.8667,7.432
|
||||||
|
87,2.9000,9.300
|
||||||
|
88,2.9333,9.283
|
||||||
|
89,2.9667,9.484
|
||||||
|
90,3.0000,11.294
|
||||||
|
91,3.0333,11.263
|
||||||
|
92,3.0667,11.606
|
||||||
|
93,3.1000,12.608
|
||||||
|
94,3.1333,12.807
|
||||||
|
95,3.1667,12.790
|
||||||
|
96,3.2000,14.517
|
||||||
|
97,3.2333,18.497
|
||||||
|
98,3.2667,26.935
|
||||||
|
99,3.3000,27.008
|
||||||
|
100,3.3333,27.010
|
||||||
|
101,3.3667,27.039
|
||||||
|
102,3.4000,27.036
|
||||||
|
103,3.4333,27.083
|
||||||
|
104,3.4667,27.239
|
||||||
|
105,3.5000,27.294
|
||||||
|
106,3.5333,27.293
|
||||||
|
107,3.5667,27.294
|
||||||
|
108,3.6000,27.289
|
||||||
|
109,3.6333,27.291
|
||||||
|
110,3.6667,27.290
|
||||||
|
111,3.7000,27.291
|
||||||
|
112,3.7333,27.289
|
||||||
|
113,3.7667,27.292
|
||||||
|
114,3.8000,27.291
|
||||||
|
115,3.8333,27.292
|
||||||
|
116,3.8667,27.291
|
||||||
|
117,3.9000,27.294
|
||||||
|
118,3.9333,27.291
|
||||||
|
119,3.9667,27.293
|
||||||
|
120,4.0000,27.290
|
||||||
|
121,4.0333,27.292
|
||||||
|
122,4.0667,27.291
|
||||||
|
123,4.1000,27.292
|
||||||
|
124,4.1333,27.290
|
||||||
|
125,4.1667,27.293
|
||||||
|
126,4.2000,27.293
|
||||||
|
127,4.2333,27.293
|
||||||
|
128,4.2667,27.292
|
||||||
|
129,4.3000,27.297
|
||||||
|
130,4.3333,27.296
|
||||||
|
131,4.3667,27.297
|
||||||
|
132,4.4000,27.296
|
||||||
|
133,4.4333,27.299
|
||||||
|
134,4.4667,27.300
|
||||||
|
135,4.5000,27.302
|
||||||
|
136,4.5333,27.301
|
||||||
|
137,4.5667,27.304
|
||||||
|
138,4.6000,27.304
|
||||||
|
139,4.6333,27.304
|
||||||
|
140,4.6667,27.302
|
||||||
|
141,4.7000,27.306
|
||||||
|
142,4.7333,27.308
|
||||||
|
143,4.7667,27.309
|
||||||
|
144,4.8000,27.309
|
||||||
|
145,4.8333,27.312
|
||||||
|
146,4.8667,27.311
|
||||||
|
147,4.9000,27.311
|
||||||
|
148,4.9333,27.310
|
||||||
|
149,4.9667,27.317
|
||||||
|
150,5.0000,27.319
|
||||||
|
151,5.0333,27.321
|
||||||
|
152,5.0667,27.322
|
||||||
|
153,5.1000,27.327
|
||||||
|
154,5.1333,27.327
|
||||||
|
155,5.1667,27.328
|
||||||
|
156,5.2000,27.327
|
||||||
|
157,5.2333,27.333
|
||||||
|
158,5.2667,27.334
|
||||||
|
159,5.3000,27.335
|
||||||
|
160,5.3333,27.334
|
||||||
|
161,5.3667,27.344
|
||||||
|
162,5.4000,27.347
|
||||||
|
163,5.4333,27.350
|
||||||
|
164,5.4667,27.351
|
||||||
|
165,5.5000,27.359
|
||||||
|
166,5.5333,27.359
|
||||||
|
167,5.5667,27.360
|
||||||
|
168,5.6000,27.358
|
||||||
|
169,5.6333,27.365
|
||||||
|
170,5.6667,27.366
|
||||||
|
171,5.7000,27.369
|
||||||
|
172,5.7333,27.367
|
||||||
|
173,5.7667,27.383
|
||||||
|
174,5.8000,27.386
|
||||||
|
175,5.8333,27.387
|
||||||
|
176,5.8667,27.386
|
||||||
|
177,5.9000,27.399
|
||||||
|
178,5.9333,27.400
|
||||||
|
179,5.9667,27.402
|
||||||
|
180,6.0000,27.400
|
||||||
|
181,6.0333,27.413
|
||||||
|
182,6.0667,27.414
|
||||||
|
183,6.1000,27.415
|
||||||
|
184,6.1333,27.413
|
||||||
|
185,6.1667,27.432
|
||||||
|
186,6.2000,27.434
|
||||||
|
187,6.2333,27.438
|
||||||
|
188,6.2667,27.436
|
||||||
|
189,6.3000,27.449
|
||||||
|
190,6.3333,27.448
|
||||||
|
191,6.3667,27.451
|
||||||
|
192,6.4000,27.449
|
||||||
|
193,6.4333,27.466
|
||||||
|
194,6.4667,27.471
|
||||||
|
195,6.5000,27.472
|
||||||
|
196,6.5333,27.470
|
||||||
|
197,6.5667,27.480
|
||||||
|
198,6.6000,27.478
|
||||||
|
199,6.6333,27.480
|
||||||
|
200,6.6667,27.476
|
||||||
|
201,6.7000,27.509
|
||||||
|
202,6.7333,27.510
|
||||||
|
203,6.7667,27.516
|
||||||
|
204,6.8000,27.514
|
||||||
|
205,6.8333,27.519
|
||||||
|
206,6.8667,27.519
|
||||||
|
207,6.9000,27.518
|
||||||
|
208,6.9333,27.516
|
||||||
|
209,6.9667,27.538
|
||||||
|
210,7.0000,27.537
|
||||||
|
211,7.0333,27.539
|
||||||
|
212,7.0667,27.538
|
||||||
|
213,7.1000,27.558
|
||||||
|
214,7.1333,27.556
|
||||||
|
215,7.1667,27.558
|
||||||
|
216,7.2000,27.554
|
||||||
|
217,7.2333,27.577
|
||||||
|
218,7.2667,27.576
|
||||||
|
219,7.3000,27.577
|
||||||
|
220,7.3333,27.572
|
||||||
|
221,7.3667,27.596
|
||||||
|
222,7.4000,27.598
|
||||||
|
223,7.4333,27.603
|
||||||
|
224,7.4667,27.597
|
||||||
|
225,7.5000,27.610
|
||||||
|
226,7.5333,27.611
|
||||||
|
227,7.5667,27.613
|
||||||
|
228,7.6000,27.607
|
||||||
|
229,7.6333,27.621
|
||||||
|
230,7.6667,27.621
|
||||||
|
231,7.7000,27.622
|
||||||
|
232,7.7333,27.617
|
||||||
|
233,7.7667,27.624
|
||||||
|
234,7.8000,27.626
|
||||||
|
235,7.8333,27.630
|
||||||
|
236,7.8667,27.622
|
||||||
|
237,7.9000,27.640
|
||||||
|
238,7.9333,27.637
|
||||||
|
239,7.9667,27.639
|
||||||
|
240,8.0000,27.633
|
||||||
|
241,8.0333,27.642
|
||||||
|
242,8.0667,27.639
|
||||||
|
243,8.1000,27.642
|
||||||
|
244,8.1333,27.637
|
||||||
|
245,8.1667,27.658
|
||||||
|
246,8.2000,27.653
|
||||||
|
247,8.2333,27.655
|
||||||
|
248,8.2667,27.649
|
||||||
|
249,8.3000,27.645
|
||||||
|
250,8.3333,27.648
|
||||||
|
251,8.3667,26.704
|
||||||
|
252,8.4000,23.887
|
||||||
|
253,8.4333,23.723
|
||||||
|
254,8.4667,16.471
|
||||||
|
255,8.5000,13.119
|
||||||
|
256,8.5333,13.102
|
||||||
|
257,8.5667,12.118
|
||||||
|
258,8.6000,10.955
|
||||||
|
259,8.6333,10.956
|
||||||
|
260,8.6667,5.365
|
||||||
|
261,8.7000,3.422
|
||||||
|
262,8.7333,3.416
|
||||||
|
263,8.7667,2.820
|
||||||
|
264,8.8000,0.020
|
||||||
|
265,8.8333,0.020
|
||||||
|
266,8.8667,0.020
|
||||||
|
267,8.9000,0.020
|
||||||
|
268,8.9333,0.020
|
||||||
|
269,8.9667,3.267
|
||||||
|
270,9.0000,5.012
|
||||||
|
271,9.0333,5.449
|
||||||
|
272,9.0667,7.339
|
||||||
|
273,9.1000,9.208
|
||||||
|
274,9.1333,10.415
|
||||||
|
275,9.1667,10.418
|
||||||
|
276,9.2000,10.462
|
||||||
|
277,9.2333,10.459
|
||||||
|
278,9.2667,10.569
|
||||||
|
279,9.3000,10.701
|
||||||
|
280,9.3333,10.868
|
||||||
|
281,9.3667,12.924
|
||||||
|
282,9.4000,14.688
|
||||||
|
283,9.4333,14.756
|
||||||
|
284,9.4667,17.184
|
||||||
|
285,9.5000,19.441
|
||||||
|
286,9.5333,19.438
|
||||||
|
287,9.5667,19.492
|
||||||
|
288,9.6000,19.492
|
||||||
|
289,9.6333,19.492
|
||||||
|
290,9.6667,19.508
|
||||||
|
291,9.7000,23.405
|
||||||
|
292,9.7333,23.417
|
||||||
|
293,9.7667,23.414
|
||||||
|
294,9.8000,23.416
|
||||||
|
295,9.8333,23.409
|
||||||
|
296,9.8667,23.467
|
||||||
|
297,9.9000,24.927
|
||||||
|
298,9.9333,24.929
|
||||||
|
299,9.9667,24.926
|
||||||
|
300,10.0000,25.788
|
||||||
|
301,10.0333,25.785
|
||||||
|
302,10.0667,25.789
|
||||||
|
303,10.1000,25.893
|
||||||
|
304,10.1333,25.908
|
||||||
|
305,10.1667,25.905
|
||||||
|
306,10.2000,25.905
|
||||||
|
307,10.2333,25.976
|
||||||
|
308,10.2667,25.986
|
||||||
|
309,10.3000,25.980
|
||||||
|
310,10.3333,25.981
|
||||||
|
311,10.3667,25.976
|
||||||
|
312,10.4000,25.984
|
||||||
|
313,10.4333,26.020
|
||||||
|
314,10.4667,26.022
|
||||||
|
315,10.5000,26.016
|
||||||
|
316,10.5333,26.021
|
||||||
|
317,10.5667,26.018
|
||||||
|
318,10.6000,26.020
|
||||||
|
319,10.6333,26.016
|
||||||
|
320,10.6667,26.021
|
||||||
|
321,10.7000,26.021
|
||||||
|
322,10.7333,26.021
|
||||||
|
323,10.7667,26.019
|
||||||
|
324,10.8000,26.022
|
||||||
|
325,10.8333,26.022
|
||||||
|
326,10.8667,26.022
|
||||||
|
327,10.9000,26.020
|
||||||
|
328,10.9333,26.025
|
||||||
|
329,10.9667,26.025
|
||||||
|
330,11.0000,26.025
|
||||||
|
331,11.0333,26.023
|
||||||
|
332,11.0667,26.029
|
||||||
|
333,11.1000,26.027
|
||||||
|
334,11.1333,26.028
|
||||||
|
335,11.1667,26.024
|
||||||
|
336,11.2000,26.024
|
||||||
|
337,11.2333,26.024
|
||||||
|
338,11.2667,26.024
|
||||||
|
339,11.3000,26.029
|
||||||
|
340,11.3333,26.029
|
||||||
|
341,11.3667,26.027
|
||||||
|
342,11.4000,26.029
|
||||||
|
343,11.4333,26.024
|
||||||
|
344,11.4667,26.027
|
||||||
|
345,11.5000,26.027
|
||||||
|
346,11.5333,26.027
|
||||||
|
347,11.5667,26.025
|
||||||
|
348,11.6000,26.031
|
||||||
|
349,11.6333,26.030
|
||||||
|
350,11.6667,26.033
|
||||||
|
351,11.7000,26.028
|
||||||
|
352,11.7333,26.032
|
||||||
|
353,11.7667,26.032
|
||||||
|
354,11.8000,26.032
|
||||||
|
355,11.8333,26.029
|
||||||
|
356,11.8667,26.034
|
||||||
|
357,11.9000,26.039
|
||||||
|
358,11.9333,26.041
|
||||||
|
359,11.9667,26.041
|
||||||
|
360,12.0000,26.040
|
||||||
|
361,12.0333,26.046
|
||||||
|
362,12.0667,26.047
|
||||||
|
363,12.1000,26.046
|
||||||
|
364,12.1333,26.050
|
||||||
|
365,12.1667,26.051
|
||||||
|
366,12.2000,26.051
|
||||||
|
367,12.2333,26.051
|
||||||
|
368,12.2667,26.051
|
||||||
|
369,12.3000,26.051
|
||||||
|
370,12.3333,26.051
|
||||||
|
371,12.3667,26.051
|
||||||
|
372,12.4000,26.050
|
||||||
|
373,12.4333,26.055
|
||||||
|
374,12.4667,26.056
|
||||||
|
375,12.5000,26.056
|
||||||
|
376,12.5333,26.055
|
||||||
|
377,12.5667,26.056
|
||||||
|
378,12.6000,26.056
|
||||||
|
379,12.6333,26.056
|
||||||
|
380,12.6667,26.055
|
||||||
|
381,12.7000,26.056
|
||||||
|
382,12.7333,26.056
|
||||||
|
383,12.7667,26.056
|
||||||
|
384,12.8000,26.055
|
||||||
|
385,12.8333,26.055
|
||||||
|
386,12.8667,26.056
|
||||||
|
387,12.9000,26.056
|
||||||
|
388,12.9333,26.056
|
||||||
|
389,12.9667,26.056
|
||||||
|
390,13.0000,26.055
|
||||||
|
391,13.0333,26.071
|
||||||
|
392,13.0667,26.072
|
||||||
|
393,13.1000,26.074
|
||||||
|
394,13.1333,26.082
|
||||||
|
395,13.1667,26.083
|
||||||
|
396,13.2000,26.083
|
||||||
|
397,13.2333,26.083
|
||||||
|
398,13.2667,26.083
|
||||||
|
399,13.3000,26.110
|
||||||
|
400,13.3333,26.111
|
||||||
|
401,13.3667,26.114
|
||||||
|
402,13.4000,26.113
|
||||||
|
403,13.4333,26.118
|
||||||
|
404,13.4667,26.119
|
||||||
|
405,13.5000,26.121
|
||||||
|
406,13.5333,26.119
|
||||||
|
407,13.5667,26.140
|
||||||
|
408,13.6000,26.140
|
||||||
|
409,13.6333,26.144
|
||||||
|
410,13.6667,26.142
|
||||||
|
411,13.7000,26.155
|
||||||
|
412,13.7333,26.157
|
||||||
|
413,13.7667,26.158
|
||||||
|
414,13.8000,26.157
|
||||||
|
415,13.8333,26.165
|
||||||
|
416,13.8667,26.165
|
||||||
|
417,13.9000,26.168
|
||||||
|
418,13.9333,26.165
|
||||||
|
419,13.9667,26.165
|
||||||
|
BIN
docs/re/captures/ui-layout/button-base-focus-ring-sprites.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/re/captures/ui-layout/extras-composited.png
Normal file
|
After Width: | Height: | Size: 439 KiB |
BIN
docs/re/captures/ui-layout/focus-ring-oracle-vs-sprite.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
2052
docs/re/captures/ui-timing/splash-build-draws.log
Normal file
790
docs/re/captures/ui-timing/splash-build-quads.csv
Normal file
@@ -0,0 +1,790 @@
|
|||||||
|
frame,draw,x,y,w,h,rot,col
|
||||||
|
1,0,0,0,1280,720,,
|
||||||
|
1,1,0,0,1280,720,,FF000000
|
||||||
|
1,2,307,331,666,65,,FFFFFFFF
|
||||||
|
2,5,0,0,1280,720,,
|
||||||
|
2,6,0,0,1280,720,,FF000000
|
||||||
|
2,7,307,331,666,65,,FFFFFFFF
|
||||||
|
3,10,0,0,1280,720,,
|
||||||
|
3,11,0,0,1280,720,,FF000000
|
||||||
|
3,12,307,331,666,65,,FFFFFFFF
|
||||||
|
4,15,0,0,1280,720,,
|
||||||
|
4,16,0,0,1280,720,,FF000000
|
||||||
|
4,17,307,331,666,65,,FFFFFFFF
|
||||||
|
5,20,0,0,1280,720,,
|
||||||
|
5,21,0,0,1280,720,,FF000000
|
||||||
|
5,22,307,331,666,65,,FFFFFFFF
|
||||||
|
6,25,0,0,1280,720,,
|
||||||
|
6,26,0,0,1280,720,,FF000000
|
||||||
|
6,27,307,331,666,65,,FFFFFFFF
|
||||||
|
7,30,0,0,1280,720,,
|
||||||
|
7,31,0,0,1280,720,,FF000000
|
||||||
|
7,32,307,331,666,65,,FFFFFFFF
|
||||||
|
8,35,0,0,1280,720,,
|
||||||
|
8,36,0,0,1280,720,,FF000000
|
||||||
|
8,37,307,331,666,65,,FFFFFFFF
|
||||||
|
9,40,0,0,1280,720,,
|
||||||
|
9,41,0,0,1280,720,,FF000000
|
||||||
|
9,42,307,331,666,65,,FFFFFFFF
|
||||||
|
10,45,0,0,1280,720,,
|
||||||
|
10,46,0,0,1280,720,,FF000000
|
||||||
|
10,47,307,331,666,65,,FFFFFFFF
|
||||||
|
11,50,0,0,1280,720,,
|
||||||
|
11,51,0,0,1280,720,,FF000000
|
||||||
|
11,52,307,331,666,65,,FFFFFFFF
|
||||||
|
12,55,0,0,1280,720,,
|
||||||
|
12,56,0,0,1280,720,,FF000000
|
||||||
|
12,57,307,331,666,65,,FFFFFFFF
|
||||||
|
13,60,0,0,1280,720,,
|
||||||
|
13,61,0,0,1280,720,,FF000000
|
||||||
|
13,62,307,331,666,65,,FFFFFFFF
|
||||||
|
14,65,0,0,1280,720,,
|
||||||
|
14,66,0,0,1280,720,,FF000000
|
||||||
|
14,67,307,331,666,65,,FFFFFFFF
|
||||||
|
15,70,0,0,1280,720,,
|
||||||
|
15,71,0,0,1280,720,,FF000000
|
||||||
|
15,72,307,331,666,65,,FFFFFFFF
|
||||||
|
16,75,0,0,1280,720,,
|
||||||
|
16,76,0,0,1280,720,,FF000000
|
||||||
|
16,77,307,331,666,65,,FFFFFFFF
|
||||||
|
17,80,0,0,1280,720,,
|
||||||
|
17,81,0,0,1280,720,,FF000000
|
||||||
|
17,82,307,331,666,65,,FFFFFFFF
|
||||||
|
18,85,0,0,1280,720,,
|
||||||
|
18,86,0,0,1280,720,,FF000000
|
||||||
|
18,87,307,331,666,65,,FFFFFFFF
|
||||||
|
19,90,0,0,1280,720,,
|
||||||
|
19,91,0,0,1280,720,,FF000000
|
||||||
|
19,92,307,331,666,65,,FFFFFFFF
|
||||||
|
20,95,0,0,1280,720,,
|
||||||
|
20,96,0,0,1280,720,,FF000000
|
||||||
|
20,97,307,331,666,65,,FFFFFFFF
|
||||||
|
21,100,0,0,1280,720,,
|
||||||
|
21,101,0,0,1280,720,,FF000000
|
||||||
|
21,102,307,331,666,65,,FFFFFFFF
|
||||||
|
22,105,0,0,1280,720,,
|
||||||
|
22,106,0,0,1280,720,,FF000000
|
||||||
|
22,107,307,331,666,65,,FFFFFFFF
|
||||||
|
23,110,0,0,1280,720,,
|
||||||
|
23,111,0,0,1280,720,,FF000000
|
||||||
|
23,112,307,331,666,65,,FFFFFFFF
|
||||||
|
24,115,0,0,1280,720,,
|
||||||
|
24,116,0,0,1280,720,,FF000000
|
||||||
|
24,117,307,331,666,65,,FFFFFFFF
|
||||||
|
25,120,0,0,1280,720,,
|
||||||
|
25,121,0,0,1280,720,,FF000000
|
||||||
|
25,122,307,331,666,65,,FFFFFFFF
|
||||||
|
26,125,0,0,1280,720,,
|
||||||
|
26,126,0,0,1280,720,,FF000000
|
||||||
|
26,127,307,331,666,65,,FFFFFFFF
|
||||||
|
27,130,0,0,1280,720,,
|
||||||
|
27,131,0,0,1280,720,,FF000000
|
||||||
|
27,132,307,331,666,65,,FFFFFFFF
|
||||||
|
28,135,0,0,1280,720,,
|
||||||
|
28,136,0,0,1280,720,,FF000000
|
||||||
|
28,137,307,331,666,65,,FFFFFFFF
|
||||||
|
29,140,0,0,1280,720,,
|
||||||
|
29,141,0,0,1280,720,,FF000000
|
||||||
|
29,142,307,331,666,65,,FFFFFFFF
|
||||||
|
30,145,0,0,1280,720,,
|
||||||
|
30,146,0,0,1280,720,,FF000000
|
||||||
|
30,147,307,331,666,65,,FFFFFFFF
|
||||||
|
31,150,0,0,1280,720,,
|
||||||
|
31,151,0,0,1280,720,,FF000000
|
||||||
|
31,152,307,331,666,65,,FFFFFFFF
|
||||||
|
32,155,0,0,1280,720,,
|
||||||
|
32,156,0,0,1280,720,,FF000000
|
||||||
|
32,157,307,331,666,65,,FFFFFFFF
|
||||||
|
33,160,0,0,1280,720,,
|
||||||
|
33,161,0,0,1280,720,,FF000000
|
||||||
|
33,162,307,331,666,65,,FFFFFFFF
|
||||||
|
34,165,0,0,1280,720,,
|
||||||
|
34,166,0,0,1280,720,,FF000000
|
||||||
|
34,167,307,331,666,65,,FFFFFFFF
|
||||||
|
35,170,0,0,1280,720,,
|
||||||
|
35,171,0,0,1280,720,,FF000000
|
||||||
|
35,172,307,331,666,65,,FFFFFFFF
|
||||||
|
36,175,0,0,1280,720,,
|
||||||
|
36,176,0,0,1280,720,,FF000000
|
||||||
|
36,177,307,331,666,65,,FFFFFFFF
|
||||||
|
37,180,0,0,1280,720,,
|
||||||
|
37,181,0,0,1280,720,,FF000000
|
||||||
|
37,182,307,331,666,65,,FFFFFFFF
|
||||||
|
38,185,0,0,1280,720,,
|
||||||
|
38,186,0,0,1280,720,,FF000000
|
||||||
|
38,187,307,331,666,65,,FFFFFFFF
|
||||||
|
39,190,0,0,1280,720,,
|
||||||
|
39,191,0,0,1280,720,,FF000000
|
||||||
|
39,192,307,331,666,65,,FFFFFFFF
|
||||||
|
40,195,0,0,1280,720,,
|
||||||
|
40,196,0,0,1280,720,,FF000000
|
||||||
|
40,197,307,331,666,65,,FFFFFFFF
|
||||||
|
41,200,0,0,1280,720,,
|
||||||
|
41,201,0,0,1280,720,,FF000000
|
||||||
|
41,202,307,331,666,65,,FFFFFFFF
|
||||||
|
42,205,0,0,1280,720,,
|
||||||
|
42,206,0,0,1280,720,,FF000000
|
||||||
|
42,207,307,331,666,65,,FFFFFFFF
|
||||||
|
43,210,0,0,1280,720,,
|
||||||
|
43,211,0,0,1280,720,,FF000000
|
||||||
|
43,212,307,331,666,65,,FFFFFFFF
|
||||||
|
44,215,0,0,1280,720,,
|
||||||
|
44,216,0,0,1280,720,,FF000000
|
||||||
|
44,217,307,331,666,65,,FFFFFFFF
|
||||||
|
45,220,0,0,1280,720,,
|
||||||
|
45,221,0,0,1280,720,,FF000000
|
||||||
|
45,222,307,331,666,65,,FFFFFFFF
|
||||||
|
47,225,0,0,1280,720,,
|
||||||
|
47,226,0,0,1280,720,,FF000000
|
||||||
|
47,227,307,331,666,65,,FFFFFFFF
|
||||||
|
48,230,0,0,1280,720,,
|
||||||
|
48,231,0,0,1280,720,,FF000000
|
||||||
|
48,232,307,331,666,65,,FFFFFFFF
|
||||||
|
49,235,0,0,1280,720,,
|
||||||
|
49,236,0,0,1280,720,,FF000000
|
||||||
|
49,237,307,331,666,65,,FFFFFFFF
|
||||||
|
50,240,0,0,1280,720,,
|
||||||
|
50,241,0,0,1280,720,,FF000000
|
||||||
|
50,242,307,331,666,65,,FFFFFFFF
|
||||||
|
51,245,0,0,1280,720,,
|
||||||
|
51,246,0,0,1280,720,,FF000000
|
||||||
|
51,247,307,331,666,65,,FFFFFFFF
|
||||||
|
52,250,0,0,1280,720,,
|
||||||
|
52,251,0,0,1280,720,,FF000000
|
||||||
|
52,252,307,331,666,65,,FFFFFFFF
|
||||||
|
53,255,0,0,1280,720,,
|
||||||
|
53,256,0,0,1280,720,,FF000000
|
||||||
|
53,257,307,331,666,65,,FFFFFFFF
|
||||||
|
54,260,0,0,1280,720,,
|
||||||
|
54,261,0,0,1280,720,,FF000000
|
||||||
|
54,262,307,331,666,65,,FFFFFFFF
|
||||||
|
55,265,0,0,1280,720,,
|
||||||
|
55,266,0,0,1280,720,,FF000000
|
||||||
|
55,267,307,331,666,65,,FFFFFFFF
|
||||||
|
56,270,0,0,1280,720,,
|
||||||
|
56,271,0,0,1280,720,,FF000000
|
||||||
|
56,272,307,331,666,65,,FFFFFFFF
|
||||||
|
57,275,0,0,1280,720,,
|
||||||
|
57,276,0,0,1280,720,,FF000000
|
||||||
|
57,277,307,331,666,65,,FFFFFFFF
|
||||||
|
58,280,0,0,1280,720,,
|
||||||
|
58,281,0,0,1280,720,,FF000000
|
||||||
|
58,282,307,331,666,65,,FFFFFFFF
|
||||||
|
59,285,0,0,1280,720,,
|
||||||
|
59,286,0,0,1280,720,,FF000000
|
||||||
|
59,287,307,331,666,65,,FFFFFFFF
|
||||||
|
60,290,0,0,1280,720,,
|
||||||
|
60,291,0,0,1280,720,,FF000000
|
||||||
|
60,292,307,331,666,65,,FFFFFFFF
|
||||||
|
61,295,0,0,1280,720,,
|
||||||
|
61,296,0,0,1280,720,,FF000000
|
||||||
|
61,297,307,331,666,65,,FFFFFFFF
|
||||||
|
62,300,0,0,1280,720,,
|
||||||
|
62,301,0,0,1280,720,,FF000000
|
||||||
|
62,302,307,331,666,65,,FFFFFFFF
|
||||||
|
63,305,0,0,1280,720,,
|
||||||
|
63,306,0,0,1280,720,,FF000000
|
||||||
|
63,307,307,331,666,65,,FFFFFFFF
|
||||||
|
64,310,0,0,1280,720,,
|
||||||
|
64,311,0,0,1280,720,,FF000000
|
||||||
|
64,312,307,331,666,65,,FFFFFFFF
|
||||||
|
65,315,0,0,1280,720,,
|
||||||
|
65,316,0,0,1280,720,,FF000000
|
||||||
|
65,317,307,331,666,65,,FFFFFFFF
|
||||||
|
66,320,0,0,1280,720,,
|
||||||
|
66,321,0,0,1280,720,,FF000000
|
||||||
|
66,322,307,331,666,65,,FFFFFFFF
|
||||||
|
67,325,0,0,1280,720,,
|
||||||
|
67,326,0,0,1280,720,,FF000000
|
||||||
|
67,327,307,331,666,65,,FFFFFFFF
|
||||||
|
68,330,0,0,1280,720,,
|
||||||
|
68,331,0,0,1280,720,,FF000000
|
||||||
|
68,332,307,331,666,65,,FFFFFFFF
|
||||||
|
69,335,0,0,1280,720,,
|
||||||
|
69,336,0,0,1280,720,,FF000000
|
||||||
|
69,337,307,331,666,65,,FFFFFFFF
|
||||||
|
70,340,0,0,1280,720,,
|
||||||
|
70,341,0,0,1280,720,,FF000000
|
||||||
|
70,342,307,331,666,65,,FFFFFFFF
|
||||||
|
71,345,0,0,1280,720,,
|
||||||
|
71,346,0,0,1280,720,,FF000000
|
||||||
|
71,347,307,331,666,65,,FFFFFFFF
|
||||||
|
72,350,0,0,1280,720,,
|
||||||
|
72,351,0,0,1280,720,,FF000000
|
||||||
|
72,352,307,331,666,65,,FFFFFFFF
|
||||||
|
73,355,0,0,1280,720,,
|
||||||
|
73,356,0,0,1280,720,,FF000000
|
||||||
|
73,357,307,331,666,65,,FFFFFFFF
|
||||||
|
74,360,0,0,1280,720,,
|
||||||
|
74,361,0,0,1280,720,,FF000000
|
||||||
|
74,362,307,331,666,65,,FFFFFFFF
|
||||||
|
75,365,0,0,1280,720,,
|
||||||
|
75,366,0,0,1280,720,,FF000000
|
||||||
|
75,367,307,331,666,65,,FFFFFFFF
|
||||||
|
76,370,0,0,1280,720,,
|
||||||
|
76,371,0,0,1280,720,,FF000000
|
||||||
|
76,372,307,331,666,65,,FFFFFFFF
|
||||||
|
77,375,0,0,1280,720,,
|
||||||
|
77,376,0,0,1280,720,,FF000000
|
||||||
|
77,377,307,331,666,65,,FFFFFFFF
|
||||||
|
78,380,0,0,1280,720,,
|
||||||
|
78,381,0,0,1280,720,,FF000000
|
||||||
|
78,382,307,331,666,65,,FEFFFFFF
|
||||||
|
79,385,0,0,1280,720,,
|
||||||
|
79,386,0,0,1280,720,,FF000000
|
||||||
|
79,387,307,331,666,65,,F9FFFFFF
|
||||||
|
80,390,0,0,1280,720,,
|
||||||
|
80,391,0,0,1280,720,,FF000000
|
||||||
|
80,392,307,331,666,65,,E7FFFFFF
|
||||||
|
81,395,0,0,1280,720,,
|
||||||
|
81,396,0,0,1280,720,,FF000000
|
||||||
|
81,397,307,331,666,65,,D7FFFFFF
|
||||||
|
82,400,0,0,1280,720,,
|
||||||
|
82,401,0,0,1280,720,,FF000000
|
||||||
|
82,402,307,331,666,65,,B5FFFFFF
|
||||||
|
83,405,0,0,1280,720,,
|
||||||
|
83,406,0,0,1280,720,,FF000000
|
||||||
|
83,407,307,331,666,65,,A5FFFFFF
|
||||||
|
84,410,0,0,1280,720,,
|
||||||
|
84,411,0,0,1280,720,,FF000000
|
||||||
|
84,412,307,331,666,65,,83FFFFFF
|
||||||
|
85,415,0,0,1280,720,,
|
||||||
|
85,416,0,0,1280,720,,FF000000
|
||||||
|
85,417,307,331,666,65,,73FFFFFF
|
||||||
|
86,420,0,0,1280,720,,
|
||||||
|
86,421,0,0,1280,720,,FF000000
|
||||||
|
86,422,307,331,666,65,,51FFFFFF
|
||||||
|
87,425,0,0,1280,720,,
|
||||||
|
87,426,0,0,1280,720,,FF000000
|
||||||
|
87,427,307,331,666,65,,41FFFFFF
|
||||||
|
88,430,0,0,1280,720,,
|
||||||
|
88,431,0,0,1280,720,,FF000000
|
||||||
|
88,432,307,331,666,65,,1FFFFFFF
|
||||||
|
89,435,0,0,1280,720,,
|
||||||
|
89,436,0,0,1280,720,,FF000000
|
||||||
|
89,437,307,331,666,65,,0FFFFFFF
|
||||||
|
90,440,0,0,1280,720,,
|
||||||
|
90,441,0,0,1280,720,,FF000000
|
||||||
|
90,442,307,331,666,65,,07FFFFFF
|
||||||
|
91,445,0,0,1280,720,,
|
||||||
|
91,446,0,0,1280,720,,FF000000
|
||||||
|
92,449,0,0,1280,720,,
|
||||||
|
92,450,0,0,1280,720,,FF000000
|
||||||
|
93,453,0,0,1280,720,,
|
||||||
|
93,454,0,0,1280,720,,FF000000
|
||||||
|
94,457,0,0,1280,720,,
|
||||||
|
94,458,0,0,1280,720,,FF000000
|
||||||
|
94,459,378,155,525,90,,22FFFFFF
|
||||||
|
94,459,512,306,262,108,,22FFFFFF
|
||||||
|
95,462,0,0,1280,720,,
|
||||||
|
95,463,0,0,1280,720,,FF000000
|
||||||
|
95,464,378,155,525,90,,44FFFFFF
|
||||||
|
95,464,512,306,262,108,,44FFFFFF
|
||||||
|
96,467,0,0,1280,720,,
|
||||||
|
96,468,0,0,1280,720,,FF000000
|
||||||
|
96,469,378,155,525,90,,66FFFFFF
|
||||||
|
96,469,512,306,262,108,,66FFFFFF
|
||||||
|
97,472,0,0,1280,720,,
|
||||||
|
97,473,0,0,1280,720,,FF000000
|
||||||
|
97,474,378,155,525,90,,88FFFFFF
|
||||||
|
97,474,512,306,262,108,,88FFFFFF
|
||||||
|
98,477,0,0,1280,720,,
|
||||||
|
98,478,0,0,1280,720,,FF000000
|
||||||
|
98,479,378,155,525,90,,AAFFFFFF
|
||||||
|
98,479,512,306,262,108,,AAFFFFFF
|
||||||
|
99,482,0,0,1280,720,,
|
||||||
|
99,483,0,0,1280,720,,FF000000
|
||||||
|
99,484,378,155,525,90,,CCFFFFFF
|
||||||
|
99,484,512,306,262,108,,CCFFFFFF
|
||||||
|
100,487,0,0,1280,720,,
|
||||||
|
100,488,0,0,1280,720,,FF000000
|
||||||
|
100,489,378,155,525,90,,EEFFFFFF
|
||||||
|
100,489,512,306,262,108,,EEFFFFFF
|
||||||
|
101,492,0,0,1280,720,,
|
||||||
|
101,493,0,0,1280,720,,FF000000
|
||||||
|
101,494,378,155,525,90,,FFFFFFFF
|
||||||
|
101,494,512,306,262,108,,FFFFFFFF
|
||||||
|
102,497,0,0,1280,720,,
|
||||||
|
102,498,0,0,1280,720,,FF000000
|
||||||
|
102,499,378,155,525,90,,FFFFFFFF
|
||||||
|
102,499,512,306,262,108,,FFFFFFFF
|
||||||
|
103,502,0,0,1280,720,,
|
||||||
|
103,503,0,0,1280,720,,FF000000
|
||||||
|
103,504,378,155,525,90,,FFFFFFFF
|
||||||
|
103,504,512,306,262,108,,FFFFFFFF
|
||||||
|
104,507,0,0,1280,720,,
|
||||||
|
104,508,0,0,1280,720,,FF000000
|
||||||
|
104,509,378,155,525,90,,FFFFFFFF
|
||||||
|
104,509,512,306,262,108,,FFFFFFFF
|
||||||
|
105,512,0,0,1280,720,,
|
||||||
|
105,513,0,0,1280,720,,FF000000
|
||||||
|
105,514,378,155,525,90,,FFFFFFFF
|
||||||
|
105,514,512,306,262,108,,FFFFFFFF
|
||||||
|
106,517,0,0,1280,720,,
|
||||||
|
106,518,0,0,1280,720,,FF000000
|
||||||
|
106,519,378,155,525,90,,FFFFFFFF
|
||||||
|
106,519,512,306,262,108,,FFFFFFFF
|
||||||
|
107,522,0,0,1280,720,,
|
||||||
|
107,523,0,0,1280,720,,FF000000
|
||||||
|
107,524,378,155,525,90,,FFFFFFFF
|
||||||
|
107,524,512,306,262,108,,FFFFFFFF
|
||||||
|
108,527,0,0,1280,720,,
|
||||||
|
108,528,0,0,1280,720,,FF000000
|
||||||
|
108,529,378,155,525,90,,FEFFFFFF
|
||||||
|
108,529,512,306,262,108,,FEFFFFFF
|
||||||
|
109,532,0,0,1280,720,,
|
||||||
|
109,533,0,0,1280,720,,FF000000
|
||||||
|
109,534,378,155,525,90,,DCFFFFFF
|
||||||
|
109,534,512,306,262,108,,DCFFFFFF
|
||||||
|
110,537,0,0,1280,720,,
|
||||||
|
110,538,0,0,1280,720,,FF000000
|
||||||
|
110,539,378,155,525,90,,BAFFFFFF
|
||||||
|
110,539,512,306,262,108,,BAFFFFFF
|
||||||
|
111,542,0,0,1280,720,,
|
||||||
|
111,543,0,0,1280,720,,FF000000
|
||||||
|
111,544,378,155,525,90,,98FFFFFF
|
||||||
|
111,544,512,306,262,108,,98FFFFFF
|
||||||
|
112,547,0,0,1280,720,,
|
||||||
|
112,548,0,0,1280,720,,FF000000
|
||||||
|
112,549,378,155,525,90,,76FFFFFF
|
||||||
|
112,549,512,306,262,108,,76FFFFFF
|
||||||
|
113,552,0,0,1280,720,,
|
||||||
|
113,553,0,0,1280,720,,FF000000
|
||||||
|
113,554,378,155,525,90,,54FFFFFF
|
||||||
|
113,554,512,306,262,108,,54FFFFFF
|
||||||
|
114,557,0,0,1280,720,,
|
||||||
|
114,558,0,0,1280,720,,FF000000
|
||||||
|
114,559,378,155,525,90,,43FFFFFF
|
||||||
|
114,559,512,306,262,108,,43FFFFFF
|
||||||
|
115,562,0,0,1280,720,,
|
||||||
|
115,563,0,0,1280,720,,FF000000
|
||||||
|
115,564,378,155,525,90,,21FFFFFF
|
||||||
|
115,564,512,306,262,108,,21FFFFFF
|
||||||
|
116,567,0,0,1280,720,,
|
||||||
|
116,568,0,0,1280,720,,FF000000
|
||||||
|
116,569,390,162,499,72,,FFFFFFFF
|
||||||
|
116,569,518,317,243,86,,FFFFFFFF
|
||||||
|
117,572,0,0,1280,720,,
|
||||||
|
117,573,0,0,1280,720,,FF000000
|
||||||
|
117,574,390,162,499,72,,FFFFFFFF
|
||||||
|
117,574,518,317,243,86,,FFFFFFFF
|
||||||
|
118,577,0,0,1280,720,,
|
||||||
|
118,578,0,0,1280,720,,FF000000
|
||||||
|
118,579,390,162,499,72,,FFFFFFFF
|
||||||
|
118,579,518,317,243,86,,FFFFFFFF
|
||||||
|
119,582,0,0,1280,720,,
|
||||||
|
119,583,0,0,1280,720,,FF000000
|
||||||
|
119,584,390,162,499,72,,FFFFFFFF
|
||||||
|
119,584,518,317,243,86,,FFFFFFFF
|
||||||
|
120,587,0,0,1280,720,,
|
||||||
|
120,588,0,0,1280,720,,FF000000
|
||||||
|
120,589,390,162,499,72,,FFFFFFFF
|
||||||
|
120,589,518,317,243,86,,FFFFFFFF
|
||||||
|
121,592,0,0,1280,720,,
|
||||||
|
121,593,0,0,1280,720,,FF000000
|
||||||
|
121,594,390,162,499,72,,FFFFFFFF
|
||||||
|
121,594,518,317,243,86,,FFFFFFFF
|
||||||
|
122,597,0,0,1280,720,,
|
||||||
|
122,598,0,0,1280,720,,FF000000
|
||||||
|
122,599,390,162,499,72,,FFFFFFFF
|
||||||
|
122,599,518,317,243,86,,FFFFFFFF
|
||||||
|
123,602,0,0,1280,720,,
|
||||||
|
123,603,0,0,1280,720,,FF000000
|
||||||
|
123,604,390,162,499,72,,FFFFFFFF
|
||||||
|
123,604,518,317,243,86,,FFFFFFFF
|
||||||
|
124,607,0,0,1280,720,,
|
||||||
|
124,608,0,0,1280,720,,FF000000
|
||||||
|
124,609,390,162,499,72,,FFFFFFFF
|
||||||
|
124,609,518,317,243,86,,FFFFFFFF
|
||||||
|
125,612,0,0,1280,720,,
|
||||||
|
125,613,0,0,1280,720,,FF000000
|
||||||
|
125,614,390,162,499,72,,FFFFFFFF
|
||||||
|
125,614,518,317,243,86,,FFFFFFFF
|
||||||
|
126,617,0,0,1280,720,,
|
||||||
|
126,618,0,0,1280,720,,FF000000
|
||||||
|
126,619,390,162,499,72,,FFFFFFFF
|
||||||
|
126,619,518,317,243,86,,FFFFFFFF
|
||||||
|
127,622,0,0,1280,720,,
|
||||||
|
127,623,0,0,1280,720,,FF000000
|
||||||
|
127,624,390,162,499,72,,FFFFFFFF
|
||||||
|
127,624,518,317,243,86,,FFFFFFFF
|
||||||
|
128,627,0,0,1280,720,,
|
||||||
|
128,628,0,0,1280,720,,FF000000
|
||||||
|
128,629,390,162,499,72,,FFFFFFFF
|
||||||
|
128,629,518,317,243,86,,FFFFFFFF
|
||||||
|
129,632,0,0,1280,720,,
|
||||||
|
129,633,0,0,1280,720,,FF000000
|
||||||
|
129,634,390,162,499,72,,FFFFFFFF
|
||||||
|
129,634,518,317,243,86,,FFFFFFFF
|
||||||
|
130,637,0,0,1280,720,,
|
||||||
|
130,638,0,0,1280,720,,FF000000
|
||||||
|
130,639,390,162,499,72,,FFFFFFFF
|
||||||
|
130,639,518,317,243,86,,FFFFFFFF
|
||||||
|
131,642,0,0,1280,720,,
|
||||||
|
131,643,0,0,1280,720,,FF000000
|
||||||
|
131,644,390,162,499,72,,FFFFFFFF
|
||||||
|
131,644,518,317,243,86,,FFFFFFFF
|
||||||
|
132,647,0,0,1280,720,,
|
||||||
|
132,648,0,0,1280,720,,FF000000
|
||||||
|
132,649,390,162,499,72,,FFFFFFFF
|
||||||
|
132,649,518,317,243,86,,FFFFFFFF
|
||||||
|
133,652,0,0,1280,720,,
|
||||||
|
133,653,0,0,1280,720,,FF000000
|
||||||
|
133,654,390,162,499,72,,FFFFFFFF
|
||||||
|
133,654,518,317,243,86,,FFFFFFFF
|
||||||
|
134,657,0,0,1280,720,,
|
||||||
|
134,658,0,0,1280,720,,FF000000
|
||||||
|
134,659,390,162,499,72,,FFFFFFFF
|
||||||
|
134,659,518,317,243,86,,FFFFFFFF
|
||||||
|
135,662,0,0,1280,720,,
|
||||||
|
135,663,0,0,1280,720,,FF000000
|
||||||
|
135,664,390,162,499,72,,FFFFFFFF
|
||||||
|
135,664,518,317,243,86,,FFFFFFFF
|
||||||
|
136,667,0,0,1280,720,,
|
||||||
|
136,668,0,0,1280,720,,FF000000
|
||||||
|
136,669,390,162,499,72,,FFFFFFFF
|
||||||
|
136,669,518,317,243,86,,FFFFFFFF
|
||||||
|
137,672,0,0,1280,720,,
|
||||||
|
137,673,0,0,1280,720,,FF000000
|
||||||
|
137,674,390,162,499,72,,FFFFFFFF
|
||||||
|
137,674,518,317,243,86,,FFFFFFFF
|
||||||
|
138,677,0,0,1280,720,,
|
||||||
|
138,678,0,0,1280,720,,FF000000
|
||||||
|
138,679,390,162,499,72,,FFFFFFFF
|
||||||
|
138,679,518,317,243,86,,FFFFFFFF
|
||||||
|
140,682,0,0,1280,720,,
|
||||||
|
140,683,0,0,1280,720,,FF000000
|
||||||
|
140,684,390,162,499,72,,FFFFFFFF
|
||||||
|
140,684,518,317,243,86,,FFFFFFFF
|
||||||
|
141,687,0,0,1280,720,,
|
||||||
|
141,688,0,0,1280,720,,FF000000
|
||||||
|
141,689,390,162,499,72,,FFFFFFFF
|
||||||
|
141,689,518,317,243,86,,FFFFFFFF
|
||||||
|
142,692,0,0,1280,720,,
|
||||||
|
142,693,0,0,1280,720,,FF000000
|
||||||
|
142,694,390,162,499,72,,FFFFFFFF
|
||||||
|
142,694,518,317,243,86,,FFFFFFFF
|
||||||
|
143,697,0,0,1280,720,,
|
||||||
|
143,698,0,0,1280,720,,FF000000
|
||||||
|
143,699,390,162,499,72,,FFFFFFFF
|
||||||
|
143,699,518,317,243,86,,FFFFFFFF
|
||||||
|
144,702,0,0,1280,720,,
|
||||||
|
144,703,0,0,1280,720,,FF000000
|
||||||
|
144,704,390,162,499,72,,FFFFFFFF
|
||||||
|
144,704,518,317,243,86,,FFFFFFFF
|
||||||
|
145,707,0,0,1280,720,,
|
||||||
|
145,708,0,0,1280,720,,FF000000
|
||||||
|
145,709,390,162,499,72,,FFFFFFFF
|
||||||
|
145,709,518,317,243,86,,FFFFFFFF
|
||||||
|
146,712,0,0,1280,720,,
|
||||||
|
146,713,0,0,1280,720,,FF000000
|
||||||
|
146,714,390,162,499,72,,FFFFFFFF
|
||||||
|
146,714,518,317,243,86,,FFFFFFFF
|
||||||
|
147,717,0,0,1280,720,,
|
||||||
|
147,718,0,0,1280,720,,FF000000
|
||||||
|
147,719,390,162,499,72,,FFFFFFFF
|
||||||
|
147,719,518,317,243,86,,FFFFFFFF
|
||||||
|
148,722,0,0,1280,720,,
|
||||||
|
148,723,0,0,1280,720,,FF000000
|
||||||
|
148,724,390,162,499,72,,FFFFFFFF
|
||||||
|
148,724,518,317,243,86,,FFFFFFFF
|
||||||
|
149,727,0,0,1280,720,,
|
||||||
|
149,728,0,0,1280,720,,FF000000
|
||||||
|
149,729,390,162,499,72,,FFFFFFFF
|
||||||
|
149,729,518,317,243,86,,FFFFFFFF
|
||||||
|
150,732,0,0,1280,720,,
|
||||||
|
150,733,0,0,1280,720,,FF000000
|
||||||
|
150,734,390,162,499,72,,FFFFFFFF
|
||||||
|
150,734,518,317,243,86,,FFFFFFFF
|
||||||
|
151,737,0,0,1280,720,,
|
||||||
|
151,738,0,0,1280,720,,FF000000
|
||||||
|
151,739,390,162,499,72,,FFFFFFFF
|
||||||
|
151,739,518,317,243,86,,FFFFFFFF
|
||||||
|
152,742,0,0,1280,720,,
|
||||||
|
152,743,0,0,1280,720,,FF000000
|
||||||
|
152,744,390,162,499,72,,FFFFFFFF
|
||||||
|
152,744,518,317,243,86,,FFFFFFFF
|
||||||
|
153,747,0,0,1280,720,,
|
||||||
|
153,748,0,0,1280,720,,FF000000
|
||||||
|
153,749,390,162,499,72,,FFFFFFFF
|
||||||
|
153,749,518,317,243,86,,FFFFFFFF
|
||||||
|
154,752,0,0,1280,720,,
|
||||||
|
154,753,0,0,1280,720,,FF000000
|
||||||
|
154,754,390,162,499,72,,FFFFFFFF
|
||||||
|
154,754,518,317,243,86,,FFFFFFFF
|
||||||
|
155,757,0,0,1280,720,,
|
||||||
|
155,758,0,0,1280,720,,FF000000
|
||||||
|
155,759,390,162,499,72,,FFFFFFFF
|
||||||
|
155,759,518,317,243,86,,FFFFFFFF
|
||||||
|
156,762,0,0,1280,720,,
|
||||||
|
156,763,0,0,1280,720,,FF000000
|
||||||
|
156,764,390,162,499,72,,FFFFFFFF
|
||||||
|
156,764,518,317,243,86,,FFFFFFFF
|
||||||
|
157,767,0,0,1280,720,,
|
||||||
|
157,768,0,0,1280,720,,FF000000
|
||||||
|
157,769,390,162,499,72,,FFFFFFFF
|
||||||
|
157,769,518,317,243,86,,FFFFFFFF
|
||||||
|
158,772,0,0,1280,720,,
|
||||||
|
158,773,0,0,1280,720,,FF000000
|
||||||
|
158,774,390,162,499,72,,FFFFFFFF
|
||||||
|
158,774,518,317,243,86,,FFFFFFFF
|
||||||
|
159,777,0,0,1280,720,,
|
||||||
|
159,778,0,0,1280,720,,FF000000
|
||||||
|
159,779,390,162,499,72,,FFFFFFFF
|
||||||
|
159,779,518,317,243,86,,FFFFFFFF
|
||||||
|
160,782,0,0,1280,720,,
|
||||||
|
160,783,0,0,1280,720,,FF000000
|
||||||
|
160,784,390,162,499,72,,FFFFFFFF
|
||||||
|
160,784,518,317,243,86,,FFFFFFFF
|
||||||
|
161,787,0,0,1280,720,,
|
||||||
|
161,788,0,0,1280,720,,FF000000
|
||||||
|
161,789,390,162,499,72,,FFFFFFFF
|
||||||
|
161,789,518,317,243,86,,FFFFFFFF
|
||||||
|
162,792,0,0,1280,720,,
|
||||||
|
162,793,0,0,1280,720,,FF000000
|
||||||
|
162,794,390,162,499,72,,FFFFFFFF
|
||||||
|
162,794,518,317,243,86,,FFFFFFFF
|
||||||
|
163,797,0,0,1280,720,,
|
||||||
|
163,798,0,0,1280,720,,FF000000
|
||||||
|
163,799,390,162,499,72,,FFFFFFFF
|
||||||
|
163,799,518,317,243,86,,FFFFFFFF
|
||||||
|
164,802,0,0,1280,720,,
|
||||||
|
164,803,0,0,1280,720,,FF000000
|
||||||
|
164,804,390,162,499,72,,FFFFFFFF
|
||||||
|
164,804,518,317,243,86,,FFFFFFFF
|
||||||
|
165,807,0,0,1280,720,,
|
||||||
|
165,808,0,0,1280,720,,FF000000
|
||||||
|
165,809,390,162,499,72,,FFFFFFFF
|
||||||
|
165,809,518,317,243,86,,FFFFFFFF
|
||||||
|
166,812,0,0,1280,720,,
|
||||||
|
166,813,0,0,1280,720,,FF000000
|
||||||
|
166,814,390,162,499,72,,FFFFFFFF
|
||||||
|
166,814,518,317,243,86,,FFFFFFFF
|
||||||
|
167,817,0,0,1280,720,,
|
||||||
|
167,818,0,0,1280,720,,FF000000
|
||||||
|
167,819,390,162,499,72,,FFFFFFFF
|
||||||
|
167,819,518,317,243,86,,FFFFFFFF
|
||||||
|
168,822,0,0,1280,720,,
|
||||||
|
168,823,0,0,1280,720,,FF000000
|
||||||
|
168,824,390,162,499,72,,FFFFFFFF
|
||||||
|
168,824,518,317,243,86,,FFFFFFFF
|
||||||
|
169,827,0,0,1280,720,,
|
||||||
|
169,828,0,0,1280,720,,FF000000
|
||||||
|
169,829,390,162,499,72,,FFFFFFFF
|
||||||
|
169,829,518,317,243,86,,FFFFFFFF
|
||||||
|
170,832,0,0,1280,720,,
|
||||||
|
170,833,0,0,1280,720,,FF000000
|
||||||
|
170,834,390,162,499,72,,FFFFFFFF
|
||||||
|
170,834,518,317,243,86,,FFFFFFFF
|
||||||
|
171,837,0,0,1280,720,,
|
||||||
|
171,838,0,0,1280,720,,FF000000
|
||||||
|
171,839,390,162,499,72,,FFFFFFFF
|
||||||
|
171,839,518,317,243,86,,FFFFFFFF
|
||||||
|
172,842,0,0,1280,720,,
|
||||||
|
172,843,0,0,1280,720,,FF000000
|
||||||
|
172,844,390,162,499,72,,FFFFFFFF
|
||||||
|
172,844,518,317,243,86,,FFFFFFFF
|
||||||
|
173,847,0,0,1280,720,,
|
||||||
|
173,848,0,0,1280,720,,FF000000
|
||||||
|
173,849,390,162,499,72,,FFFFFFFF
|
||||||
|
173,849,518,317,243,86,,FFFFFFFF
|
||||||
|
174,852,0,0,1280,720,,
|
||||||
|
174,853,0,0,1280,720,,FF000000
|
||||||
|
174,854,390,162,499,72,,FFFFFFFF
|
||||||
|
174,854,518,317,243,86,,FFFFFFFF
|
||||||
|
175,857,0,0,1280,720,,
|
||||||
|
175,858,0,0,1280,720,,FF000000
|
||||||
|
175,859,390,162,499,72,,FFFFFFFF
|
||||||
|
175,859,518,317,243,86,,FFFFFFFF
|
||||||
|
176,862,0,0,1280,720,,
|
||||||
|
176,863,0,0,1280,720,,FF000000
|
||||||
|
176,864,390,162,499,72,,FFFFFFFF
|
||||||
|
176,864,518,317,243,86,,FFFFFFFF
|
||||||
|
177,867,0,0,1280,720,,
|
||||||
|
177,868,0,0,1280,720,,FF000000
|
||||||
|
177,869,390,162,499,72,,FFFFFFFF
|
||||||
|
177,869,518,317,243,86,,FFFFFFFF
|
||||||
|
178,872,0,0,1280,720,,
|
||||||
|
178,873,0,0,1280,720,,FF000000
|
||||||
|
178,874,390,162,499,72,,FFFFFFFF
|
||||||
|
178,874,518,317,243,86,,FFFFFFFF
|
||||||
|
179,877,0,0,1280,720,,
|
||||||
|
179,878,0,0,1280,720,,FF000000
|
||||||
|
179,879,390,162,499,72,,FFFFFFFF
|
||||||
|
179,879,518,317,243,86,,FFFFFFFF
|
||||||
|
180,882,0,0,1280,720,,
|
||||||
|
180,883,0,0,1280,720,,FF000000
|
||||||
|
180,884,390,162,499,72,,FFFFFFFF
|
||||||
|
180,884,518,317,243,86,,FFFFFFFF
|
||||||
|
181,887,0,0,1280,720,,
|
||||||
|
181,888,0,0,1280,720,,FF000000
|
||||||
|
181,889,390,162,499,72,,FFFFFFFF
|
||||||
|
181,889,518,317,243,86,,FFFFFFFF
|
||||||
|
182,892,0,0,1280,720,,
|
||||||
|
182,893,0,0,1280,720,,FF000000
|
||||||
|
182,894,390,162,499,72,,FFFFFFFF
|
||||||
|
182,894,518,317,243,86,,FFFFFFFF
|
||||||
|
183,897,0,0,1280,720,,
|
||||||
|
183,898,0,0,1280,720,,FF000000
|
||||||
|
183,899,390,162,499,72,,FFFFFFFF
|
||||||
|
183,899,518,317,243,86,,FFFFFFFF
|
||||||
|
184,902,0,0,1280,720,,
|
||||||
|
184,903,0,0,1280,720,,FF000000
|
||||||
|
184,904,390,162,499,72,,FFFFFFFF
|
||||||
|
184,904,518,317,243,86,,FFFFFFFF
|
||||||
|
185,907,0,0,1280,720,,
|
||||||
|
185,908,0,0,1280,720,,FF000000
|
||||||
|
185,909,390,162,499,72,,FFFFFFFF
|
||||||
|
185,909,518,317,243,86,,FFFFFFFF
|
||||||
|
186,912,0,0,1280,720,,
|
||||||
|
186,913,0,0,1280,720,,FF000000
|
||||||
|
186,914,390,162,499,72,,FFFFFFFF
|
||||||
|
186,914,518,317,243,86,,FFFFFFFF
|
||||||
|
187,917,0,0,1280,720,,
|
||||||
|
187,918,0,0,1280,720,,FF000000
|
||||||
|
187,919,390,162,499,72,,FFFFFFFF
|
||||||
|
187,919,518,317,243,86,,FFFFFFFF
|
||||||
|
188,922,0,0,1280,720,,
|
||||||
|
188,923,0,0,1280,720,,FF000000
|
||||||
|
188,924,390,162,499,72,,FFFFFFFF
|
||||||
|
188,924,518,317,243,86,,FFFFFFFF
|
||||||
|
189,927,0,0,1280,720,,
|
||||||
|
189,928,0,0,1280,720,,FF000000
|
||||||
|
189,929,390,162,499,72,,FFFFFFFF
|
||||||
|
189,929,518,317,243,86,,FFFFFFFF
|
||||||
|
190,932,0,0,1280,720,,
|
||||||
|
190,933,0,0,1280,720,,FF000000
|
||||||
|
190,934,390,162,499,72,,FFFFFFFF
|
||||||
|
190,934,518,317,243,86,,FFFFFFFF
|
||||||
|
191,937,0,0,1280,720,,
|
||||||
|
191,938,0,0,1280,720,,FF000000
|
||||||
|
191,939,390,162,499,72,,FFFFFFFF
|
||||||
|
191,939,518,317,243,86,,FFFFFFFF
|
||||||
|
192,942,0,0,1280,720,,
|
||||||
|
192,943,0,0,1280,720,,FF000000
|
||||||
|
192,944,390,162,499,72,,FFFFFFFF
|
||||||
|
192,944,518,317,243,86,,FFFFFFFF
|
||||||
|
193,947,0,0,1280,720,,
|
||||||
|
193,948,0,0,1280,720,,FF000000
|
||||||
|
193,949,390,162,499,72,,FFFFFFFF
|
||||||
|
193,949,518,317,243,86,,FFFFFFFF
|
||||||
|
194,952,0,0,1280,720,,
|
||||||
|
194,953,0,0,1280,720,,FF000000
|
||||||
|
194,954,390,162,499,72,,FFFFFFFF
|
||||||
|
194,954,518,317,243,86,,FFFFFFFF
|
||||||
|
195,957,0,0,1280,720,,
|
||||||
|
195,958,0,0,1280,720,,FF000000
|
||||||
|
195,959,390,162,499,72,,FFFFFFFF
|
||||||
|
195,959,518,317,243,86,,FFFFFFFF
|
||||||
|
196,962,0,0,1280,720,,
|
||||||
|
196,963,0,0,1280,720,,FF000000
|
||||||
|
196,964,390,162,499,72,,FFFFFFFF
|
||||||
|
196,964,518,317,243,86,,FFFFFFFF
|
||||||
|
197,967,0,0,1280,720,,
|
||||||
|
197,968,0,0,1280,720,,FF000000
|
||||||
|
197,969,390,162,499,72,,FFFFFFFF
|
||||||
|
197,969,518,317,243,86,,FFFFFFFF
|
||||||
|
198,972,0,0,1280,720,,
|
||||||
|
198,973,0,0,1280,720,,FF000000
|
||||||
|
198,974,390,162,499,72,,FFFFFFFF
|
||||||
|
198,974,518,317,243,86,,FFFFFFFF
|
||||||
|
199,977,0,0,1280,720,,
|
||||||
|
199,978,0,0,1280,720,,FF000000
|
||||||
|
199,979,390,162,499,72,,FEFFFFFF
|
||||||
|
199,979,518,317,243,86,,FEFFFFFF
|
||||||
|
200,982,0,0,1280,720,,
|
||||||
|
200,983,0,0,1280,720,,FF000000
|
||||||
|
200,984,390,162,499,72,,F3FFFFFF
|
||||||
|
200,984,518,317,243,86,,F3FFFFFF
|
||||||
|
201,987,0,0,1280,720,,
|
||||||
|
201,988,0,0,1280,720,,FF000000
|
||||||
|
201,989,390,162,499,72,,EDFFFFFF
|
||||||
|
201,989,518,317,243,86,,EDFFFFFF
|
||||||
|
202,992,0,0,1280,720,,
|
||||||
|
202,993,0,0,1280,720,,FF000000
|
||||||
|
202,994,390,162,499,72,,D7FFFFFF
|
||||||
|
202,994,518,317,243,86,,D7FFFFFF
|
||||||
|
203,997,0,0,1280,720,,
|
||||||
|
203,998,0,0,1280,720,,FF000000
|
||||||
|
203,999,390,162,499,72,,B5FFFFFF
|
||||||
|
203,999,518,317,243,86,,B5FFFFFF
|
||||||
|
204,1002,0,0,1280,720,,
|
||||||
|
204,1003,0,0,1280,720,,FF000000
|
||||||
|
204,1004,390,162,499,72,,94FFFFFF
|
||||||
|
204,1004,518,317,243,86,,94FFFFFF
|
||||||
|
205,1007,0,0,1280,720,,
|
||||||
|
205,1008,0,0,1280,720,,FF000000
|
||||||
|
205,1009,390,162,499,72,,83FFFFFF
|
||||||
|
205,1009,518,317,243,86,,83FFFFFF
|
||||||
|
206,1012,0,0,1280,720,,
|
||||||
|
206,1013,0,0,1280,720,,FF000000
|
||||||
|
206,1014,390,162,499,72,,62FFFFFF
|
||||||
|
206,1014,518,317,243,86,,62FFFFFF
|
||||||
|
207,1017,0,0,1280,720,,
|
||||||
|
207,1018,0,0,1280,720,,FF000000
|
||||||
|
207,1019,390,162,499,72,,41FFFFFF
|
||||||
|
207,1019,518,317,243,86,,41FFFFFF
|
||||||
|
208,1022,0,0,1280,720,,
|
||||||
|
208,1023,0,0,1280,720,,FF000000
|
||||||
|
208,1024,390,162,499,72,,30FFFFFF
|
||||||
|
208,1024,518,317,243,86,,30FFFFFF
|
||||||
|
209,1027,0,0,1280,720,,
|
||||||
|
209,1028,0,0,1280,720,,FF000000
|
||||||
|
209,1029,390,162,499,72,,17FFFFFF
|
||||||
|
209,1029,518,317,243,86,,17FFFFFF
|
||||||
|
210,1032,0,0,1280,720,,
|
||||||
|
210,1033,0,0,1280,720,,FF000000
|
||||||
|
210,1034,390,162,499,72,,0FFFFFFF
|
||||||
|
210,1034,518,317,243,86,,0FFFFFFF
|
||||||
|
211,1037,0,0,1280,720,,
|
||||||
|
211,1038,0,0,1280,720,,FF000000
|
||||||
|
211,1039,390,162,499,72,,07FFFFFF
|
||||||
|
211,1039,518,317,243,86,,07FFFFFF
|
||||||
|
212,1042,0,0,1280,720,,
|
||||||
|
212,1043,0,0,1280,720,,FF000000
|
||||||
|
213,1046,0,0,1280,720,,
|
||||||
|
213,1047,0,0,1280,720,,FF000000
|
||||||
|
214,1050,0,0,1280,720,,
|
||||||
|
214,1051,0,0,1280,720,,FF000000
|
||||||
|
215,1054,0,0,1280,720,,
|
||||||
|
216,1057,0,0,1280,720,,
|
||||||
|
217,1060,0,0,1280,720,,
|
||||||
|
218,1063,0,0,1280,720,,
|
||||||
|
219,1066,0,0,1280,720,,
|
||||||
|
220,1069,0,0,1280,720,,
|
||||||
|
221,1072,0,0,1280,720,,
|
||||||
|
221,1073,0,0,1280,720,,
|
||||||
|
223,1076,0,0,1280,720,,
|
||||||
|
223,1077,0,0,1280,720,,
|
||||||
|
224,1080,0,0,1280,720,,
|
||||||
|
224,1081,0,0,1280,720,,
|
||||||
|
226,1084,0,0,1280,720,,
|
||||||
|
226,1085,0,0,1280,720,,
|
||||||
|
227,1088,0,0,1280,720,,
|
||||||
|
227,1089,0,0,1280,720,,
|
||||||
|
228,1092,0,0,1280,720,,
|
||||||
|
228,1093,0,0,1280,720,,
|
||||||
|
229,1096,0,0,1280,720,,
|
||||||
|
229,1097,0,0,1280,720,,
|
||||||
|
230,1100,0,0,1280,720,,
|
||||||
|
230,1101,0,0,1280,720,,
|
||||||
|
231,1104,0,0,1280,720,,
|
||||||
|
231,1105,0,0,1280,720,,
|
||||||
|
232,1108,0,0,1280,720,,
|
||||||
|
232,1109,0,0,1280,720,,
|
||||||
|
233,1112,0,0,1280,720,,
|
||||||
|
233,1113,0,0,1280,720,,
|
||||||
|
234,1116,0,0,1280,720,,
|
||||||
|
234,1117,0,0,1280,720,,
|
||||||
|
236,1120,0,0,1280,720,,
|
||||||
|
236,1121,0,0,1280,720,,
|
||||||
|
237,1124,0,0,1280,720,,
|
||||||
|
237,1125,0,0,1280,720,,
|
||||||
|
238,1128,0,0,1280,720,,
|
||||||
|
238,1129,0,0,1280,720,,
|
||||||
|
239,1132,0,0,1280,720,,
|
||||||
|
239,1133,0,0,1280,720,,
|
||||||
|
240,1136,0,0,1280,720,,
|
||||||
|
240,1137,0,0,1280,720,,
|
||||||
|
241,1140,0,0,1280,720,,
|
||||||
|
241,1141,0,0,1280,720,,
|
||||||
|
33
docs/re/data/attract-frame-match.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Frame-signature match of captured frames against five candidate movies.
|
||||||
|
32x18 normalised grayscale, movie frames cropped to the 675/720 the game
|
||||||
|
surface shows, 1 fps sampling, best correlation over the whole movie.
|
||||||
|
|
||||||
|
CONTROL - frames captured during the BOOT movie, known to be ADV.wmv:
|
||||||
|
p2.png -> ADV @116s 0.644 (runner-up S13A 0.438)
|
||||||
|
p3.png -> ADV @121s 0.994 (runner-up S03A 0.650)
|
||||||
|
p4.png -> S13A @ 35s 0.984 (runner-up ADV 0.926)
|
||||||
|
p5.png -> ADV @129s 0.933 (runner-up S13A 0.809)
|
||||||
|
p6.png -> ADV @134s 0.992 (runner-up S13A 0.499)
|
||||||
|
p7.png -> S15B @ 88s 0.000 (runner-up S13A 0.000)
|
||||||
|
p8.png -> S13A @ 65s 0.576 (runner-up ADV 0.534)
|
||||||
|
|
||||||
|
TEST - frames captured during the ATTRACT loop, 5 s apart:
|
||||||
|
attract01.png -> ADV @ 39s 0.990 (runner-up S13A 0.487)
|
||||||
|
attract02.png -> ADV @ 45s 0.961 (runner-up S13A 0.411)
|
||||||
|
attract03.png -> S03A @ 37s 0.731 (runner-up ADV 0.422)
|
||||||
|
attract04.png -> ADV @ 56s 0.955 (runner-up S03A 0.532)
|
||||||
|
attract05.png -> ADV @ 62s 0.934 (runner-up S15B 0.245)
|
||||||
|
attract06.png -> S15B @ 65s 0.885 (runner-up ADV 0.743)
|
||||||
|
attract07.png -> ADV @ 75s 0.961 (runner-up S03A 0.355)
|
||||||
|
attract08.png -> ADV @ 80s 0.938 (runner-up S03A 0.753)
|
||||||
|
attract09.png -> ADV @ 85s 0.974 (runner-up S03A 0.484)
|
||||||
|
attract10.png -> ADV @ 92s 0.892 (runner-up S13A 0.754)
|
||||||
|
attract11.png -> S03A @ 24s 0.606 (runner-up ADV 0.588)
|
||||||
|
attract12.png -> ADV @102s 0.994 (runner-up S03A 0.374)
|
||||||
|
attract13.png -> ADV @108s 0.977 (runner-up S13A 0.496)
|
||||||
|
attract14.png -> ADV @113s 0.836 (runner-up S06B 0.464)
|
||||||
|
attract15.png -> ADV @119s 0.958 (runner-up S15B 0.424)
|
||||||
|
attract16.png -> S13A @ 32s 0.996 (runner-up ADV 0.980)
|
||||||
|
attract17.png -> ADV @131s 0.865 (runner-up S13A 0.769)
|
||||||
|
attract18.png -> ADV @137s 0.993 (runner-up S13A 0.502)
|
||||||
|
attract19.png -> S13A @ 65s 0.551 (runner-up ADV 0.534)
|
||||||
35
docs/re/data/bgm-wave-census.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
bank waves per-wave (bytes, bytes/s, Hz, seconds)
|
||||||
|
BGM_001.slb 2 4466688 25697 48000 173.82s | 4673536 26887 48000 173.82s EQUAL-LENGTH
|
||||||
|
BGM_002.slb 2 4382720 26376 48000 166.16s | 4667392 28090 48000 166.16s EQUAL-LENGTH
|
||||||
|
BGM_003.slb 2 3555328 26201 48000 135.69s | 3414016 25159 48000 135.70s EQUAL-LENGTH
|
||||||
|
BGM_004.slb 2 6311936 32874 48000 192.00s | 5787648 30143 48000 192.01s EQUAL-LENGTH
|
||||||
|
BGM_005.slb 2 4005888 28013 48000 143.00s | 3917824 27397 48000 143.00s EQUAL-LENGTH
|
||||||
|
BGM_006.slb 2 3946496 26377 48000 149.62s | 3915776 26172 48000 149.62s EQUAL-LENGTH
|
||||||
|
BGM_007.slb 2 7565312 29269 48000 258.48s | 8099840 31337 48000 258.48s EQUAL-LENGTH
|
||||||
|
BGM_008.slb 2 3258368 29947 48000 108.80s | 3397632 31227 48000 108.80s EQUAL-LENGTH
|
||||||
|
BGM_009.slb 2 3819520 25636 48000 148.99s | 3614720 24262 48000 148.99s EQUAL-LENGTH
|
||||||
|
BGM_010.slb 2 4190208 28457 48000 147.25s | 4100096 27845 48000 147.25s EQUAL-LENGTH
|
||||||
|
BGM_011.slb 2 4222976 25232 48000 167.37s | 4311040 25758 48000 167.37s EQUAL-LENGTH
|
||||||
|
BGM_012.slb 2 5562368 31605 48000 176.00s | 5404672 30709 48000 176.00s EQUAL-LENGTH
|
||||||
|
BGM_013.slb 2 6539264 29904 48000 218.68s | 6457344 29530 48000 218.67s EQUAL-LENGTH
|
||||||
|
BGM_014.slb 2 6656000 31438 48000 211.72s | 6948864 32821 48000 211.72s EQUAL-LENGTH
|
||||||
|
BGM_015.slb 2 8017920 29230 48000 274.30s | 7974912 29074 48000 274.30s EQUAL-LENGTH
|
||||||
|
BGM_016.slb 2 3549184 28341 48000 125.23s | 2775040 22159 48000 125.23s EQUAL-LENGTH
|
||||||
|
BGM_017.slb 2 5353472 30690 48000 174.44s | 5062656 29023 48000 174.44s EQUAL-LENGTH
|
||||||
|
BGM_018.slb 2 3620864 26878 48000 134.71s | 2920448 21679 48000 134.71s EQUAL-LENGTH
|
||||||
|
BGM_019.slb 2 6834176 30665 48000 222.87s | 6713344 30123 48000 222.86s EQUAL-LENGTH
|
||||||
|
BGM_020.slb 2 4804608 29592 44100 162.36s | 4675584 28798 44100 162.36s EQUAL-LENGTH
|
||||||
|
BGM_021.slb 2 7780352 28034 44100 277.53s | 7694336 27724 44100 277.53s EQUAL-LENGTH
|
||||||
|
BGM_022.slb 2 5545984 28006 44100 198.03s | 5867520 29630 44100 198.03s EQUAL-LENGTH
|
||||||
|
BGM_023.slb 2 4962304 28001 44100 177.22s | 5044224 28463 44100 177.22s EQUAL-LENGTH
|
||||||
|
BGM_101.slb 2 4800512 27481 48000 174.68s | 2635776 15089 48000 174.68s EQUAL-LENGTH
|
||||||
|
BGM_102.slb 2 1150976 30703 48000 37.49s | 1269760 33872 48000 37.49s EQUAL-LENGTH
|
||||||
|
BGM_103.slb 2 3876864 44181 48000 87.75s | 3930112 44788 48000 87.75s EQUAL-LENGTH
|
||||||
|
BGM_104.slb 2 2672640 31503 48000 84.84s | 2674688 31527 48000 84.84s EQUAL-LENGTH
|
||||||
|
BGM_105.slb 2 3581952 35879 48000 99.83s | 3850240 38566 48000 99.84s EQUAL-LENGTH
|
||||||
|
BGM_106.slb 2 4098048 45389 48000 90.29s | 2383872 36575 48000 65.18s
|
||||||
|
BGM_107.slb 2 2418688 37109 48000 65.18s | 3696640 28676 48000 128.91s
|
||||||
|
BGM_108.slb 2 3696640 28676 48000 128.91s | 2887680 35432 48000 81.50s
|
||||||
|
BGM_109.slb 4 2932736 35985 48000 81.50s | 112640 13377 48000 8.42s | 225280 14228 48000 15.83s | 235520 14795 48000 15.92s
|
||||||
|
|
||||||
|
32 BGM banks; 28 are exactly two waves of equal duration
|
||||||
10
docs/re/data/doc-link-audit.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# tools/re-capture/doc_link_check.py -- 2026-08-29, after the repair pass
|
||||||
|
|
||||||
|
1050 link(s) resolve
|
||||||
|
|
||||||
|
5 MISSING target(s):
|
||||||
|
docs/re/autopilot-knowledge-sources.md -> ../../MEMORY.md
|
||||||
|
docs/re/challenge-mission-gate.md -> ../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h
|
||||||
|
docs/re/entities-live-roster.md -> ../../MEMORY.md
|
||||||
|
docs/re/mission-freeze-resume-spin.md -> canary-build-verified-env-confound.md
|
||||||
|
docs/re/stage-drift-is-navigation-not-save.md -> structures/weapon-datasheet-runtime.md
|
||||||
7
docs/re/data/eff-bit-alpha-test.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
matched 170 decoded textures to a T8aD flag word
|
||||||
|
bit SET n= 61 mean %(RGB>A) 55.52 median 52.52
|
||||||
|
bit clear n=109 mean %(RGB>A) 33.66 median 30.17
|
||||||
|
|
||||||
|
premultiplied would require ~0% for the flagged group -> REFUTED
|
||||||
|
best single-threshold accuracy: 76.5% at %(RGB>A) > 83.5
|
||||||
|
(base rate, always-guess-majority: 64.1%)
|
||||||
24
docs/re/data/eff-bit-census.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# tools/re-capture/eff_bit_census.py over dat/GP_*.pak -- 2026-08-29
|
||||||
|
|
||||||
|
sprites with a resolvable preceding name: 14709
|
||||||
|
bit SET & name has 'eff' : 2338
|
||||||
|
bit SET & name lacks 'eff': 2657 <-- counterexamples to 'set => eff'
|
||||||
|
bit clear & name has 'eff' : 1399
|
||||||
|
bit clear & name lacks 'eff': 8315
|
||||||
|
|
||||||
|
P(name has 'eff' | bit set) = 0.468
|
||||||
|
P(name has 'eff' | bit clear) = 0.144
|
||||||
|
|
||||||
|
counterexamples (bit set, no 'eff'):
|
||||||
|
('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126')
|
||||||
|
('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27')
|
||||||
|
('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126')
|
||||||
|
('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_ring0.t32', '00000832', '142x142')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_light0.t32', '00000832', '29x25')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_light1.t32', '00000832', '30x29')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_light2.t32', '00000832', '29x30')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_light3.t32', '00000832', '25x29')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_ring1.t32', '00000832', '72x72')
|
||||||
|
('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101')
|
||||||
2
docs/re/data/eightax-detail-test.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
main menu: corr +0.0475 controls +0.0032 (shift) -0.0075 (flip) ceiling 0.070 -> 68% of it
|
||||||
|
title: corr +0.0634 controls +0.0095 (shift) +0.0086 (flip) ceiling 0.094 -> 68% of it
|
||||||
29
docs/re/data/gamepart-class-ids.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
GamePart ids bound to a C++ class by the executable's own registration strings
|
||||||
|
(24 strings, 24 distinct ids)
|
||||||
|
|
||||||
|
0 GamePart_Title
|
||||||
|
3 GamePart_SaveLoad
|
||||||
|
4 GamePart_SaveLoad
|
||||||
|
5 GamePart_Extras
|
||||||
|
6 GamePart_MovieTheater
|
||||||
|
7 GamePart_MissionSelect
|
||||||
|
8 GamePart_Options
|
||||||
|
9 GamePart_Movie
|
||||||
|
10 GamePart_Bunk
|
||||||
|
11 GamePart_ReadyRoom
|
||||||
|
12 GamePart_Hangar
|
||||||
|
13 GamePart_Arsenal
|
||||||
|
14 GamePart_PilotLog
|
||||||
|
15 GamePart_System
|
||||||
|
17 GamePart_MainGame
|
||||||
|
19 GamePart_PauseMenu
|
||||||
|
20 GamePart_StageClear
|
||||||
|
21 GamePart_MissionLog
|
||||||
|
22 GamePart_GameOver
|
||||||
|
23 GamePart_Debriefing
|
||||||
|
24 GamePart_Dialog
|
||||||
|
25 GamePart_Tutorial
|
||||||
|
26 GamePart_ChallengeMission
|
||||||
|
27 GamePart_Leaderboard
|
||||||
|
|
||||||
|
ids 0..28 with NO registration string: [1, 2, 16, 18, 28]
|
||||||
9
docs/re/data/gamma-call-evidence.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# canary --log_mask=12 --log_level=3 (Kernel logging ON), 2026-08-29
|
||||||
|
# control: the same log carries 359 VdRetrainEDRAM and 358 VdGetSystemCommandBuffer lines,
|
||||||
|
# so a missing call would have been visible.
|
||||||
|
|
||||||
|
F 820006F4 8284E27C 1B9 ( 441) VdGetCurrentDisplayGamma
|
||||||
|
F 8200070C 8284E2BC 1BA ( 442) VdGetCurrentDisplayInformation
|
||||||
|
F 82000710 8284E2CC 1D3 ( 467) VdSetDisplayMode
|
||||||
|
d> F8000008 VdGetSystemCommandBuffer(701CF830, 701CF804)
|
||||||
|
d> F8000008 VdGetCurrentDisplayGamma(701CE1F8(00000000), 701CE1F0(0))
|
||||||
51
docs/re/data/kf-angle-census.txt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# tools/re-capture/kf_rotation_census.py over dat/GP_*.pak — 2026-08-28
|
||||||
|
# CONTROL must read 3/3; see the tool docstring for why.
|
||||||
|
|
||||||
|
CONTROL ptloop01: 3/3 blocks found, +12 = {30} (want 30)
|
||||||
|
CONTROL ptloop02: 3/3 blocks found, +12 = {-45} (want -45)
|
||||||
|
|
||||||
|
blocks scanned disc-wide: 83862
|
||||||
|
+4: non-zero in 3990 ( 4.76 %)
|
||||||
|
+8: non-zero in 3878 ( 4.62 %)
|
||||||
|
+12: non-zero in 12164 (14.50 %)
|
||||||
|
|
||||||
|
value histogram (non-zero), top 25:
|
||||||
|
+4 = 180 x3880
|
||||||
|
+8 = 180 x3102
|
||||||
|
+12 = 90 x1824
|
||||||
|
+12 = -90 x1176
|
||||||
|
+12 = 360 x1173
|
||||||
|
+12 = 120 x492
|
||||||
|
+12 = 180 x474
|
||||||
|
+12 = -58 x402
|
||||||
|
+12 = 53 x396
|
||||||
|
+12 = -125 x378
|
||||||
|
+12 = -120 x366
|
||||||
|
+12 = -66 x294
|
||||||
|
+12 = 114 x240
|
||||||
|
+8 = 90 x201
|
||||||
|
+12 = 129 x200
|
||||||
|
+12 = -43 x199
|
||||||
|
+12 = 115 x186
|
||||||
|
+12 = -360 x180
|
||||||
|
+8 = -180 x156
|
||||||
|
+12 = 58 x156
|
||||||
|
+8 = 178 x144
|
||||||
|
+12 = -33 x138
|
||||||
|
+12 = 124 x138
|
||||||
|
+12 = -133 x126
|
||||||
|
+12 = 130 x126
|
||||||
|
|
||||||
|
examples:
|
||||||
|
+12 ('GP_BUNK', '117ca14f', '0x73961b', 360)
|
||||||
|
+12 ('GP_BUNK', '117ca14f', '0x7398ac', 360)
|
||||||
|
+12 ('GP_BUNK', '117ca14f', '0x739a93', 360)
|
||||||
|
+12 ('GP_BUNK', '117ca14f', '0x739d24', 360)
|
||||||
|
+4 ('GP_BUNK', '117ca14f', '0x73ae5d', -180)
|
||||||
|
+4 ('GP_BUNK', '117ca14f', '0x73ae85', -180)
|
||||||
|
+4 ('GP_BUNK', '1ff0bcb0', '0x733d68', -180)
|
||||||
|
+4 ('GP_BUNK', '1ff0bcb0', '0x733d90', -180)
|
||||||
|
+8 ('GP_DEBRIEFING_PILOTLOG', '18ab1d66', '0x6f8', 90)
|
||||||
|
+8 ('GP_DEBRIEFING_PILOTLOG', '18ab1d66', '0x770', 90)
|
||||||
|
+8 ('GP_DEBRIEFING_PILOTLOG', '1c8a0129', '0x6f8', 90)
|
||||||
|
+8 ('GP_DEBRIEFING_PILOTLOG', '1c8a0129', '0x770', 90)
|
||||||
97
docs/re/data/movie-audio-layouts.csv
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
ADV.wmv,wmapro,48000,6,5.1
|
||||||
|
RT01A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT01B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT01C_1.wmv,wmav2,48000,2,unknown
|
||||||
|
RT01C_2.wmv,wmav2,48000,2,unknown
|
||||||
|
RT02A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT02B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT02C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT02D_1.wmv,wmav2,48000,2,unknown
|
||||||
|
RT02D_2.wmv,wmav2,48000,2,unknown
|
||||||
|
RT03A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT03B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT03C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT03D.wmv,wmav2,48000,2,unknown
|
||||||
|
RT04A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT04B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT05A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT05B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT05C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT06A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT06B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT06C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT06D.wmv,wmav2,48000,2,unknown
|
||||||
|
RT07A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT07B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT07C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT08A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT08B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT08C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT09A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT09B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT09C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT09D.wmv,wmav2,48000,2,unknown
|
||||||
|
RT10A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT10B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT11A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT11B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT11C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT12A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT12B_1.wmv,wmav2,48000,2,unknown
|
||||||
|
RT12B_2.wmv,wmav2,48000,2,unknown
|
||||||
|
RT13A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT13B_1.wmv,wmav2,48000,2,unknown
|
||||||
|
RT13B_2.wmv,wmav2,48000,2,unknown
|
||||||
|
RT14A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT14B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT14C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT15A.wmv,wmav2,48000,2,unknown
|
||||||
|
RT15B.wmv,wmav2,48000,2,unknown
|
||||||
|
RT15C.wmv,wmav2,48000,2,unknown
|
||||||
|
RT16C.wmv,wmav2,48000,2,unknown
|
||||||
|
S00A.wmv,wmapro,48000,6,5.1
|
||||||
|
S01A.wmv,wmapro,48000,6,5.1
|
||||||
|
S02A.wmv,wmapro,48000,6,5.1
|
||||||
|
S02B.wmv,wmapro,48000,6,5.1
|
||||||
|
S02C.wmv,wmapro,48000,6,5.1
|
||||||
|
S03A.wmv,wmapro,48000,6,5.1
|
||||||
|
S04A.wmv,wmapro,48000,6,5.1
|
||||||
|
S04B.wmv,wmapro,48000,6,5.1
|
||||||
|
S05A.wmv,wmapro,48000,6,5.1
|
||||||
|
S06A.wmv,wmapro,48000,6,5.1
|
||||||
|
S06B.wmv,wmapro,48000,6,5.1
|
||||||
|
S07A.wmv,wmapro,48000,6,5.1
|
||||||
|
S07B.wmv,wmapro,48000,6,5.1
|
||||||
|
S09B.wmv,wmapro,48000,6,5.1
|
||||||
|
S10B.wmv,wmapro,48000,6,5.1
|
||||||
|
S11A.wmv,wmapro,48000,6,5.1
|
||||||
|
S11C.wmv,wmapro,48000,6,5.1
|
||||||
|
S12A.wmv,wmapro,48000,6,5.1
|
||||||
|
S12B.wmv,wmapro,48000,6,5.1
|
||||||
|
S12C.wmv,wmapro,48000,6,5.1
|
||||||
|
S13A.wmv,wmapro,48000,6,5.1
|
||||||
|
S13B.wmv,wmapro,48000,6,5.1
|
||||||
|
S14A.wmv,wmapro,48000,6,5.1
|
||||||
|
S15A.wmv,wmapro,48000,6,5.1
|
||||||
|
S15B.wmv,wmapro,48000,6,5.1
|
||||||
|
S15C.wmv,wmapro,48000,6,5.1
|
||||||
|
S16A.wmv,wmapro,48000,6,5.1
|
||||||
|
hokyu_DS_s02A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_DS_s07A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_DS_s07H.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_DS_s08A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_DS_s13A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_DS_s14H.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s02A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s02H.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s03A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s03H.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s06A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s06H.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s09A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s09H.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s11A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s14A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s15A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s24A.wmv,wmav2,48000,2,unknown
|
||||||
|
hokyu_LS_s27A.wmv,wmav2,48000,2,unknown
|
||||||
|
57
docs/re/data/name-lookup-callsites.txt
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
The r5 (name) argument at every call site of sub_821CC860.
|
||||||
|
Recovered by forward register simulation over the 100 instructions before
|
||||||
|
each call, tracking lis/addi/subi/mr. Gated on three sites read by eye:
|
||||||
|
0x821c6524 TITLE_SCREEN, 0x821c67c8 TITLE_MENU, 0x821c73c8 LOADING -- all three
|
||||||
|
reproduce, so the remaining rows are trusted.
|
||||||
|
|
||||||
|
call site caller r5 name
|
||||||
|
0x82185f14 sub_82185E80 0x820a2160 EXTRA
|
||||||
|
0x821860b4 sub_82186050 0x820a214c LOADING
|
||||||
|
0x82186350 sub_82186270 0x820a2140 NEW_ITEM
|
||||||
|
0x821863f8 sub_82186270 0x820a2140 NEW_ITEM
|
||||||
|
0x8218c094 sub_8218BFE0 0x820a2384 MENU
|
||||||
|
0x8219403c sub_82193648 - (not recovered)
|
||||||
|
0x82196b44 sub_82196A50 0x820a2168 FILE
|
||||||
|
0x82198540 sub_82198440 0x820a2570 BASE
|
||||||
|
0x821988e4 sub_82198808 0x820a270c MESSAGE
|
||||||
|
0x82198a5c sub_82198808 0x820a28ec MENU_DISABLE_SKIP
|
||||||
|
0x8219c3b4 sub_8219C328 0x820a2acc INFO
|
||||||
|
0x821a0b4c sub_821A0AD0 0x820a255c FADE
|
||||||
|
0x821a0b98 sub_821A0AD0 0x820a2ae8 BG
|
||||||
|
0x821a0bdc sub_821A0AD0 0x820a2adc DEBRIEFING
|
||||||
|
0x821a1028 sub_821A0F80 0x820a2ab4 LOCAL_BOARD
|
||||||
|
0x821a113c sub_821A0F80 0x820a255c FADE
|
||||||
|
0x821a1188 sub_821A0F80 0x820a2ae8 BG
|
||||||
|
0x821a129c sub_821A0F80 0x820a2554 TITLE
|
||||||
|
0x821a12e8 sub_821A0F80 0x820a2ad4 BLACK
|
||||||
|
0x821a13b4 sub_821A0F80 0x820a2ab4 LOCAL_BOARD
|
||||||
|
0x821a1a6c sub_821A19B0 0x820a2ac0 LIVE_BOARD
|
||||||
|
0x821a1bd4 sub_821A19B0 0x820a255c FADE
|
||||||
|
0x821a1c20 sub_821A19B0 0x820a2ae8 BG
|
||||||
|
0x821a1d34 sub_821A19B0 0x820a2554 TITLE
|
||||||
|
0x821a1d80 sub_821A19B0 0x820a2ad4 BLACK
|
||||||
|
0x821a1e4c sub_821A19B0 0x820a2ac0 LIVE_BOARD
|
||||||
|
0x821a2b48 sub_821A2A80 0x820a2a74 DETAIL
|
||||||
|
0x821b368c sub_821B34E8 0x820a2570 BASE
|
||||||
|
0x821b3780 sub_821B34E8 0x820a32ac WINDOW
|
||||||
|
0x821b9210 sub_821B9118 0x820a2168 FILE
|
||||||
|
0x821bc60c sub_821BC568 - (not recovered)
|
||||||
|
0x821bca58 sub_821BC978 0x820a2384 MENU
|
||||||
|
0x821be188 sub_821BDF88 0x820a2570 BASE
|
||||||
|
0x821be1ec sub_821BDF88 0x820a38ac TUTORIAL_MENU
|
||||||
|
0x821c1d30 sub_821C1B58 0x820a2168 FILE
|
||||||
|
0x821c2f04 sub_821C2EA0 0x820a2168 FILE
|
||||||
|
0x821c5c88 sub_821C5818 0x820a3d3c TITLE_SCREEN
|
||||||
|
0x821c5d70 sub_821C5818 0x820a339c BUTTON
|
||||||
|
0x821c6524 sub_821C6458 0x820a3d3c TITLE_SCREEN
|
||||||
|
0x821c67c8 sub_821C6458 0x820a3d30 TITLE_MENU
|
||||||
|
0x821c73c8 sub_821C6458 0x820a214c LOADING
|
||||||
|
0x821c9488 sub_821C93C8 0x820a2168 FILE
|
||||||
|
0x822b2e04 sub_822B2D80 0x820ab24c KEY
|
||||||
|
0x822b581c sub_822B5790 0x820ab2a4 GAMMA_TITLE
|
||||||
|
0x822b5914 sub_822B5790 0x820ab298 GAMMA_RGB
|
||||||
|
0x822b71ac sub_822B7128 0x820a99dc GAME
|
||||||
|
0x822b8700 sub_822B8670 0x820a3124 PAD
|
||||||
|
0x822b9ebc sub_822B9E38 0x820a1784 SOUND
|
||||||
|
|
||||||
|
46/48 recovered.
|
||||||
23
docs/re/data/order-crosscheck-reach.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# How much of each recorded paint order its draw capture actually cross-checked.
|
||||||
|
# 2026-08-29, tools/re-capture/order_crosscheck_reach.py
|
||||||
|
#
|
||||||
|
# The three measured_paint_order vectors are reads of the runtime CHILD ARRAY
|
||||||
|
# (ui-screen-runtime.md: "paint order (child slots)"). These captures are the
|
||||||
|
# cross-check, not the source. Quads = indices/4: a single draw can carry
|
||||||
|
# several (the menu's draw 9 has indices=24 = 6 quads), so counting draws
|
||||||
|
# under-reads what is on screen.
|
||||||
|
|
||||||
|
== docs/re/captures/title-screen-draw-order.log
|
||||||
|
frame 1: 8 draws, 12 quads, 5 textures
|
||||||
|
frame 2: 8 draws, 12 quads, 5 textures
|
||||||
|
frame 3: 8 draws, 12 quads, 5 textures
|
||||||
|
textures in EVERY captured frame: 5 of 5
|
||||||
|
== docs/re/captures/title-draw-order-with-alpha.log
|
||||||
|
frame 1: 8 draws, 12 quads, 5 textures
|
||||||
|
frame 2: 8 draws, 12 quads, 5 textures
|
||||||
|
textures in EVERY captured frame: 5 of 5
|
||||||
|
== docs/re/captures/main-menu-draw-order.log
|
||||||
|
frame 0: 9 draws, 16 quads, 5 textures
|
||||||
|
frame 3: 7 draws, 9 quads, 4 textures
|
||||||
|
textures in EVERY captured frame: 4 of 5
|
||||||
|
absent from some frame: ['11C30000']
|
||||||
35
docs/re/data/paint-order-audit.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# cargo run -p sylpheed-formats --example paint_order_audit -- dat/GP_TITLE.pak
|
||||||
|
# 2026-08-29
|
||||||
|
|
||||||
|
1682 | let decl = &decls[0];
|
||||||
|
290 | let mut flush = |base: u32,
|
||||||
|
entry 0 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||||
|
entry 1 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||||
|
entry 2 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||||
|
entry 3 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||||
|
entry 4 title 24 elements
|
||||||
|
derived == measured : NO
|
||||||
|
inverted pairs : 8 (of which same-layer-key ties: 8)
|
||||||
|
measured: [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8]
|
||||||
|
derived : [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, 22, 23, 21, 8]
|
||||||
|
keys : [32928, 32928, 32928, 32928, 32928, 32928, 32832, 32928, 4294967295, 32768, 4294967295, 32784, 32784, 4294967295, 32899, 32899, 32899, 32899, 32899, 32898, 32897, 33024, 32936, 32937]
|
||||||
|
entry 5 main menu 16 elements
|
||||||
|
derived == measured : YES
|
||||||
|
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||||
|
entry 6 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
|
||||||
|
entry 7 (no measured order) 30 elements, 37 tied pairs, 16 of them OVERLAPPING
|
||||||
|
entry 8 main menu 16 elements
|
||||||
|
derived == measured : YES
|
||||||
|
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||||
|
entry 9 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
|
||||||
|
entry 10 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||||
|
entry 11 splash 7 elements
|
||||||
|
derived == measured : YES
|
||||||
|
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||||
|
entry 12 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||||
|
entry 13 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||||
|
entry 14 splash 7 elements
|
||||||
|
derived == measured : YES
|
||||||
|
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||||
|
entry 15 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||||
|
5 build(s) with a measured order were checked
|
||||||
25
docs/re/data/plateau-census.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
CONTROL GP_TITLE build 7: fallback elements [8, 13, 14] (want [8, 13, 14])
|
||||||
|
index 8 is 'ptlogo_eff3.t32' (want ptlogo_eff3.t32)
|
||||||
|
|
||||||
|
elements with a keyframe group, disc-wide: 15493
|
||||||
|
no plateau -> rest pose is GUESSED: 3807 (24.57 %)
|
||||||
|
|
||||||
|
most common guessed elements:
|
||||||
|
1002 pb_name_eff.t32
|
||||||
|
90 gbp301i.t32
|
||||||
|
84 pb_warp_eff.t32
|
||||||
|
78 pbb_destroyer.t32
|
||||||
|
70 gbp303i.t32
|
||||||
|
60 pb_w_line.t32
|
||||||
|
54 pbmwindow_eff.t32
|
||||||
|
54 pbfriendly.t32
|
||||||
|
32 pzeff00.prm
|
||||||
|
32 pzeff02.t32
|
||||||
|
32 px_mission_area.rat
|
||||||
|
30 pbr_fighter.t32
|
||||||
|
|
||||||
|
--- the 3807 plateau-less elements, by what each candidate rule returns
|
||||||
|
current rule (longest dwell) returns an INVISIBLE pose : 1711 ( 44.9 %)
|
||||||
|
current rule returns a ZERO-SCALE (degenerate) pose : 195 ( 5.1 %)
|
||||||
|
'rest = last keyframe' returns an INVISIBLE pose : 1618 ( 42.5 %)
|
||||||
|
the two rules agree : 1911 ( 50.2 %)
|
||||||
16
docs/re/data/plateauless-endstate.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# tools/re-capture/plateauless_endstate.py over dat/GP_*.pak -- 2026-08-29
|
||||||
|
|
||||||
|
plateau-less, multi-keyframe, ending VISIBLE: 687
|
||||||
|
alpha never decreases (a fade/slide IN that stops): 472 (68.7 %)
|
||||||
|
ends at its maximum alpha but dips on the way : 203 (29.5 %)
|
||||||
|
ends visible but NOT at maximum alpha : 12 (1.7 %)
|
||||||
|
|
||||||
|
examples of the monotone kind (alpha sequence, then scale pairs):
|
||||||
|
GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_MAIN_GAME_D2D.pak pghud_speed_cut.t32 [0, 255] [(100, 100), (100, 100)]
|
||||||
|
GP_MAIN_GAME_D2D.pak pgmsg_restart_sub.rat [255, 255] [(106, 106), (100, 100)]
|
||||||
52
docs/re/data/ratc-child-name-audit.txt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# RATC child names: the printable-run scan vs the `opt ` block, all 33 dat/*.pak
|
||||||
|
# 2026-08-29, crates/sylpheed-formats/examples/ratc_child_names.rs
|
||||||
|
#
|
||||||
|
# RUN 1 was taken BEFORE the fix, when ratc::parse still named children by
|
||||||
|
# scanning for the last printable run. It is the evidence for the defect.
|
||||||
|
|
||||||
|
RATC children scanned : 18002
|
||||||
|
with an `opt ` block: 17942
|
||||||
|
scanned name AGREES : 17918
|
||||||
|
scanned name DIFFERS : 24
|
||||||
|
|
||||||
|
distinct disagreements (scanned -> opt), with counts:
|
||||||
|
'OX -> po_keys_win1.t32 x2
|
||||||
|
8AX -> pbbg.t32 x12
|
||||||
|
8AX -> pmbase.t32 x4
|
||||||
|
8AX -> pteff04.t32 x2
|
||||||
|
8AX -> pteff05.t32 x4
|
||||||
|
|
||||||
|
first 20 occurrences:
|
||||||
|
GP_OPTIONS.pak entry 20 'OX -> po_keys_win1.t32
|
||||||
|
GP_OPTIONS.pak entry 22 'OX -> po_keys_win1.t32
|
||||||
|
GP_READY_ROOM.pak entry 132 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 141 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 211 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 219 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 233 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 234 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 264 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 265 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 967 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 999 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 1097 8AX -> pbbg.t32
|
||||||
|
GP_READY_ROOM.pak entry 1099 8AX -> pbbg.t32
|
||||||
|
GP_STAGE_CLEAR.pak entry 2 8AX -> pmbase.t32
|
||||||
|
GP_STAGE_CLEAR.pak entry 4 8AX -> pmbase.t32
|
||||||
|
GP_STAGE_CLEAR.pak entry 7 8AX -> pmbase.t32
|
||||||
|
GP_STAGE_CLEAR.pak entry 8 8AX -> pmbase.t32
|
||||||
|
GP_TITLE.pak entry 4 8AX -> pteff04.t32
|
||||||
|
GP_TITLE.pak entry 5 8AX -> pteff05.t32
|
||||||
|
|
||||||
|
# RUN 2, same command AFTER the fix. ratc::parse now prefers the `opt ` name, so
|
||||||
|
# the two readings must agree everywhere -- which is the check that the fix is
|
||||||
|
# complete rather than partial.
|
||||||
|
|
||||||
|
RATC children scanned : 18002
|
||||||
|
with an `opt ` block: 17942
|
||||||
|
scanned name AGREES : 17942
|
||||||
|
scanned name DIFFERS : 0
|
||||||
|
|
||||||
|
distinct disagreements (scanned -> opt), with counts:
|
||||||
|
|
||||||
|
first 20 occurrences:
|
||||||
26
docs/re/data/ratc-name-fix-render-effect.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Effect on the five port screens of naming RATC children from the `opt `
|
||||||
|
# block. 2026-08-29. `sylpheed-cli screen render --build N GP_TITLE.pak`,
|
||||||
|
# before vs after the ratc.rs change. The newly-resolved element is the
|
||||||
|
# full-resolution background (pteff04.t32 on the title, pteff05.t32 on the
|
||||||
|
# menus), which the old name `8AX` hid.
|
||||||
|
|
||||||
|
build screen mean brightness high-frequency detail
|
||||||
|
4 title 72.77 -> 72.81 8.722 -> 10.062 x1.15 pixels changed >2: 30.0%
|
||||||
|
5 main menu (JP) 42.73 -> 42.74 3.978 -> 5.051 x1.27 pixels changed >2: 20.0%
|
||||||
|
6 EXTRAS (JP) 44.08 -> 44.09 3.953 -> 5.025 x1.27 pixels changed >2: 19.4%
|
||||||
|
8 main menu 41.57 -> 41.58 3.830 -> 4.970 x1.30 pixels changed >2: 21.0%
|
||||||
|
9 EXTRAS 42.99 -> 43.00 3.867 -> 5.000 x1.29 pixels changed >2: 20.2%
|
||||||
|
|
||||||
|
# Brightness unmoved, detail up ~a quarter = the same artwork at twice the
|
||||||
|
# resolution replacing a 2x upscale. New CONTENT would move the mean.
|
||||||
|
|
||||||
|
# Covering check: the background is opaque and full-screen and paints 4th
|
||||||
|
# of 16 on the menu, so it could hide what follows. Standard deviation
|
||||||
|
# inside each element's resting rect (build 8), before vs after:
|
||||||
|
ptbtn01 sd 59.98 -> 60.03
|
||||||
|
ptbtn03 sd 63.40 -> 63.45
|
||||||
|
ptbtn05 sd 66.54 -> 66.58
|
||||||
|
ptframe1 sd 51.16 -> 51.20
|
||||||
|
ptmsg sd 56.11 -> 56.12
|
||||||
|
loop-area sd 50.99 -> 51.04
|
||||||
|
# Nothing is covered.
|
||||||
58
docs/re/data/ratc-tan-frame-sequence.txt
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# The 60 RATC children with no `opt ` block — probe output
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cargo run -p sylpheed-formats --example ratc_optless_children -- $SYLPHEED_DISC/dat/*.pak
|
||||||
|
|
||||||
|
RATC children scanned : 18002
|
||||||
|
with NO accepted `opt ` block: 60
|
||||||
|
... of which are child #0 : 0
|
||||||
|
|
||||||
|
why, by cause:
|
||||||
|
TagBeyondWindow x60
|
||||||
|
|
||||||
|
every occurrence (name is what the FALLBACK scan returned):
|
||||||
|
GP_READY_ROOM.pak entry 26 child 1 TagBeyondWindow(213)
|
||||||
|
24 bytes before the magic: 00 00 00 07 00 00 00 28 00 00 00 08 00 00 00 28 00 00 00 09 00 00 00 28
|
||||||
|
GP_READY_ROOM.pak entry 26 child 2 TagBeyondWindow(60813)
|
||||||
|
24 bytes before the magic: 0a eb 61 00 08 eb 61 00 06 eb 61 00 05 eb 61 00 03 eb 61 00 01 eb 61 00
|
||||||
|
... (the remaining 54 rows are the same five bundles' children 1..10;
|
||||||
|
every distance is 213 + n*60600, i.e. the SAME tag)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading bundle 26 directly — the ten are frames of one `.tan`
|
||||||
|
|
||||||
|
```
|
||||||
|
entry 26: 15 children, payload 706609 bytes
|
||||||
|
0 @0x00000444 T8aD size 1933 opt@-40 name='pbf15_energie_generator2.t32'
|
||||||
|
1 @0x00000bd1 T8aD size 60600 opt@-213 name='pb_f15_eg_anm.tan'
|
||||||
|
2 @0x0000f889 T8aD size 60600 opt@-60813 name='pb_f15_eg_anm.tan'
|
||||||
|
3 @0x0001e541 T8aD size 60600 opt@-121413 name='pb_f15_eg_anm.tan'
|
||||||
|
4 @0x0002d1f9 T8aD size 60600 opt@-182013 name='pb_f15_eg_anm.tan'
|
||||||
|
5 @0x0003beb1 T8aD size 60600 opt@-242613 name='pb_f15_eg_anm.tan'
|
||||||
|
6 @0x0004ab69 T8aD size 60600 opt@-303213 name='pb_f15_eg_anm.tan'
|
||||||
|
7 @0x00059821 T8aD size 60600 opt@-363813 name='pb_f15_eg_anm.tan'
|
||||||
|
8 @0x000684d9 T8aD size 60600 opt@-424413 name='pb_f15_eg_anm.tan'
|
||||||
|
9 @0x00077191 T8aD size 60600 opt@-485013 name='pb_f15_eg_anm.tan'
|
||||||
|
10 @0x00085e49 T8aD size 60632 opt@-545613 name='pb_f15_eg_anm.tan'
|
||||||
|
11 @0x00094b21 T8aD size 17523 opt@-32 name='pbf15_pd_inside2.t32'
|
||||||
|
12 @0x00098f94 T8aD size 75068 opt@-35 name='pbenergie_generator.t32'
|
||||||
|
13 @0x000ab4d0 T8aD size 4697 opt@-28 name='pbf15_eg_eff.t32'
|
||||||
|
14 @0x000ac729 RATC size 264 opt@-25 name='pb_s15_eg.rat'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Disc-wide: every `opt ` name, by extension
|
||||||
|
|
||||||
|
```
|
||||||
|
`opt ` blocks disc-wide: 18718
|
||||||
|
|
||||||
|
by extension:
|
||||||
|
.t32 x14756
|
||||||
|
.rat x3311
|
||||||
|
.prm x367
|
||||||
|
.tbm x224
|
||||||
|
.sbo x54
|
||||||
|
.tan x6
|
||||||
|
|
||||||
|
.tan resources with >=1 child: 6
|
||||||
|
GP_READY_ROOM.pak pb_f15_eg_anm.tan frames= 10 sizes=[60600, 60632] x6 bundles
|
||||||
|
```
|
||||||
8
docs/re/data/rest-rule-blast-radius.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# tools/re-capture/rest_rule_blast_radius.py over dat/GP_*.pak -- 2026-08-29
|
||||||
|
|
||||||
|
genuinely ambiguous elements (plateau-less, 2+ keyframes): 2305
|
||||||
|
the two rules AGREE on : 409 (17.7 %)
|
||||||
|
they DIFFER on : 1896 (82.3 %)
|
||||||
|
|
||||||
|
rule dwell : returns an INVISIBLE pose 1711 ( 74.2 %), zero-scale 195 ( 8.5 %)
|
||||||
|
rule last : returns an INVISIBLE pose 1618 ( 70.2 %), zero-scale 43 ( 1.9 %)
|
||||||
7
docs/re/data/rotation-top-level-examples.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# `sylpheed-cli screen info --geometry`, rows with a non-zero rotation (r=).
|
||||||
|
# GP_DIALOG builds 0 and 6; GP_DEBRIEFING_PILOTLOG build 10. 2026-08-28
|
||||||
|
|
||||||
|
4 pceff03.t32 247x281 38x38 NO 8: 630,348 100%,100% a=0 r=90 12: 550,273 100%,100% a=128 r=30 14: 510,235 100%,100% a=192 r=10 16: 490,217 100%,100% a=224 r=3 52: 470,198 100%,100% a=255 58: 470,198 100%,100% a=255 -: 470,198 100%,100% a=0
|
||||||
|
5 pceff04.t32 257x311 478x586 NO 8: 413,77 100%,100% a=0 r=90 12: 483,167 100%,100% a=128 r=30 14: 518,212 100%,100% a=192 r=10 16: 536,234 100%,100% a=224 r=3 52: 553,247 100%,100% a=255 58: 553,247 100%,100% a=255 -: 553,247 100%,100% a=0
|
||||||
|
1 pzeff02.t32 529x519 528x524 NO 197: 376,58 0%,0% a=0 217: 376,58 112%,112% a=255 r=43 232: 376,58 145%,145% a=64 r=61 237: 376,58 173%,173% a=32 r=75 -: 376,58 200%,200% a=0 r=90
|
||||||
|
5 pjeff24a.t32 382x140 610x622 NO -: 335,49 210%,210% a=53 r=90
|
||||||
32
docs/re/data/rotation-toplevel-census.txt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
CONTROL GP_TITLE build 4 (nested rotations only): 0 top-level rotated (want 0)
|
||||||
|
CONTROL GP_DIALOG build 0: 2 top-level rotated (want 2)
|
||||||
|
[4] pceff03.t32 r = [90, 30, 10, 3, 0, 0, 0]
|
||||||
|
[5] pceff04.t32 r = [90, 30, 10, 3, 0, 0, 0]
|
||||||
|
|
||||||
|
top-level elements with a keyframe group, disc-wide: 15493
|
||||||
|
carrying a non-zero rotation: 2152 (13.89 %)
|
||||||
|
|
||||||
|
by archive:
|
||||||
|
1868 GP_READY_ROOM.pak
|
||||||
|
40 GP_DIALOG.pak
|
||||||
|
37 GP_MAIN_GAME_D2D.pak
|
||||||
|
37 GP_MAIN_GAME_E2D.pak
|
||||||
|
37 GP_MAIN_GAME_F2D.pak
|
||||||
|
37 GP_MAIN_GAME_I2D.pak
|
||||||
|
37 GP_MAIN_GAME_J2D.pak
|
||||||
|
37 GP_MAIN_GAME_S2D.pak
|
||||||
|
10 GP_GAMEOVER.pak
|
||||||
|
8 GP_DEBRIEFING_PILOTLOG.pak
|
||||||
|
4 GP_TITLE.pak
|
||||||
|
|
||||||
|
most common rotated element names:
|
||||||
|
444 pbb_destroyer.t32
|
||||||
|
402 pbr_destroyer.t32
|
||||||
|
276 pbr_fighter.t32
|
||||||
|
174 pbb_fighter.t32
|
||||||
|
168 pbb_aircraft.t32
|
||||||
|
84 pb_warp_eff.t32
|
||||||
|
48 pbtab.t32
|
||||||
|
35 gbp012i.t32
|
||||||
|
32 pzeff02.t32
|
||||||
|
30 gbp001i.t32
|
||||||
39
docs/re/data/se-cue-runtime-offsets.txt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
SE cue waves located inside Static.slb by playing them.
|
||||||
|
|
||||||
|
Method: run Canary with --xma_param_probe=true, drive the main menu, and read the
|
||||||
|
per-stream XMA-PARAM lines. Each line carries the stream's first 32 bytes; those
|
||||||
|
bytes are then searched for in Static.slb's 8 353 472 readable bytes.
|
||||||
|
|
||||||
|
event packets bytes channels/rate offset in Static.slb
|
||||||
|
------------------------- ------- ------ -------------- --------------------
|
||||||
|
d-pad move (cursor) 4 8192 mono 48000 Hz 0x1ec0
|
||||||
|
B / back (cancel) 2 4096 mono 48000 Hz 0x0ec0
|
||||||
|
A / confirm (title->menu) 6 12288 mono 48000 Hz 0x5d6c0
|
||||||
|
|
||||||
|
Each head matched at exactly ONE offset. The first two are contiguous:
|
||||||
|
0x0ec0 + 4096 = 0x1ec0.
|
||||||
|
|
||||||
|
Simultaneously at the main menu, two STEREO 48 kHz streams were decoding:
|
||||||
|
1893 packets / 3 876 864 B and 1919 packets / 3 930 112 B. See
|
||||||
|
structures/bgm-two-stems.md -- this is the runtime observation that the two
|
||||||
|
stems of a music bank play at the same time.
|
||||||
|
|
||||||
|
DECODED 2026-08-28 with tools/re-capture/slb_extract_wave.py + ffmpeg's xma1:
|
||||||
|
|
||||||
|
event offset packets decoded rms peak envelope shape
|
||||||
|
---------------- -------- ------- --------- ----- ----- ------------------------
|
||||||
|
d-pad move 0x1ec0 4 0.533 s 2085 29813 sharp attack, monotonic decay
|
||||||
|
B / back 0x0ec0 2 0.344 s 2985 16973 attack, peak in frame 2
|
||||||
|
(pre-input cue) 0x5d6c0 6 1.016 s 4327 32767 peaks in frame 3, long decay
|
||||||
|
|
||||||
|
CONTROL: the same synthesized RIFF wrapper applied to BGM_001.slb's first wave
|
||||||
|
(offset 14336, stereo) decodes to 173.808875 s -- identical to the duration
|
||||||
|
obtained from that bank's own on-disc RIFF header. The wrapper is correct.
|
||||||
|
|
||||||
|
SECOND RUN 2026-08-28 -- independent boot, same probe:
|
||||||
|
* d-pad move and B reproduced with IDENTICAL head bytes and sizes (2 of 2 runs).
|
||||||
|
* The 12288-byte wave fires on the A that advances title -> main menu, together
|
||||||
|
with the two stereo BGM stems (the menu's music starting). Counting from the
|
||||||
|
title rather than the menu is what attributed it.
|
||||||
|
* Left and Right fired NO new stream -- no distinct invalid cue. (Dedup caveat:
|
||||||
|
a press replaying an already-heard wave would also log nothing.)
|
||||||
326
docs/re/data/se-ui-cues.txt
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
Every SE cue on the disc, from tables.pak SOUNDS. 322 cues.
|
||||||
|
BANK_SE names the single bank they all live in: Static.slb
|
||||||
|
0 of the 322 has its own entry in FILES (5 135 bank paths).
|
||||||
|
|
||||||
|
1 SE_UI_START
|
||||||
|
2 SE_UI_CURSOR
|
||||||
|
3 SE_UI_DECIDE
|
||||||
|
4 SE_UI_CANSEL
|
||||||
|
5 SE_UI_IMPOSI
|
||||||
|
6 SE_UI_WAIT
|
||||||
|
7 SE_UI_NEXT
|
||||||
|
8 SE_UI_SUB_WIN_OPN
|
||||||
|
9 SE_UI_SUB_WIN_CLS
|
||||||
|
10 SE_UI_ALART_WIN
|
||||||
|
11 SE_UI_PAUSE
|
||||||
|
12 SE_UI_SPLASH_IN
|
||||||
|
13 SE_UI_SPLASH_OUT
|
||||||
|
14 SE_UI_LOAD_CMP
|
||||||
|
15 SE_TUT_BOX_CLEAR
|
||||||
|
16 SE_TUT_UNIT_IN
|
||||||
|
17 SE_UI_WEAPON_PLAN
|
||||||
|
18 SE_UI_WEAPON_CREATE
|
||||||
|
21 SE_HUD_MIS_LOCK
|
||||||
|
22 SE_HUD_LOCK_ALART
|
||||||
|
23 SE_HUD_WP_EMPTY
|
||||||
|
24 SE_HUD_WP_CHARGED
|
||||||
|
25 SE_HUD_DMG_ALARM
|
||||||
|
26 SE_HUD_OH_ALART
|
||||||
|
27 SE_HUD_OVER_HEAT
|
||||||
|
28 SE_HUD_CALL
|
||||||
|
29 SE_HUD_EMERG_CALL
|
||||||
|
30 SE_HUD_MES_END
|
||||||
|
31 SE_HUD_MES_SHAT
|
||||||
|
32 SE_HUD_MIS_SEARCH
|
||||||
|
33 SE_HUD_INVALID
|
||||||
|
34 SE_HUD_MW_CHG
|
||||||
|
35 SE_HUD_TRG_CHG
|
||||||
|
36 SE_HUD_MOD_CHG
|
||||||
|
37 SE_HUD_RADIO_MENU
|
||||||
|
38 SE_HUD_RADIO_ORDER
|
||||||
|
39 SE_HUD_RADIO_CANSEL
|
||||||
|
40 SE_HUD_TM_USE
|
||||||
|
41 SE_HUD_TM_INP
|
||||||
|
42 SE_HUD_TM_COM
|
||||||
|
43 SE_HUD_TM_OK
|
||||||
|
44 SE_HUD_TM_ERROR
|
||||||
|
45 SE_HUD_RADER_MARKER
|
||||||
|
46 SE_HUD_GUIDE
|
||||||
|
47 SE_HUD_SUB_TARGET
|
||||||
|
48 SE_HUD_SP_CHARGE
|
||||||
|
49 SE_HUD_SP_LV_UP
|
||||||
|
50 SE_BR_PASS
|
||||||
|
51 SE_BR_SHAT
|
||||||
|
52 SE_BR_PROGRESS
|
||||||
|
53 SE_BR_DECO_1
|
||||||
|
54 SE_BR_FRAME
|
||||||
|
55 SE_BR_DECO_2
|
||||||
|
56 SE_BR_HELP
|
||||||
|
57 SE_BR_DISP
|
||||||
|
58 SE_BR_COWIN
|
||||||
|
59 SE_BR_WAIT
|
||||||
|
60 SE_BR_NEXT
|
||||||
|
61 SE_BR_FRIENDRY
|
||||||
|
62 SE_BR_ENEMY
|
||||||
|
63 SE_BR_MESS
|
||||||
|
64 SE_BR_POINT
|
||||||
|
65 SE_BR_MARK
|
||||||
|
66 SE_BR_OUT
|
||||||
|
67 SE_BR_MAIN_OP
|
||||||
|
68 SE_BR_MAIN_CLS
|
||||||
|
69 SE_BR_JUMP_OUT
|
||||||
|
70 SE_BR_JUMP_IN
|
||||||
|
71 SE_BR_PROMETEUS
|
||||||
|
72 SE_BR_PLANET_BROKEN
|
||||||
|
73 SE_HUD_SP_RETURN
|
||||||
|
74 SE_HUD_SP_ATACK
|
||||||
|
75 SE_HUD_SP_SLOW
|
||||||
|
76 SE_HUD_AAM_SITE_OPN
|
||||||
|
77 SE_HUD_AAM_SITE_CLS
|
||||||
|
78 SE_HUD_ASM_SITE_OPN
|
||||||
|
79 SE_HUD_ASM_SITE_CLS
|
||||||
|
81 SE_UI_MISSION_START
|
||||||
|
82 SE_UI_MISSION_UPDATE
|
||||||
|
83 SE_UI_MISSION_END
|
||||||
|
101 SE_SW_nbe01_1
|
||||||
|
102 SE_SW_nbe02_1
|
||||||
|
103 SE_SW_nbe03_1
|
||||||
|
104 SE_SW_nla01_1
|
||||||
|
105 SE_SW_nvu01_1
|
||||||
|
106 SE_SW_nvu02_1
|
||||||
|
107 SE_SW_nvu03_1
|
||||||
|
108 SE_SW_nvu04_1
|
||||||
|
109 SE_SW_nsh01_1
|
||||||
|
110 SE_SW_nsh02_1
|
||||||
|
111 SE_SW_nsh03_1
|
||||||
|
112 SE_SW_nsp01_1
|
||||||
|
113 SE_SW_nsp02_1
|
||||||
|
114 SE_SW_nvu01_2
|
||||||
|
115 SE_SW_nvu01_3
|
||||||
|
116 SE_SW_nvu02_2
|
||||||
|
117 SE_SW_nvu02_3
|
||||||
|
118 SE_SW_nvu02_4
|
||||||
|
119 SE_SW_nvu03_2
|
||||||
|
120 SE_SW_nvu03_3
|
||||||
|
121 SE_SW_nvu04_2
|
||||||
|
122 SE_SW_nla02_1
|
||||||
|
123 SE_SW_SPIT_1
|
||||||
|
201 SE_MW_wvu01_1
|
||||||
|
202 SE_MW_wvu02_1
|
||||||
|
203 SE_MW_wvu03_1
|
||||||
|
204 SE_MW_wvu04_1
|
||||||
|
205 SE_MW_wbe01_1
|
||||||
|
206 SE_MW_wbe02_1
|
||||||
|
207 SE_MW_whb01_1
|
||||||
|
208 SE_MW_whb02_1
|
||||||
|
209 SE_MW_wla01_1
|
||||||
|
210 SE_MW_wla02_1
|
||||||
|
211 SE_MW_wla03_1
|
||||||
|
212 SE_MW_wla04_1
|
||||||
|
213 SE_MW_wAA01_1
|
||||||
|
214 SE_MW_wAA02_1
|
||||||
|
215 SE_MW_wAA03_1
|
||||||
|
216 SE_MW_wAA04_1
|
||||||
|
217 SE_MW_wAA04_2
|
||||||
|
218 SE_MW_wAA05_1
|
||||||
|
219 SE_MW_wAA07_1
|
||||||
|
220 SE_MW_wAA0x_2
|
||||||
|
221 SE_MW_wro01_1
|
||||||
|
222 SE_MW_wro02_1
|
||||||
|
223 SE_MW_wro03_1
|
||||||
|
224 SE_MW_wro04_1
|
||||||
|
225 SE_MW_wAS01_1
|
||||||
|
226 SE_MW_wAS02_1
|
||||||
|
227 SE_MW_wAS03_1
|
||||||
|
228 SE_MW_wAS04_1
|
||||||
|
229 SE_MW_wAS05_1
|
||||||
|
230 SE_MW_wAS0x_2
|
||||||
|
231 SE_MW_wAS0x_3
|
||||||
|
232 SE_MW_wto01_1
|
||||||
|
233 SE_MW_wto01_2
|
||||||
|
234 SE_MW_wto02_1
|
||||||
|
235 SE_MW_wto02_2
|
||||||
|
236 SE_MW_wto03_1
|
||||||
|
237 SE_MW_wto03_2
|
||||||
|
238 SE_MW_wto04_1
|
||||||
|
239 SE_MW_wto04_2
|
||||||
|
240 SE_MW_wbo01_1
|
||||||
|
241 SE_MW_wbo02_1
|
||||||
|
242 SE_MW_wbo03_1
|
||||||
|
243 SE_MW_wbo04_1
|
||||||
|
244 SE_MW_wbo05_1
|
||||||
|
245 SE_MW_wbo06_1
|
||||||
|
246 SE_MW_wbo07_1
|
||||||
|
247 SE_MW_wbo08_1
|
||||||
|
248 SE_MW_wbo09_1
|
||||||
|
249 SE_MW_wKE01_1
|
||||||
|
250 SE_MW_wKE02_1
|
||||||
|
251 SE_MW_wKE03_1
|
||||||
|
252 SE_MW_wgu01_1
|
||||||
|
253 SE_MW_wgu02_1
|
||||||
|
254 SE_MW_wgu03_1
|
||||||
|
255 SE_MW_wgu04_1
|
||||||
|
256 SE_MW_wgu05_1
|
||||||
|
257 SE_MW_wgu05_2
|
||||||
|
258 SE_MW_wgu06_1
|
||||||
|
259 SE_MW_wgu07_1
|
||||||
|
260 SE_MW_wsp01_1
|
||||||
|
261 SE_MW_wsp01_2
|
||||||
|
262 SE_MW_wsp01_3
|
||||||
|
263 SE_MW_L3GP_1
|
||||||
|
264 SE_MW_THOR_1
|
||||||
|
301 SE_SPE_mcm01_1
|
||||||
|
302 SE_SPE_mcm02_1
|
||||||
|
304 SE_SPE_mpr01_1
|
||||||
|
351 SE_SWP_BEAM_1
|
||||||
|
352 SE_SWP_RAILGUN_1
|
||||||
|
353 SE_SWP_PCANNON_1
|
||||||
|
354 SE_SWP_LASER_1
|
||||||
|
355 SE_SWP_BVULCAN_1
|
||||||
|
356 SE_SWP_VULCAN_1
|
||||||
|
357 SE_SWP_AAM_1
|
||||||
|
358 SE_SWP_ASM_1
|
||||||
|
359 SE_SWP_BEAM_2
|
||||||
|
401 SE_EXT_OVER_HEAT
|
||||||
|
402 SE_EXT_OH_RECOVER
|
||||||
|
403 SE_EXT_EMPTY
|
||||||
|
404 SE_EXT_EJECT_H
|
||||||
|
405 SE_EXT_EJECT_M
|
||||||
|
406 SE_EXT_EJECT_L
|
||||||
|
407 SE_EXT_EJECT_FAE
|
||||||
|
408 SE_EXT_MGT_OPN
|
||||||
|
409 SE_EXT_MGT_CLS
|
||||||
|
410 SE_EXT_PHOTON_CHG
|
||||||
|
411 SE_EXT_GRAVITY_CHG
|
||||||
|
412 SE_EXT_RECHARGE
|
||||||
|
413 SE_EXT_RECHG_CMP
|
||||||
|
414 SE_EXT_DOUBLER
|
||||||
|
415 SE_EXT_SPIT_CHG
|
||||||
|
501 SE_DS_ENGINE
|
||||||
|
502 SE_DS_POWER_UP
|
||||||
|
503 SE_DS_AFTER_BURNER
|
||||||
|
504 SE_DS_REVERSE
|
||||||
|
505 SE_DS_COCKPIT
|
||||||
|
506 SE_DS_POWER_DOWN
|
||||||
|
507 SE_DS_AB_END
|
||||||
|
508 SE_DS_SIDE
|
||||||
|
509 SE_DS_ROLL
|
||||||
|
511 SE_ENG_ERAN
|
||||||
|
512 SE_ENG_ERAN_Q
|
||||||
|
513 SE_ENG_VINDI
|
||||||
|
514 SE_ENG_DELTA
|
||||||
|
515 SE_ENG_UNE
|
||||||
|
516 SE_ENG_ADAN
|
||||||
|
517 SE_ENG_SUPPLY
|
||||||
|
518 SE_ENG_ATK_S
|
||||||
|
519 SE_ENG_ATK_L
|
||||||
|
551 SE_ENG_ACRO
|
||||||
|
552 SE_ENG_JUN
|
||||||
|
553 SE_ENG_KUCHI
|
||||||
|
554 SE_ENG_OOGATA
|
||||||
|
555 SE_ENG_YUSOU
|
||||||
|
556 SE_ENG_BS
|
||||||
|
557 SE_ENG_AAF
|
||||||
|
558 SE_ENG_ASF
|
||||||
|
559 SE_ENG_CARRIER
|
||||||
|
560 SE_ENG_MISSILE
|
||||||
|
561 SE_ENG_SIDE_S
|
||||||
|
562 SE_ENG_SIDE_L
|
||||||
|
601 SE_EXP_CRAFT
|
||||||
|
602 SE_EXP_EX_CRAFT
|
||||||
|
603 SE_EXP_ATK
|
||||||
|
604 SE_EXP_OUT
|
||||||
|
605 SE_EXP_BS_PART_L
|
||||||
|
606 SE_EXP_BS_PART_M
|
||||||
|
607 SE_EXP_BS_PART_S
|
||||||
|
608 SE_EXP_BS_BREAK_LL
|
||||||
|
609 SE_EXP_BS_BREAK_L
|
||||||
|
610 SE_EXP_BS_BREAK_M
|
||||||
|
611 SE_EXP_BS_BREAK_S
|
||||||
|
612 SE_EXP_BS_BREAK_SS
|
||||||
|
613 SE_EXP_BS_OMEN
|
||||||
|
614 SE_EXP_BS_BOMB
|
||||||
|
615 SE_EXP_MISSILE
|
||||||
|
616 SE_EXP_TCAF_CRUISER
|
||||||
|
617 SE_EXP_MINE
|
||||||
|
651 SE_IMP_BM_DMG
|
||||||
|
652 SE_IMP_BM
|
||||||
|
653 SE_IMP_LS_DMG
|
||||||
|
654 SE_IMP_LS
|
||||||
|
655 SE_IMP_VULCAN
|
||||||
|
656 SE_IMP_SHOTGUN
|
||||||
|
657 SE_IMP_FIRE
|
||||||
|
658 SE_IMP_MISSILE
|
||||||
|
659 SE_IMP_ROCKET
|
||||||
|
660 SE_IMP_SMOKE
|
||||||
|
661 SE_IMP_TORPEDO
|
||||||
|
662 SE_IMP_BOMB
|
||||||
|
663 SE_IMP_KINETIC
|
||||||
|
681 SE_IMP_BULLET
|
||||||
|
683 SE_IMP_BS_BEAM
|
||||||
|
684 SE_IMP_BS_RGUN
|
||||||
|
685 SE_IMP_BS_PCANNON
|
||||||
|
686 SE_IMP_BS_LASER
|
||||||
|
687 SE_IMP_BS_BVULCAN
|
||||||
|
688 SE_IMP_BS_VULCAN
|
||||||
|
689 SE_IMP_BS_AAM
|
||||||
|
690 SE_IMP_BS_ASM
|
||||||
|
691 SE_IMP_GRV
|
||||||
|
701 SE_SHIELD_DOWN
|
||||||
|
702 SE_SHIELD_RECHARGE
|
||||||
|
703 SE_SHIELD_DOWN_BS
|
||||||
|
751 SE_BOSS_CRY
|
||||||
|
752 SE_BOSS_CLOSE
|
||||||
|
753 SE_BOSS_OPEN
|
||||||
|
754 SE_BOSS_STUMBLE
|
||||||
|
755 SE_BOSS_MOMENTS
|
||||||
|
756 SE_BOSS_ATK_A
|
||||||
|
758 SE_BOSS_ATK_C
|
||||||
|
759 SE_BOSS_LASER
|
||||||
|
760 SE_BOSS_HOMING
|
||||||
|
761 SE_BOSS_CHARGE
|
||||||
|
762 SE_BOSS_CORE_CHARGE
|
||||||
|
763 SE_BOSS_SPREAD
|
||||||
|
764 SE_CORE_COLLAPSE
|
||||||
|
781 SE_PROME_AMB_1
|
||||||
|
782 SE_PROME_AMB_2
|
||||||
|
783 SE_PROME_AMB_3
|
||||||
|
784 SE_PROME_FIRE
|
||||||
|
785 SE_ENV_MINE
|
||||||
|
786 SE_HNG_ARM_MV
|
||||||
|
787 SE_HNG_MOUNT
|
||||||
|
788 SE_HNG_UN_MOUNT
|
||||||
|
789 SE_HNG_ARM_UP
|
||||||
|
790 SE_HNG_ARM_DWN
|
||||||
|
791 SE_HNG_MOUNT_2
|
||||||
|
792 SE_HNG_UN_MOUNT_2
|
||||||
|
793 SE_HNG_MOUNT_3
|
||||||
|
794 SE_HNG_UN_MOUNT_3
|
||||||
|
795 SE_HNG_MOUNT_4
|
||||||
|
796 SE_HNG_UN_MOUNT_4
|
||||||
|
807 SE_ETC_JMP_OP
|
||||||
|
808 SE_ETC_JMP_IN
|
||||||
|
809 SE_ETC_JMP_OUT
|
||||||
|
810 SE_ENV_BS_M
|
||||||
|
811 SE_ENV_BS_S
|
||||||
|
812 SE_ENV_BS_L
|
||||||
|
813 SE_ENV_BS_LL
|
||||||
|
814 SE_ENV_CARRIER
|
||||||
|
815 SE_ETC_HP_UNE
|
||||||
|
816 SE_ETC_HP_ADAN
|
||||||
|
817 SE_ETC_HP_ACRO
|
||||||
|
818 SE_ENV_WIND
|
||||||
|
819 SE_ENV_WIND_TURN
|
||||||
|
820 SE_ETC_TURRET_L
|
||||||
|
821 SE_ETC_TURRET_M
|
||||||
|
822 SE_ETC_TURRET_S
|
||||||
|
823 SE_PASS_DS
|
||||||
|
824 SE_PASS_ADAN
|
||||||
|
825 SE_PASS_ERAN
|
||||||
|
826 SE_PASS_PLUS
|
||||||
|
827 SE_PASS_TYPEQ
|
||||||
|
828 SE_PASS_VINDI
|
||||||
|
829 SE_ETC_COLLISION
|
||||||
|
830 SE_ETC_SHIELD_DMG
|
||||||
|
900 SE_LFE_BOMB
|
||||||
|
901 SE_LFE_ENG
|
||||||
17
docs/re/data/se-wave-riff-decode.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# se_wave_riff — the three menu cues, decoded end to end
|
||||||
|
|
||||||
|
$ cargo run -p sylpheed-formats --example se_wave_dump -- /tmp/se
|
||||||
|
|
||||||
|
/tmp/se/move.riff: 8252 bytes (4 packets at 0x1ec0)
|
||||||
|
/tmp/se/back.riff: 4156 bytes (2 packets at 0xec0)
|
||||||
|
/tmp/se/confirm.riff: 12348 bytes (6 packets at 0x5d6c0)
|
||||||
|
refusal path ok: Static.slb: 1048576 packets at 0x1ec0 need 2147483648 bytes, but the bank declares only 8970240 bytes
|
||||||
|
|
||||||
|
$ ffmpeg -i <cue>.riff <cue>.wav # then measure the PCM
|
||||||
|
|
||||||
|
move 48000 Hz mono 0.533 s rms 2084.7 peak 29813 non-quiet 47.5%
|
||||||
|
back 48000 Hz mono 0.344 s rms 2984.7 peak 16973 non-quiet 95.1%
|
||||||
|
confirm 48000 Hz mono 1.016 s rms 4327.0 peak 32767 non-quiet 92.5%
|
||||||
|
Non-silent, plausible envelopes, durations consistent with a UI blip.
|
||||||
|
The refusal path is exercised in the same run: an impossible packet count is
|
||||||
|
rejected rather than returning a short stream.
|
||||||
22
docs/re/data/slow-xpr-files.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# .xpr files whose `mesh info` decode exceeds 25 s (debug build), of 166 scanned
|
||||||
|
# 2026-08-29. Stage_S02 measured at 144 s to completion, rc=0 -- heavy, not hung.
|
||||||
|
|
||||||
|
Hangar.xpr
|
||||||
|
Stage_S02.xpr
|
||||||
|
Stage_S03.xpr
|
||||||
|
Stage_S04.xpr
|
||||||
|
Stage_S06.xpr
|
||||||
|
Stage_S07.xpr
|
||||||
|
Stage_S08.xpr
|
||||||
|
Stage_S09.xpr
|
||||||
|
Stage_S11.xpr
|
||||||
|
Stage_S12.xpr
|
||||||
|
Stage_S13.xpr
|
||||||
|
Stage_S15.xpr
|
||||||
|
Stage_S24.xpr
|
||||||
|
Stage_S25.xpr
|
||||||
|
Stage_S26.xpr
|
||||||
|
Stage_S27.xpr
|
||||||
|
Stage_S28.xpr
|
||||||
|
Stage_S29.xpr
|
||||||
|
ptc_pack.xpr
|
||||||
11
docs/re/data/splash-phase-timeline.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Measured splash phase timeline -- from docs/re/captures/ui-timing/splash-build-draws.log
|
||||||
|
# 2026-08-29. Frames are VdSwap counts; 1 frame = 1/30 s (2 units/frame, 1 unit = 1/60 s).
|
||||||
|
#
|
||||||
|
# phase frames n seconds start (rel. capture)
|
||||||
|
publisher: SQUARE ENIX logo 1-90 90 3.00+ 0.00 <- floor: already at a=255 on f1
|
||||||
|
(gap, nothing drawn) 91-93 3 0.10 3.00
|
||||||
|
developer: both glows 94-115 22 0.73 3.10
|
||||||
|
developer: both logos 116-211 96 3.20 3.83
|
||||||
|
#
|
||||||
|
# palogo_anima / palogo_anima_eff: 0 draws in the whole 214-frame window.
|
||||||
|
# The glow->logo switch is a single frame boundary (f115 -> f116), no overlap.
|
||||||
20
docs/re/data/splash-ramp-check.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CONTROL — the eff glow's fade-in must be linear at a constant step
|
||||||
|
alphas [34, 68, 102, 136, 170, 204, 238] steps [34, 34, 34, 34, 34, 34]
|
||||||
|
=> 2.000 units/frame; t(f) = 2*f - 171
|
||||||
|
check: declared hold ends t=45 -> predicted frame 108.0; observed last full-alpha frame 107
|
||||||
|
|
||||||
|
LOGO — palogo_gamearts, checked against that calibration
|
||||||
|
observed: first drawn frame 116 at alpha 255; full alpha through 198; fade-out 199..211
|
||||||
|
declared a= 0 at t= 15 -> frame 93.0 observed alpha NOT DRAWN
|
||||||
|
declared a= 0 at t= 30 -> frame 100.5 observed alpha NOT DRAWN
|
||||||
|
declared a=255 at t=190 -> frame 180.5 observed alpha 255
|
||||||
|
declared a=255 at t=194 -> frame 182.5 observed alpha 255
|
||||||
|
declared a=232 at t=206 -> frame 188.5 observed alpha 255
|
||||||
|
declared a= 32 at t=210 -> frame 190.5 observed alpha 255
|
||||||
|
|
||||||
|
SHAPE, independent of any calibration:
|
||||||
|
declared fade-out spans t=194..210 (16 units); of that,
|
||||||
|
12/16 units drop only 23/255 of the alpha (a near-flat leg),
|
||||||
|
4/16 units drop 200/255 (a cliff).
|
||||||
|
captured fade-out: [255, 254, 243, 237, 215, 181, 148, 131, 98, 65, 48, 23, 15, 7]
|
||||||
|
per-frame drops: [1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8] -> no near-flat leg
|
||||||
97
docs/re/data/title-state-machine.txt
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
GamePart_Title has TWO nested state fields.
|
||||||
|
|
||||||
|
OUTER -- sub_821C7850, the part's dispatcher
|
||||||
|
821c786c lwz r11, 16(r30) ; only runs when this+16 == 3
|
||||||
|
821c7870 cmpwi cr6, r11, 3
|
||||||
|
821c7874 bne cr6, <exit>
|
||||||
|
821c787c lwz r11, 132(r30) ; phase = this+0x84
|
||||||
|
821c7880 cmplwi cr6, r11, 0x4 ; five phases, 0..4
|
||||||
|
821c7888 lis/addi r12, 0x821C78A0 ; jump table
|
||||||
|
821c789c bctr
|
||||||
|
|
||||||
|
phase 0 -> sub_821C5690 (the splash -- independently identified as the
|
||||||
|
splash mechanics in the iterate3E notes)
|
||||||
|
phase 1 -> inline block at 0x821c790c
|
||||||
|
phase 2 -> sub_821C5818
|
||||||
|
phase 3 -> sub_821C5EC0
|
||||||
|
phase 4 -> sub_821C6458 (the title/menu machine, below)
|
||||||
|
|
||||||
|
All four call sites forward the SAME three arguments: (this, r29, r28),
|
||||||
|
where r28 is this function's own third argument -- an EVENT CODE passed
|
||||||
|
through unchanged from ITS caller.
|
||||||
|
|
||||||
|
INNER -- sub_821C6458, the title/menu machine (phase 4 only)
|
||||||
|
|
||||||
|
DISPATCH
|
||||||
|
821c6474 lwz r11, 136(r30) ; state = this+0x88
|
||||||
|
821c6478 cmplwi cr6, r11, 0x9 ; 10 states, 0..9
|
||||||
|
821c647c bgt cr6, 0x821C75B8 ; out of range -> default
|
||||||
|
821c6480 lis r12, 0x821C
|
||||||
|
821c6484 addi r12, r12, 25752 ; jump table at 0x821C6498
|
||||||
|
821c6488 slwi r0, r11, 2
|
||||||
|
821c648c lwzx r0, r12, r0
|
||||||
|
821c6490 mtctr r0
|
||||||
|
821c6494 bctr
|
||||||
|
|
||||||
|
NOTE: a disassembler decodes 0x821C6498..0x821C64BC as `lwz r16, N(r28)`
|
||||||
|
instructions. They are the jump TABLE's ten words, not code.
|
||||||
|
|
||||||
|
STATES
|
||||||
|
case 0 -> 0x821c64c0 installs "TITLE_SCREEN"
|
||||||
|
case 1 -> 0x821c65d0
|
||||||
|
case 2 -> 0x821c66e4 installs "TITLE_MENU"
|
||||||
|
case 3 -> 0x821c6b18
|
||||||
|
case 4 -> 0x821c6b5c nested switch, table at 0x821c6b7c (live: 0,3,5,8)
|
||||||
|
case 5 -> 0x821c6fd4
|
||||||
|
case 6 -> 0x821c7028
|
||||||
|
case 7 -> 0x821c72dc
|
||||||
|
case 8 -> 0x821c733c installs "LOADING"
|
||||||
|
case 9 -> 0x821c7558
|
||||||
|
|
||||||
|
TRANSITIONS -- every `li rX,N ; stw rX,136(r30)` in the function
|
||||||
|
from to at
|
||||||
|
0 1 0x821c6598
|
||||||
|
0 2 0x821c65c8
|
||||||
|
1 2 0x821c66a4
|
||||||
|
1 2 0x821c66dc
|
||||||
|
2 4 0x821c6afc
|
||||||
|
3 4 0x821c6b54
|
||||||
|
4 0 0x821c6e00
|
||||||
|
4 5 0x821c6e70
|
||||||
|
4 8 0x821c6ed0
|
||||||
|
4 8 0x821c6f20
|
||||||
|
4 8 0x821c6f7c
|
||||||
|
4 8 0x821c6fcc
|
||||||
|
5 6 0x821c7020
|
||||||
|
6 7 0x821c725c
|
||||||
|
6 9 0x821c72a8
|
||||||
|
6 2 0x821c72d4
|
||||||
|
7 9 0x821c7334
|
||||||
|
8 2 0x821c7548
|
||||||
|
(state 9 stores nothing -- terminal within this function)
|
||||||
|
|
||||||
|
THE CONDITION ON STATE 4's EDGES -- decoded
|
||||||
|
sub_821C6458(this, ?, r5) takes an EVENT CODE in its third argument.
|
||||||
|
State 4 -- the input-waiting state -- dispatches on it:
|
||||||
|
|
||||||
|
821c6b5c cmplwi cr6, r27, 0x19 ; 26 events, 0..25
|
||||||
|
821c6b60 bgt cr6, <default>
|
||||||
|
821c6b68 addi r12, r12, 27516 ; table at 0x821c6b7c
|
||||||
|
821c6b78 bctr
|
||||||
|
|
||||||
|
Only 6 of the 26 are handled; the other 20 fall through with no state change.
|
||||||
|
|
||||||
|
event 0 -> block 0x821c6be4 -> state 0 (TITLE_SCREEN -- back to the title)
|
||||||
|
event 3 -> block 0x821c6e78 -> state 8 (LOADING)
|
||||||
|
event 5 -> block 0x821c6f84 -> state 8 (LOADING)
|
||||||
|
event 8 -> block 0x821c6f34 -> state 8 (LOADING)
|
||||||
|
event 10 -> block 0x821c6e28 -> state 5
|
||||||
|
event 25 -> block 0x821c6ed8 -> state 8 (LOADING)
|
||||||
|
|
||||||
|
WHAT IS NOT DECODED
|
||||||
|
* what the event NUMBERS mean -- they are forwarded unchanged from
|
||||||
|
sub_821C7850's caller, so the vocabulary is defined at least one level up -- button id, menu-item id or message id;
|
||||||
|
* the conditions on edges out of states other than 4;
|
||||||
|
* what states 1, 3, 5, 6, 7 and 9 do (they install no named screen);
|
||||||
|
* state 3 is never a destination here, so something outside this function
|
||||||
|
sets it.
|
||||||
24
docs/re/data/zero-scale-census.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# tools/re-capture/zero_scale_census.py -- 2026-08-28
|
||||||
|
|
||||||
|
elements with a keyframe group: 15493
|
||||||
|
at least one keyframe at scale 0: 2166
|
||||||
|
EVERY keyframe at scale 0 (would vanish): 0
|
||||||
|
zero is a transient the element grows out of: 1762
|
||||||
|
rest poses with scale 0%%: 195
|
||||||
|
... of which alpha > 0, so blit paints a 1-pixel sliver: 126
|
||||||
|
... alpha == 0, harmless: 69
|
||||||
|
|
||||||
|
by archive:
|
||||||
|
126 GP_READY_ROOM.pak
|
||||||
|
|
||||||
|
examples (pak, entry, element idx, name, sx, sy, alpha):
|
||||||
|
('GP_READY_ROOM.pak', 0, 2, 'pbacropolis.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 0, 6, 'pbFriendly.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 0, 10, 'pbFriendly.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 0, 14, 'pbFriendly.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 7, 2, 'pbacropolis.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 7, 6, 'pbFriendly.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 7, 10, 'pbFriendly.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 7, 14, 'pbFriendly.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 74, 2, 'pbr_enemy.t32', 0, 100, 255)
|
||||||
|
('GP_READY_ROOM.pak', 76, 2, 'pbr_enemy.t32', 0, 100, 255)
|
||||||
50
docs/re/doc-link-audit.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# ✅ Do the docs' cited artifacts exist? — 11 broken links repaired
|
||||||
|
|
||||||
|
**Status:** ✅ checked mechanically and fixed, with the count verified both ways.
|
||||||
|
|
||||||
|
The brief's rule is *"commit the reference data beside the finding, so the port
|
||||||
|
can be built without a disc in the loop"*. An answer whose evidence is not
|
||||||
|
reachable cannot be used. Nothing had ever checked that.
|
||||||
|
|
||||||
|
[`tools/re-capture/doc_link_check.py`](../../tools/re-capture/doc_link_check.py)
|
||||||
|
walks every markdown file under `docs/`, resolves each relative link, and reports
|
||||||
|
targets that do not exist — and, separately, targets that exist but are **empty**,
|
||||||
|
which is the sneakier failure since a zero-byte file looks fine in any listing.
|
||||||
|
|
||||||
|
| | before | after |
|
||||||
|
|---|---|---|
|
||||||
|
| links resolving | 1 038 | **1 049** |
|
||||||
|
| missing targets | **16** | 5 |
|
||||||
|
| empty targets | 0 | 0 |
|
||||||
|
|
||||||
|
**+11 resolving, −11 missing — the two numbers pair exactly**, which is the check
|
||||||
|
that the repair did what it claimed and nothing else.
|
||||||
|
|
||||||
|
## 🔴 Two of them were the evidence for the UI decode itself
|
||||||
|
|
||||||
|
`structures/ui-rat-layout.md` is the layout decode the port is built on. Its two
|
||||||
|
figures — the ones backing *"the tutorial PAUSE menu rebuilds pixel-accurately
|
||||||
|
from its sprites"* and *"the same method reproduces the main menu"* — were
|
||||||
|
written as `captures/ui-layout/…` from a file in `structures/`, one directory
|
||||||
|
too shallow. **The headline evidence for the decode was unreachable from its own
|
||||||
|
document.**
|
||||||
|
|
||||||
|
## What was wrong, and what still is
|
||||||
|
|
||||||
|
Eleven links had the **wrong relative depth** while their targets existed — a
|
||||||
|
missing or surplus `../`, or a missing `structures/`. Those are repaired; each
|
||||||
|
was rewritten only when exactly one candidate path resolved, so nothing was
|
||||||
|
guessed.
|
||||||
|
|
||||||
|
❔ **Five remain genuinely absent** and are left alone rather than invented:
|
||||||
|
|
||||||
|
| doc | target |
|
||||||
|
|---|---|
|
||||||
|
| `autopilot-knowledge-sources.md`, `entities-live-roster.md` | `../../MEMORY.md` — outside the repo |
|
||||||
|
| `challenge-mission-gate.md` | a header in the separate `xenia-canary-native` tree |
|
||||||
|
| `stage-drift-is-navigation-not-save.md` | `structures/weapon-datasheet-runtime.md` — never written |
|
||||||
|
| `mission-freeze-resume-spin.md` | `canary-build-verified-env-confound.md` — never written |
|
||||||
|
|
||||||
|
None is port-relevant: they are mission, entity and emulator-side documents. Two
|
||||||
|
name documents that do not exist, which is a different problem from a bad path
|
||||||
|
and is not something to paper over with a link fix.
|
||||||
@@ -79,7 +79,7 @@ looks stuck on the main menu while it is in fact three screens further on.
|
|||||||
|
|
||||||
## Still open
|
## Still open
|
||||||
|
|
||||||
* The **world-unit measurement** ([collisionset](collisionset.md)) — positions
|
* The **world-unit measurement** ([collisionset](structures/collisionset.md)) — positions
|
||||||
in world units against the HUD's own distance readout. This run reached the
|
in world units against the HUD's own distance readout. This run reached the
|
||||||
mission but the flight HUD was not yet up (green 0.03 %, vs 1.3–1.5 % in
|
mission but the flight HUD was not yet up (green 0.03 %, vs 1.3–1.5 % in
|
||||||
flight), so no distance readout was available to compare against.
|
flight), so no distance readout was available to compare against.
|
||||||
|
|||||||
63
docs/re/five-screens-acceptance.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# ✅ The five screens, end to end — what each one is actually worth
|
||||||
|
|
||||||
|
**Status:** ✅ measured, 2026-08-29. One page, because the answers for these five
|
||||||
|
screens are otherwise spread across a dozen documents and none of them says how
|
||||||
|
good the result *is*.
|
||||||
|
|
||||||
|
Rendered with `sylpheed-cli screen render` (adding `--all --black` for the two
|
||||||
|
splash halves) and correlated against the committed live captures.
|
||||||
|
|
||||||
|
| screen | build | elements drawn | corr vs capture | alignment |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| title | 4 | 15 / 24 | **+0.9500** | dy=0 dx=0 |
|
||||||
|
| main menu | 5 | 11 / 16 | **+0.9460** | dy=0 dx=0 |
|
||||||
|
| `EXTRAS` | 6 | 13 / 18 | **+0.9440** | dy=0 dx=0 |
|
||||||
|
| publisher splash | 10 | 2 / 3 | **+0.9600** | dy=0 dx=0 |
|
||||||
|
| developer splash | 11 | 6 / 7 | **+0.9643** | dy=0 dx=0 |
|
||||||
|
|
||||||
|
Every one aligns at **exactly zero offset** — searched ±2 px in both axes — so
|
||||||
|
placement and scale are right and the residual is tone and detail, not geometry.
|
||||||
|
|
||||||
|
## Every omission is accounted for
|
||||||
|
|
||||||
|
The counts are not slack; each undrawn element has a reason already documented:
|
||||||
|
|
||||||
|
| screen | not drawn | why |
|
||||||
|
|---|---|---|
|
||||||
|
| title | `ptlogo1`×2, `ptlogo2`×2 | `kind & 0x4` **ghost instances** — the draw capture shows one quad per wordmark |
|
||||||
|
| | `pteff00.prm`, `pteff02.prm` | `.prm` primitives, off without `--primitives` |
|
||||||
|
| | `ptloop01.rat`, `ptloop02.rat` | `loop*` animations, off without `--animated`; at rest both sit off-screen |
|
||||||
|
| | `pteff04.t32` | the **`8AX` name mismatch** — the art *is* on screen, drawn via `ptbase2` ([8AX](structures/ui-8ax-fullres-background.md)) |
|
||||||
|
| main menu, `EXTRAS` | 2 `.prm` + 2 `loop*` + `pteff05.t32` | same three reasons |
|
||||||
|
| both splashes | `palogo_eff0.prm` | `.prm` primitive |
|
||||||
|
|
||||||
|
4 + 2 + 2 + 1 = 9 on the title; 2 + 2 + 1 = 5 on each menu; 1 on each splash.
|
||||||
|
**Nothing is unexplained.**
|
||||||
|
|
||||||
|
## What the residual is, per screen
|
||||||
|
|
||||||
|
Ranked by what a port would notice:
|
||||||
|
|
||||||
|
1. **Tone.** The capture is darker than the composite by a gamma of ≈1.34–1.49
|
||||||
|
([tone curve](structures/ui-render-tone-curve.md)) — the game's own display
|
||||||
|
ramp, not an emulator artefact. This is the largest single contributor to the
|
||||||
|
0.94–0.96 rather than higher.
|
||||||
|
2. **`8AX` resolution.** The background is drawn from the 640×360 `ptbase`
|
||||||
|
upscaled 2×, where the game uses the native 1280×720 `8AX`
|
||||||
|
([measured](structures/ui-8ax-fullres-background.md), two screens at 68 % of
|
||||||
|
ceiling).
|
||||||
|
3. **Paint-order ties on `EXTRAS` only.** One drawable overlapping tie
|
||||||
|
(`ptframe3`×`ptframe4`), and the capture is consistent with our order
|
||||||
|
([check](structures/ui-paint-order-derived-check.md)). The other four screens
|
||||||
|
use orders measured from the game.
|
||||||
|
4. **Rotation is decoded but not rendered** — and does not affect any of these
|
||||||
|
five at rest ([rotation](structures/ui-keyframe-rotation.md)).
|
||||||
|
|
||||||
|
## Reach
|
||||||
|
|
||||||
|
* These are **static composites at the resting pose** against **single frames**.
|
||||||
|
Nothing here says the animation is right; Q1's interpolation law is settled but
|
||||||
|
a multi-keyframe group's absolute timing is not
|
||||||
|
([time unit](ui-keyframe-time-unit.md)).
|
||||||
|
* Correlation on a whole frame is dominated by large areas. It is a sanity figure,
|
||||||
|
not a per-element check — the per-element work is in the documents linked above.
|
||||||
263
docs/re/menu-audio-cues.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Menu audio — the event vocabulary is on the disc; the binding is not
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED` and **decoded** for the cue vocabulary and the bank
|
||||||
|
that holds it. 🟡 the cue↔event binding is a **name match**, not a measurement.
|
||||||
|
❔ two negatives with their reach: no cue names a screen's music, and the audio
|
||||||
|
for an individual SE cue cannot yet be extracted.
|
||||||
|
|
||||||
|
Answers most of [MISSION Q8](../port/MISSION.md).
|
||||||
|
|
||||||
|
## ✅ The UI cue vocabulary — decoded
|
||||||
|
|
||||||
|
`dat/tables.pak`'s sound table has a `SOUNDS` record of 5 798 cues. 322 are `SE_*`
|
||||||
|
and the **low block is the user-interface vocabulary**, named by the game's
|
||||||
|
authors after the *events*, not after the sounds:
|
||||||
|
|
||||||
|
| id | cue | id | cue |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | `SE_UI_START` | 9 | `SE_UI_SUB_WIN_CLS` |
|
||||||
|
| **2** | **`SE_UI_CURSOR`** | 10 | `SE_UI_ALART_WIN` |
|
||||||
|
| **3** | **`SE_UI_DECIDE`** | 11 | `SE_UI_PAUSE` |
|
||||||
|
| **4** | **`SE_UI_CANSEL`** *(sic)* | 12 | `SE_UI_SPLASH_IN` |
|
||||||
|
| **5** | **`SE_UI_IMPOSI`** | 13 | `SE_UI_SPLASH_OUT` |
|
||||||
|
| 6 | `SE_UI_WAIT` | 14 | `SE_UI_LOAD_CMP` |
|
||||||
|
| 7 | `SE_UI_NEXT` | 17/18 | `SE_UI_WEAPON_PLAN` / `_CREATE` |
|
||||||
|
| 8 | `SE_UI_SUB_WIN_OPN` | 81–83 | `SE_UI_MISSION_START` / `_UPDATE` / `_END` |
|
||||||
|
|
||||||
|
Full 322-cue list in [`data/se-ui-cues.txt`](data/se-ui-cues.txt). The remaining
|
||||||
|
blocks are gameplay: `SE_HUD_*` (21–83), `SE_BR_*` briefing (50–72), `SE_SW_*` /
|
||||||
|
`SE_MW_*` weapons, `SE_DS_*` engine, `SE_EXP_*` explosions, `SE_SHIELD_*`.
|
||||||
|
|
||||||
|
## ✅ They all live in one bank — decoded
|
||||||
|
|
||||||
|
The sound table's `BANK_SE` record is a **single field**: `Static.slb`. And the
|
||||||
|
disc-wide check agrees — **0 of the 322 `SE_*` cues has an entry in `FILES`**, the
|
||||||
|
5 135-path list where every voice, briefing and BGM bank is named. SE is the one
|
||||||
|
family that is not one-cue-one-file.
|
||||||
|
|
||||||
|
`SETTINGS` names the XACT project too: `PATH` = `game:\dat\sound.pak+`,
|
||||||
|
`PARAM` = `Pj_Silph.xgs`.
|
||||||
|
|
||||||
|
## 🟡 The binding: a name match, and I am calling it that
|
||||||
|
|
||||||
|
MISSION asks which cue fires on **move / confirm / back / error**. Read off the
|
||||||
|
names:
|
||||||
|
|
||||||
|
| event | cue |
|
||||||
|
|---|---|
|
||||||
|
| cursor moves | `SE_UI_CURSOR` (2) |
|
||||||
|
| Ⓐ confirm | `SE_UI_DECIDE` (3) |
|
||||||
|
| Ⓑ back | `SE_UI_CANSEL` (4) |
|
||||||
|
| invalid / error | `SE_UI_IMPOSI` (5) |
|
||||||
|
| submenu opens / closes | `SE_UI_SUB_WIN_OPN` / `_CLS` (8/9) |
|
||||||
|
| the splash | `SE_UI_SPLASH_IN` / `_OUT` (12/13) |
|
||||||
|
|
||||||
|
**Nobody has watched the game emit cue 2 on a d-pad press.** This is inference
|
||||||
|
from identifiers, and it belongs in the same box as Q4's GamePart ids — with one
|
||||||
|
honest difference worth stating: these are the *authors' own event names*, chosen
|
||||||
|
to describe when the sound plays, not asset labels I am interpreting. It is a
|
||||||
|
strong name match. It is still a name match, and the port is authoring it.
|
||||||
|
|
||||||
|
Measuring it needs the guest's cue-play call observed with its argument — the
|
||||||
|
same instrumentation Q4's residual wants, and not something this container can do
|
||||||
|
today (it runs `--mute=true` against an SDL dummy device, so there is no audio
|
||||||
|
path to watch either).
|
||||||
|
|
||||||
|
## ✅ Which BGM per screen — the TABLE cannot say, but the CODE can
|
||||||
|
|
||||||
|
All 32 BGM cues are named `BGM_001`…`BGM_109`, and nothing in `SOUNDS`, `FILES` or
|
||||||
|
the bank headers names a screen. That negative stands **for the tables**.
|
||||||
|
|
||||||
|
It does not stand for the executable. `GamePart_Title`'s phase handler
|
||||||
|
`sub_821C5580` contains `li r5, 1103` feeding a sound call — **cue 1103 =
|
||||||
|
`BGM_103`** — and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 bytes)
|
||||||
|
are byte-for-byte the two streams the XMA probe saw decoding at the main menu.
|
||||||
|
Three routes, one answer; see
|
||||||
|
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md).
|
||||||
|
|
||||||
|
So the per-screen BGM binding is recoverable **per screen, from the code that
|
||||||
|
plays it** — not from any table.
|
||||||
|
|
||||||
|
## ❔ And a new negative: an individual SE's audio is not extractable yet
|
||||||
|
|
||||||
|
`Static.slb` is **8 353 472 readable bytes** (declared 8 970 240 — exactly the
|
||||||
|
616 768 over-declaration the corpus already records), and it contains
|
||||||
|
**0 `RIFF`, 0 `seek`, 0 `WAVE`** — scanned over the whole buffer, so this is not
|
||||||
|
an offset problem. Every other bank on the disc is delimited by `seek` magic at
|
||||||
|
`data_at + declared_size` (7 620/7 620 in
|
||||||
|
[`slb-data-offset.md`](structures/slb-data-offset.md)); `Static.slb` is outside
|
||||||
|
that population. So the route that extracts every voice line and every BGM does
|
||||||
|
**not** give you `SE_UI_CURSOR`'s audio: the cue is named, the bank is named, and
|
||||||
|
the wave inside the bank is not locatable by any boundary marker.
|
||||||
|
|
||||||
|
**The named next step is `Pj_Silph.xgs`** — the XACT project file `SETTINGS`
|
||||||
|
points at, which in XACT is exactly where cue→wave-index lives. It **is** in
|
||||||
|
`sound.pak`: `name_hash("Pj_Silph.xgs")` resolves to TOC index 9454. ⚠️ Its entry
|
||||||
|
is 533 bytes at a 2048-aligned offset and carries no `XGSF` magic — and its region
|
||||||
|
has phase 1728, so those 533 bytes are very likely the *previous* bank's tail
|
||||||
|
rather than its own (the leading-region effect, [`slb-data-offset.md`]
|
||||||
|
(structures/slb-data-offset.md)). Locating its real bytes is the first job, not
|
||||||
|
parsing XACT.
|
||||||
|
|
||||||
|
## For the port
|
||||||
|
|
||||||
|
* the cue **names and ids** are disc facts — use them as the event vocabulary;
|
||||||
|
* which event fires which cue is **authored from the table above**;
|
||||||
|
* which BGM plays on which screen is **authored** — nothing on the disc says;
|
||||||
|
* and the UI **sound effects cannot be exported yet**. That is a gap in the
|
||||||
|
assets, not in the naming.
|
||||||
|
|
||||||
|
## ❔ `Pj_Silph.xgs` is a dead end — retracting the lead, with the reach
|
||||||
|
|
||||||
|
The section above named `Pj_Silph.xgs` as "the named next step", on the reasoning
|
||||||
|
that in XACT the cue→wave index lives in the project file. **That route is dead.**
|
||||||
|
|
||||||
|
| check | result |
|
||||||
|
|---|---|
|
||||||
|
| the entry's own bytes | 533 bytes, high entropy, **no `XGSF` magic** |
|
||||||
|
| ±8 KB around the entry in the flat concatenation | no `XGSF`, `SDBK`, `WBND`, `RIFF` or `seek` |
|
||||||
|
| **all of `sound.pak` — 1.08 GB across `.p00`–`.p04`** | **0 × `XGSF`, 0 × `SDBK`, 0 × `WBND`** |
|
||||||
|
| the executable | **0** occurrences of `XGSF`/`XACT`; no string matching `xact`, `.xgs`, `wavebank` or `soundbank` |
|
||||||
|
|
||||||
|
**Control, run first:** the same magic scan over `BGM_001.slb`'s neighbourhood
|
||||||
|
finds `RIFF` exactly where the bank structure says it should be. The scan works;
|
||||||
|
the magic genuinely is not there.
|
||||||
|
|
||||||
|
So the `.xgs` / `.slb` names are **inherited from the authoring tool, not from the
|
||||||
|
shipped format** — nothing on this disc is a parseable XACT container. There is no
|
||||||
|
XACT project file to read, and writing an XACT parser would have been wasted work.
|
||||||
|
|
||||||
|
### Where that leaves the SE audio: undecodable, with reach
|
||||||
|
|
||||||
|
Locating an individual SE cue's wave inside `Static.slb` is **not possible from
|
||||||
|
anything found on the disc**. Looked in:
|
||||||
|
|
||||||
|
* `Static.slb` itself — 8 353 472 readable bytes, **0 `RIFF` / 0 `seek` / 0 `WAVE`**,
|
||||||
|
where every one of the other 7 620 banks is delimited by `seek` magic at
|
||||||
|
`data_at + declared_size` ([`structures/slb-data-offset.md`](structures/slb-data-offset.md));
|
||||||
|
* the XACT project file — absent, as above;
|
||||||
|
* the sound table's five records — `SOUNDS` gives cue→id, `FILES` gives bank
|
||||||
|
paths, `BANK_SE` gives the one bank name, `SETTINGS` gives a path and the
|
||||||
|
(absent) project file, `STAGES` is empty. **No record carries an offset.**
|
||||||
|
|
||||||
|
**What does corroborate the binding:** `Static.slb` is 8 353 472 bytes, and at the
|
||||||
|
bitrates the disc uses elsewhere (25 697–31 000 B/s) that is **269–325 seconds of
|
||||||
|
audio — 0.84 to 1.01 s per cue over 322 cues.** Exactly the shape of a bank of
|
||||||
|
short UI/HUD/weapon effects, which is what `BANK_SE` says it is. The bank is the
|
||||||
|
right bank; only the index into it is missing.
|
||||||
|
|
||||||
|
**If it is ever picked up**, the route is the running game, not the disc: watch
|
||||||
|
the guest hand a wave offset/length to the decoder when a cue fires. That is the
|
||||||
|
same instrumentation the cue↔event binding needs, and the same one this container
|
||||||
|
cannot run.
|
||||||
|
|
||||||
|
## ✅ Retracting "cannot be extracted" — the waves are locatable, by playing them
|
||||||
|
|
||||||
|
The section above concluded that an individual SE cue's audio "cannot be
|
||||||
|
extracted". **That was too strong, and the tool to do it was already in this
|
||||||
|
build.** Canary here carries a cvar `xma_param_probe`, added for exactly this
|
||||||
|
purpose — its own comment says it logs each XMA stream's parameters and head
|
||||||
|
bytes "so raw sound.pak entries can be matched to real decode params".
|
||||||
|
|
||||||
|
Run with `--xma_param_probe=true`, drive the menu, and each newly-decoded stream
|
||||||
|
prints one line with its packet count, channel count, sample rate and **first 32
|
||||||
|
bytes**. Searching those bytes in `Static.slb` finds the wave.
|
||||||
|
|
||||||
|
| event | packets | bytes | format | offset in `Static.slb` |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **d-pad move** (cursor) | 4 | 8 192 | mono 48 kHz | **`0x1ec0`** |
|
||||||
|
| **Ⓑ back** (cancel) | 2 | 4 096 | mono 48 kHz | **`0x0ec0`** |
|
||||||
|
| **Ⓐ confirm** (title → main menu) | 6 | 12 288 | mono 48 kHz | **`0x5d6c0`** |
|
||||||
|
|
||||||
|
Each head matched at **exactly one** offset. Reference data in
|
||||||
|
[`data/se-cue-runtime-offsets.txt`](data/se-cue-runtime-offsets.txt).
|
||||||
|
|
||||||
|
**Structurally this also cracks the bank's layout.** The first two are
|
||||||
|
*contiguous* — `0x0ec0 + 4096 = 0x1ec0` — so `Static.slb` is a packed run of
|
||||||
|
XMA waves, each a whole number of 2 048-byte packets, with no delimiter between
|
||||||
|
them. That is why the `seek`-magic scan found nothing: there is nothing to find.
|
||||||
|
A wave is defined only by (offset, packet count), and those live in the game's
|
||||||
|
own tables, not in the bank.
|
||||||
|
|
||||||
|
⚠️ **The order is not cue-id order.** `Ⓑ`'s wave precedes the cursor's in the
|
||||||
|
file, while the name match puts `SE_UI_CURSOR` at id 2 and `SE_UI_CANSEL` at 4.
|
||||||
|
So the index cannot be derived by counting; it has to be observed per cue.
|
||||||
|
|
||||||
|
### What is now measured, and what is still a name match
|
||||||
|
|
||||||
|
* ✅ **measured** — a d-pad move and a Ⓑ press each play a *specific, different*
|
||||||
|
wave, and both waves are located in `Static.slb`. The port can have the audio.
|
||||||
|
* 🟡 **still a name match** — that the cursor's wave *is the cue called*
|
||||||
|
`SE_UI_CURSOR`. The event→wave binding is measured; the event→*name* binding is
|
||||||
|
still read off the authors' identifiers.
|
||||||
|
* ✅ **decoded to PCM** — see below. The cues play.
|
||||||
|
|
||||||
|
### ✅ The waves decode — artifact, not assertion
|
||||||
|
|
||||||
|
[`tools/re-capture/slb_extract_wave.py`](../../tools/re-capture/slb_extract_wave.py)
|
||||||
|
wraps a `(bank, offset, packet count, channels, rate)` slice in a synthesized
|
||||||
|
XMA1 `RIFF` for ffmpeg's `xma1` decoder:
|
||||||
|
|
||||||
|
```
|
||||||
|
slb_extract_wave.py Static.slb 0x1ec0 4 # -> Static_0x1ec0.riff -> 0.533 s
|
||||||
|
```
|
||||||
|
|
||||||
|
| event | offset | packets | decodes to | RMS | peak | envelope |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| d-pad move | `0x1ec0` | 4 | **0.533 s** | 2 085 | 29 813 | sharp attack, monotonic decay |
|
||||||
|
| Ⓑ back | `0x0ec0` | 2 | **0.344 s** | 2 985 | 16 973 | attack, peak in frame 2 |
|
||||||
|
| **Ⓐ confirm** | `0x5d6c0` | 6 | **1.016 s** | 4 327 | 32 767 | peaks in frame 3, long decay |
|
||||||
|
|
||||||
|
All three are mono 48 kHz, audible from sample 0, and have the percussive
|
||||||
|
attack-and-decay shape of UI blips — not silence, not noise.
|
||||||
|
|
||||||
|
**Control, run first:** the *same* synthesized wrapper applied to `BGM_001.slb`'s
|
||||||
|
first wave decodes to **173.808875 s** — identical to the duration obtained from
|
||||||
|
that bank's own on-disc `RIFF` header in
|
||||||
|
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md). The wrapper is not
|
||||||
|
approximately right; it reproduces a known-good decode exactly.
|
||||||
|
|
||||||
|
The bitrates are consistent too: 8 192 B / 0.533 s ≈ 15.4 kB/s, 4 096 / 0.344 ≈
|
||||||
|
11.9 kB/s, 12 288 / 1.016 ≈ 12.1 kB/s — mono at roughly half the stereo BGM rate.
|
||||||
|
|
||||||
|
🟡 **The decoded audio is deliberately not committed.** Three commands regenerate
|
||||||
|
it from the disc, and the corpus keeps measurements and tooling rather than
|
||||||
|
extracted game audio. The offsets, packet counts and this tool are the
|
||||||
|
deliverable.
|
||||||
|
|
||||||
|
## The three cues the milestone needs are bound — 2026-08-28, second run
|
||||||
|
|
||||||
|
A second, independent boot repeated the probe and pinned the one that was still
|
||||||
|
unattributed.
|
||||||
|
|
||||||
|
| event | wave | packets / bytes | runs agreeing |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **d-pad move** | `Static.slb+0x1ec0` | 4 / 8 192 | **2 of 2** — identical head bytes |
|
||||||
|
| **Ⓑ back** | `Static.slb+0x0ec0` | 2 / 4 096 | **2 of 2** — identical head bytes |
|
||||||
|
| **Ⓐ confirm** | `Static.slb+0x5d6c0` | 6 / 12 288 | newly bound this run |
|
||||||
|
|
||||||
|
The `0x5d6c0` wave was previously logged as "played before any input", because
|
||||||
|
the first run only started counting at the main menu. Counting from the **title**
|
||||||
|
instead shows it appears exactly on the Ⓐ that advances title → main menu, along
|
||||||
|
with the two stereo BGM stems (the menu's music starting).
|
||||||
|
|
||||||
|
So move / confirm / back — everything the five-screen milestone needs — are
|
||||||
|
located and decodable.
|
||||||
|
|
||||||
|
🟡 **What that Ⓐ measurement does not separate.** The same press both *confirms*
|
||||||
|
and *opens a screen*, so this wave could be the cue the vocabulary calls
|
||||||
|
`SE_UI_DECIDE` or the one it calls `SE_UI_SUB_WIN_OPN`. The port needs *a* sound
|
||||||
|
on confirm and this is the one the game plays there; which name it carries is not
|
||||||
|
established.
|
||||||
|
|
||||||
|
## ❔ ⬅ and ➡ play nothing distinct
|
||||||
|
|
||||||
|
[`menu-navigation-semantics.md`](menu-navigation-semantics.md) measured that
|
||||||
|
left/right do not move the cursor. They also **fire no new XMA stream** — so
|
||||||
|
there is no distinct "invalid" cue on the main menu, and `SE_UI_IMPOSI` was not
|
||||||
|
observed. The port should leave left/right silent.
|
||||||
|
|
||||||
|
⚠️ **Reach of that negative:** the probe dedups on head bytes, so a press that
|
||||||
|
replayed an *already-heard* wave would log nothing. At that point in the run the
|
||||||
|
cursor, confirm and BGM waves had all been heard. So this excludes a **distinct**
|
||||||
|
invalid cue; it cannot exclude left/right quietly replaying one of those three.
|
||||||
136
docs/re/menu-navigation-semantics.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# The title menu — how it moves, and where each button goes
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED` (**measured**, by driving the running game) for the
|
||||||
|
movement rules and for four of the five main-menu destinations. 🟡 the GamePart
|
||||||
|
*id* behind each destination is a **name match onto the decoded id table**, not a
|
||||||
|
measurement. ❔ `NEW GAME` deliberately untested.
|
||||||
|
|
||||||
|
Answers [MISSION Q5](../port/MISSION.md) and most of Q4. Nothing here is on the
|
||||||
|
disc in any form found so far — the port is **authoring** these rules from this
|
||||||
|
page, not transcribing a field.
|
||||||
|
|
||||||
|
## Q5 — movement
|
||||||
|
|
||||||
|
Two boots via `tools/re-capture/boot_menu.sh`, cursor read off the focus ring
|
||||||
|
with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py).
|
||||||
|
|
||||||
|
| | behaviour | evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| **initial focus, main menu** | **`TUTORIAL`** — the *middle* item, not the top | 2/2 boots, the first frame after the menu appears |
|
||||||
|
| **initial focus, `EXTRAS`** | `MISSION SELECT` — the top item | [`extras-wrap.png`](captures/menu-nav/extras-wrap.png) |
|
||||||
|
| **up / down** | one item per press, no auto-repeat at the durations tried | |
|
||||||
|
| **wrap at the top** | ⬆ from the first item goes to the **last** | [`wrap-montage.png`](captures/menu-nav/wrap-montage.png), panels 1→2 |
|
||||||
|
| **wrap at the bottom** | ⬇ from the last item goes to the **first** | same, panels 3→4, and 4 presses from `EXTRAS` landing on `OPTIONS` — i.e. wrapping — is what makes the count come out |
|
||||||
|
| **left / right** | **nothing**, on the main menu | cursor unmoved across one ⬅ and one ➡ |
|
||||||
|
| **Ⓑ on a submenu** | returns to the parent **with focus restored to the item you entered from** — `LOAD GAME`→`LOAD GAME`, `TUTORIAL`→`TUTORIAL`, `OPTIONS`→`OPTIONS`, `EXTRAS`→`EXTRAS` | 4/4 |
|
||||||
|
| **Ⓑ on the main menu** | goes to the **title**, which re-draws `PRESS Ⓐ BUTTON` after a beat | |
|
||||||
|
| **Ⓑ on the title** | **nothing** | |
|
||||||
|
|
||||||
|
Wrap holds on both screens tested — the 5-item main menu and the 3-item `EXTRAS`
|
||||||
|
submenu — so it is a menu rule, not a per-screen table.
|
||||||
|
|
||||||
|
**🟡 Initial focus is reproducible but not established as invariant.** Both of my
|
||||||
|
boots opened on `TUTORIAL`, and both used `boot_menu.sh`. The run recorded in
|
||||||
|
[`menu-state-in-memory.md`](menu-state-in-memory.md) reached `EXTRAS` with *four*
|
||||||
|
downs from the main menu, which only works from `NEW GAME`. Either the harness
|
||||||
|
path matters or something persists. **Do not hardcode `TUTORIAL` without
|
||||||
|
re-testing it**; what is solid is that the menu does *not* open on the top item
|
||||||
|
in this harness.
|
||||||
|
|
||||||
|
## Q4 — where each button goes
|
||||||
|
|
||||||
|
Measured by driving: focus the item, press Ⓐ, read the screen's own title.
|
||||||
|
|
||||||
|
| button | screen it opens | evidence | GamePart id |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `NEW GAME` | ❔ **not tested** | — | — |
|
||||||
|
| `LOAD GAME` | the save-slot list, `LOAD GAME` / `Current Storage` | [`q4-destinations.png`](captures/menu-nav/q4-destinations.png) left | 🟡 `3 GP_LOAD` |
|
||||||
|
| `TUTORIAL` | the lesson list, `TUTORIAL`, Level 1 / Level 2 | same, middle | 🟡 `25 GP_TUTORIAL` |
|
||||||
|
| `OPTIONS` | `OPTIONS` — GAME / CONTROL / SOUND / SCREEN SETTINGS / BACK | same, right | 🟡 `8 GP_OPTIONS` |
|
||||||
|
| `EXTRAS` | **`GP_TITLE.pak` build 6** — MISSION SELECT / MOVIE THEATER / BACK | [`ui-title-build-map.md`](ui-title-build-map.md) | 🟡 `5 GP_EXTRAS` |
|
||||||
|
| `EXTRAS ▸ MISSION SELECT` | the stage list + Wide Area Space Map | | 🟡 `7 GP_MISSION_SELECT` |
|
||||||
|
| `EXTRAS ▸ MOVIE THEATER` | ❔ not tested | | 🟡 `6 GP_MOVIE_THEATER` |
|
||||||
|
|
||||||
|
**Say which, as the gate asks.** The *screen* each button opens is **measured** —
|
||||||
|
I pressed the button and read the title off the framebuffer. The **GamePart id is
|
||||||
|
not measured**: it is the entry of the decoded 29-id table at `.rdata 0x820A1630`
|
||||||
|
([`challenge-mission-gate.md`](challenge-mission-gate.md) §3) whose *name* matches
|
||||||
|
the screen I saw. The table is decoded; the *binding* of a button to an entry in
|
||||||
|
it is a name match I made by eye. The port should treat these ids as authored.
|
||||||
|
|
||||||
|
Worth noting that the ids and the paks are not one-to-one: `GP_EXTRAS` is id 5
|
||||||
|
with **no pak of its own** — its artwork is a build inside `GP_TITLE.pak`.
|
||||||
|
|
||||||
|
**❔ `NEW GAME` was deliberately not pressed.** Ⓐ on it leads to a standing
|
||||||
|
black-screen hang that ends the run
|
||||||
|
([`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md)),
|
||||||
|
and this iteration needed the session. It is the one destination still unmeasured.
|
||||||
|
|
||||||
|
### The cheap way to finish this, and to make it a measurement
|
||||||
|
|
||||||
|
`0x828A690C` holds a **live screen id** — `1` title, `3` main menu, `4` extras —
|
||||||
|
and `0x828F38AC` the cursor ([`menu-state-in-memory.md`](menu-state-in-memory.md)).
|
||||||
|
Reading those while pressing Ⓐ turns "the screen said OPTIONS" into a measured
|
||||||
|
transition, and works under `--gpu=null` with no screenshots at all. ⚠️ Note that
|
||||||
|
those values are **not** GamePart ids — `GP_TITLE` is GamePart 0 and `GP_EXTRAS`
|
||||||
|
is 5, but the word reads 1 and 4 — so it is a third enumeration and mapping it to
|
||||||
|
the id table is itself unfinished work.
|
||||||
|
|
||||||
|
## ✅ `NEW GAME` — measured 2026-08-28, and it is not a hang
|
||||||
|
|
||||||
|
The one destination this page could not test has been driven. **Ⓐ on `NEW GAME`
|
||||||
|
does not hang.** It opens two more menus first:
|
||||||
|
|
||||||
|
```
|
||||||
|
NEW GAME -> DIFFICULTY -> SELECT DATA -> guest crash
|
||||||
|
```
|
||||||
|
|
||||||
|
* **`DIFFICULTY`** — `EASY` / `NORMAL` / `HARD` / `BACK`, footer
|
||||||
|
`Ⓐ : OK Ⓑ : Back`, opening focused on **`NORMAL`**
|
||||||
|
([capture](captures/newgame-path/newgame-difficulty.png)). It sat unchanged for
|
||||||
|
90 s with the emulator healthy — a menu waiting for input, which is exactly
|
||||||
|
what "a standing hang" looks like to a screenshot loop that never presses
|
||||||
|
anything.
|
||||||
|
* Ⓐ on `NORMAL` → **`SELECT DATA`**, a save-slot picker headed
|
||||||
|
`Current Storage: Dummy HDD`, prompting to pick a file for the auto-save.
|
||||||
|
* Then the guest throws, and Xenia pauses with `PC: 0x82307128`
|
||||||
|
([capture](captures/newgame-path/newgame-selectdata-crash.png)).
|
||||||
|
|
||||||
|
**That crash is already in the corpus and is not new**: `0x82307128` is inside
|
||||||
|
`sub_823070B0`, the cache-manager STL erase documented in
|
||||||
|
[`title-crash-stl-tree.md`](title-crash-stl-tree.md), whose trigger is an
|
||||||
|
incomplete on-disc shader/code cache — not the menu path. The corpus also already
|
||||||
|
holds `captures/select-data-crash.png`.
|
||||||
|
|
||||||
|
So Q4's last row closes as **measured**:
|
||||||
|
|
||||||
|
| button | screen it opens |
|
||||||
|
|---|---|
|
||||||
|
| `NEW GAME` | **`DIFFICULTY`** (then `SELECT DATA`) |
|
||||||
|
|
||||||
|
🟡 The GamePart ids for these two are unmeasured, like the rest — `24 GP_DIALOG`
|
||||||
|
and `3 GP_LOAD` / `2 GP_SELECT_STORAGE` are name-match candidates and nothing
|
||||||
|
more.
|
||||||
|
|
||||||
|
### Initial focus — a fourth data point, and it still varies
|
||||||
|
|
||||||
|
This boot opened the main menu on **`NEW GAME`**. Running tally across four
|
||||||
|
boots of the same harness: `TUTORIAL`, `TUTORIAL`, `NEW GAME`, `NEW GAME`.
|
||||||
|
Unchanged conclusion: **do not hardcode it.**
|
||||||
|
|
||||||
|
### The `NEW GAME` path completes — the `SELECT DATA` crash is state, not path
|
||||||
|
|
||||||
|
A second run of the same path, 2026-08-28, **did not crash**:
|
||||||
|
|
||||||
|
```
|
||||||
|
NEW GAME → DIFFICULTY → (Ⓐ on NORMAL) → SELECT DATA → (Ⓐ on a slot) → a movie plays
|
||||||
|
```
|
||||||
|
|
||||||
|
The previous run's throw at `PC 0x82307128` is therefore **not inherent to this
|
||||||
|
menu path** — the same six presses got through it. That is consistent with the
|
||||||
|
trigger `title-crash-stl-tree.md` already names (an incomplete on-disc cache) and
|
||||||
|
with nothing about `NEW GAME`. 🟡 n = 1 either way; do not read it as "fixed".
|
||||||
|
|
||||||
|
Worth recording because the first observation could easily have hardened into
|
||||||
|
"the new-game path crashes", which is what "A on NEW GAME hangs" had already
|
||||||
|
become once.
|
||||||
@@ -264,7 +264,7 @@ and the boot never happens.
|
|||||||
|
|
||||||
A Stage 02 run froze after ~4 minutes of flight (screen still `flight`, not GAME
|
A Stage 02 run froze after ~4 minutes of flight (screen still `flight`, not GAME
|
||||||
OVER), and `gdb_bt.sh` took backtraces of all **79** threads
|
OVER), and `gdb_bt.sh` took backtraces of all **79** threads
|
||||||
([`captures/stage02-freeze-gdb-backtraces.txt`](../captures/stage02-freeze-gdb-backtraces.txt)).
|
([`captures/stage02-freeze-gdb-backtraces.txt`](captures/stage02-freeze-gdb-backtraces.txt)).
|
||||||
|
|
||||||
**Every single one is in a wait.** Not one thread is executing guest code or
|
**Every single one is in a wait.** Not one thread is executing guest code or
|
||||||
sitting in a xenia loop:
|
sitting in a xenia loop:
|
||||||
@@ -394,7 +394,7 @@ and the control are all in place; what is missing is one frozen sample.
|
|||||||
The fourth run froze **9 seconds** into the watcher's window, in flight
|
The fourth run froze **9 seconds** into the watcher's window, in flight
|
||||||
(`freeze_watch.sh` confirmed the HUD was still on screen), and the probe built
|
(`freeze_watch.sh` confirmed the HUD was still on screen), and the probe built
|
||||||
for exactly this moment reported **the healthy-run baseline and nothing else**
|
for exactly this moment reported **the healthy-run baseline and nothing else**
|
||||||
([`captures/stage02-freeze-stuck-wait-probe.txt`](../captures/stage02-freeze-stuck-wait-probe.txt)):
|
([`captures/stage02-freeze-stuck-wait-probe.txt`](captures/stage02-freeze-stuck-wait-probe.txt)):
|
||||||
|
|
||||||
```
|
```
|
||||||
FROZEN IN FLIGHT at 9s
|
FROZEN IN FLIGHT at 9s
|
||||||
|
|||||||
178
docs/re/movie-binding.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Which movie plays where — the boot intro, the attract loop, the new-game intro
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED`. The bindings are **decoded** from the movie manifest,
|
||||||
|
and the attract movie is independently **measured** from captured frames. One
|
||||||
|
part — whether playback is skippable — is 🟡 and the corpus contradicts itself
|
||||||
|
about it; that is written down rather than resolved by assertion.
|
||||||
|
|
||||||
|
Answers [MISSION Q9](../port/MISSION.md), and contributes to Q6.
|
||||||
|
|
||||||
|
## The manifest names the boot-side slots — decoded
|
||||||
|
|
||||||
|
The movie manifest (`dat/tables.pak` entry `0x5b983a08`, schema `0x067025b9`,
|
||||||
|
[`movie_manifest.rs`](../../crates/sylpheed-formats/src/movie_manifest.rs)) is an
|
||||||
|
array of slots whose **key is the semantic role**, with a parallel array of value
|
||||||
|
records in the same order. Its first eight slots are the whole boot-side flow:
|
||||||
|
|
||||||
|
| slot key | movie | also bound |
|
||||||
|
|---|---|---|
|
||||||
|
| `LOGO1` … `LOGO4` | `logo1.wmv` … `logo4.wmv` | — |
|
||||||
|
| **`ADVERTISE_MOVIE`** | **`ADV.wmv`** | `VOICE_ADV` |
|
||||||
|
| `STAFF_ROLL` | `SYLPH_HD720p_8M-CBR_2ch.wmv` | subtitle table |
|
||||||
|
| **`MS00A`** | **`S00A.wmv`** | `SUBTITLE_S00A`, `VOICE_S00A`, a `pwterop_s01a.prt` overlay |
|
||||||
|
| `MS01A` | `S01A.wmv` | `SUBTITLE_S01A`, `VOICE_S01A` |
|
||||||
|
|
||||||
|
The alignment is not in doubt here even though the manifest has valueless slots
|
||||||
|
elsewhere: `logo1..4` zip 1:1 onto `LOGO1..4`, and `MS00A`→`S00A.wmv`,
|
||||||
|
`MS01A`→`S01A.wmv` are the manifest's own anchor rule.
|
||||||
|
|
||||||
|
**The `logo1`–`logo4` slots are bound to `.wmv` files that are not on the disc** —
|
||||||
|
already established, and the reason the developer splash is a screen, not a video.
|
||||||
|
|
||||||
|
## ✅ The boot intro and the attract movie are the same asset
|
||||||
|
|
||||||
|
There is **no separate boot-intro slot**. `ADV.wmv` is bound to
|
||||||
|
`ADVERTISE_MOVIE` — it *is* the advertise movie, and the boot simply plays it
|
||||||
|
first. For the port that means one video, not two.
|
||||||
|
|
||||||
|
### Measured independently, from captured frames
|
||||||
|
|
||||||
|
Frames captured 5 s apart during an attract cycle were matched against five
|
||||||
|
candidate movies by frame signature (32×18 normalised grayscale, movie frames
|
||||||
|
cropped to the 675 of 720 rows the game surface shows, 1 fps sampling).
|
||||||
|
`tools/re-capture/frame_match.py`; full table in
|
||||||
|
[`data/attract-frame-match.txt`](data/attract-frame-match.txt).
|
||||||
|
|
||||||
|
**15 of 19 attract frames matched `ADV.wmv`, and the matched timestamps advance
|
||||||
|
monotonically at the sampling rate** — 39, 45, 56, 62, 75, 80, 85, 92, 102, 108,
|
||||||
|
113, 119, 131, 137 s — ending at 137 s, which is `ADV.wmv`'s full length, after
|
||||||
|
which the title returned. That is not a similarity score, it is a playhead.
|
||||||
|
|
||||||
|
**The control was run first and behaves the same way.** Frames captured during
|
||||||
|
the *boot* movie — known to be `ADV.wmv` — matched `ADV` at 116, 121, 129, 134 s.
|
||||||
|
Its failure mode is worth stating: on near-black frames the correlation collapses
|
||||||
|
(one control frame scored 0.000, another tied `S13A` 0.984 against `ADV` 0.926).
|
||||||
|
Those are exactly the 4 attract frames that did not match. **A dark frame carries
|
||||||
|
no signature; it is not evidence for the runner-up.**
|
||||||
|
|
||||||
|
### 🔴 This corrects an earlier claim of mine
|
||||||
|
|
||||||
|
Two iterations ago I recorded the attract movie as "≈85 s, so probably not
|
||||||
|
`ADV.wmv`'s 137 s". That was wrong, and the mistake was arithmetic on an
|
||||||
|
unobserved start: my sampling began when the movie was already **39 s in**, so
|
||||||
|
what I timed was the *tail* of it, not the whole thing.
|
||||||
|
|
||||||
|
## ✅ The new-game intro is `S00A.wmv`
|
||||||
|
|
||||||
|
Slot `MS00A` — the prologue story intro, 93.87 s, with its own subtitle table,
|
||||||
|
`VOICE_S00A`, and a text overlay. Decoded from the manifest, **not** measured:
|
||||||
|
Ⓐ on `NEW GAME` hangs the emulator
|
||||||
|
([`menu-navigation-semantics.md`](menu-navigation-semantics.md)), so this
|
||||||
|
binding has not been watched happening.
|
||||||
|
|
||||||
|
## 🟡 Skippable — the corpus contradicts itself, and I did not settle it
|
||||||
|
|
||||||
|
* [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) states
|
||||||
|
plainly that "Ⓐ during a movie skips the movie, every time".
|
||||||
|
* But `boot_menu.sh` / `skip_intro.sh` as they stand **deliberately do not tap
|
||||||
|
during a movie**, logging `movie … -> waiting it out (tapping breaks the
|
||||||
|
title)`, and the same document records run G: 88 Ⓐ presses through the boot
|
||||||
|
left a permanent black screen.
|
||||||
|
|
||||||
|
Both cannot be the whole story. **The test**: boot, tap Ⓐ exactly once well
|
||||||
|
inside the movie, and record whether the title arrives early. One boot, and it
|
||||||
|
was not run this iteration.
|
||||||
|
|
||||||
|
## ✅ What ends the attract movie
|
||||||
|
|
||||||
|
Nothing intervenes: it **plays to its end**. The last matched frame is `ADV` at
|
||||||
|
137 s — the file's full duration — and the title is back on the next sample. So
|
||||||
|
the attract cycle is: title, idle ~8–10 s, fade to black, `ADV.wmv` in full, back
|
||||||
|
to the title (which redraws `PRESS Ⓐ BUTTON`).
|
||||||
|
|
||||||
|
## What this contributes to Q6
|
||||||
|
|
||||||
|
The manifest is **data the game reads** to sequence the boot side: the slot keys
|
||||||
|
are in play order and the key *is* the role. That is not the whole driver — it
|
||||||
|
says what plays, not what decides to advance — but it is the first file-side
|
||||||
|
piece of Q6's second half, and it means the boot-side asset order does not have
|
||||||
|
to be authored.
|
||||||
|
|
||||||
|
## ✅ Skippable — settled 2026-08-28: **one Ⓐ skips the movie**
|
||||||
|
|
||||||
|
The test named above was run, and the answer is unambiguous.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| baseline: title arrives with **no** input | ~193 s, ~196 s, ~193 s (three boots) |
|
||||||
|
| **one Ⓐ tapped at ~45 s into the boot** | **title at ~57 s** |
|
||||||
|
|
||||||
|
The press is provably the cause and provably singular: Canary's own
|
||||||
|
`[RE-INPUT] XamInputGetKeystrokeEx reached the driver` counter went **3 → 4**
|
||||||
|
across the tap, so exactly one keystroke was delivered, and the title arrived
|
||||||
|
~12 s later instead of ~150 s later.
|
||||||
|
|
||||||
|
**And the skipped-to title is fully functional**, which is the part that matters
|
||||||
|
for the harness folklore: it draws the `PRESS Ⓐ BUTTON` plate (2 408 plate pixels
|
||||||
|
by the same probe used elsewhere), and a second Ⓐ opened the main menu normally
|
||||||
|
([`title-after-single-A-skip.png`](captures/newgame-path/title-after-single-A-skip.png)).
|
||||||
|
|
||||||
|
### So what is `skip_intro.sh` protecting against?
|
||||||
|
|
||||||
|
Not a single tap. The failure recorded in
|
||||||
|
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) is run G —
|
||||||
|
**88 presses at 4 s intervals through the whole boot** — which left a permanent
|
||||||
|
black screen. Hammering breaks it; one press does exactly what the button is for.
|
||||||
|
The scripts' `waiting it out (tapping breaks the title)` comment is **too broad**,
|
||||||
|
and it costs every scripted boot ~2.5 minutes.
|
||||||
|
|
||||||
|
## ✅ `S00A.wmv` watched — measured 2026-08-28
|
||||||
|
|
||||||
|
The manifest's `MS00A → S00A.wmv` has now been confirmed against the running
|
||||||
|
game. The first attempt, with 5-second screenshots, gave an ambiguous partial
|
||||||
|
playhead; re-run with **`ffmpeg x11grab` at 10 fps** and matched every 0.5 s, it
|
||||||
|
is unambiguous:
|
||||||
|
|
||||||
|
| capture t | `S00A` score @ playhead | `ADV` (runner-up) |
|
||||||
|
|---|---|---|
|
||||||
|
| 5.0 s | **0.998** @ 1.0 s | 0.787 @ 126.5 s |
|
||||||
|
| 6.0 s | **0.999** @ 1.5 s | 0.820 @ 126.5 s |
|
||||||
|
| 7.5 s | **1.000** @ 3.0 s | 0.836 @ 126.5 s |
|
||||||
|
| 9.0 s | **0.999** @ 4.5 s | 0.823 @ 126.5 s |
|
||||||
|
| 12.0 s | **0.998** @ 7.0 s | 0.985 @ 34.5 s |
|
||||||
|
| 16.0 s | **0.999** @ 10.0 s | 0.886 @ 37.5 s |
|
||||||
|
| 17.5 s | **0.967** @ 11.0 s | 0.460 @ 87.0 s |
|
||||||
|
|
||||||
|
Across the 25 consecutive samples from capture 5.0 s to 17.5 s the `S00A`
|
||||||
|
playhead is **strictly non-decreasing** — 1.0, 1.5, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
|
||||||
|
4.5, 5.0, 5.5, 5.5, 6.0, 6.5, 7.0, 7.0, 7.5, 8.0, 8.5, 9.0, 9.0, 9.5, 10.0, 10.5,
|
||||||
|
11.0 — advancing at essentially real time, with scores at or above 0.96. **`S00A`
|
||||||
|
is the top match on 23 of the 27 frames that carry signal** (contrast > 35;
|
||||||
|
the other 25 of 52 frames are the transition and are correctly ignored).
|
||||||
|
|
||||||
|
So the new-game intro is `S00A.wmv`: **decoded** from the manifest and now
|
||||||
|
**measured** off the game. It begins ~4.5 s after Ⓐ on the save slot.
|
||||||
|
|
||||||
|
### 🟡 An aside worth recording: `ADV.wmv` reuses `S00A` footage
|
||||||
|
|
||||||
|
In the window `S00A` 5.5–10 s, `ADV` also scores 0.97–0.99 — at its *own*
|
||||||
|
monotonically advancing playhead, 33.5–37.5 s. Two different movies both matching
|
||||||
|
strongly with two consistent playheads is not noise; it reads as **the boot movie
|
||||||
|
being a trailer cut from the story cutscenes**, `S00A` among them. That would
|
||||||
|
explain why the sparse first attempt kept flipping between them. Not investigated
|
||||||
|
further.
|
||||||
|
|
||||||
|
### ⚠️ Sparse sampling was the whole problem
|
||||||
|
|
||||||
|
The earlier attempt sampled every 5 s and produced contrast 23–37 frames, a
|
||||||
|
playhead that would not join up, and one high-contrast frame preferring `ADV`. The
|
||||||
|
same question at 0.5 s spacing answers itself at 0.999. **The movie was never
|
||||||
|
ambiguous; the sampling was.**
|
||||||
|
|
||||||
|
### The run was killed before the mission load
|
||||||
|
|
||||||
|
An earlier run of this path disappeared at ~145 s with **no crash line in its own
|
||||||
|
log** — routine `MEM-WATCH rss=1153MB` to the end — so an external kill, not a
|
||||||
|
guest fault, and the second this session. The `DELTASABER` plates of
|
||||||
|
[`ui-title-build-map.md`](ui-title-build-map.md) need a mission load and were not
|
||||||
|
reached.
|
||||||
90
docs/re/ready-room-probe.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# S1 — the Ready Room probe: **no-go**, and not for the reason expected
|
||||||
|
|
||||||
|
**Status:** one iteration, as [MISSION §S1](../port/MISSION.md) specifies. This is
|
||||||
|
the written go/no-go. No Ready Room work has been started.
|
||||||
|
|
||||||
|
## Recommendation: no-go
|
||||||
|
|
||||||
|
Not because it is 3D — **it is not** — and not because the names are
|
||||||
|
unrecoverable. Because the probe found that **`GP_READY_ROOM.pak` does not appear
|
||||||
|
to contain the Ready Room's own menu screen at all**, so the question the mission
|
||||||
|
framed ("only 6 of 1 106 names resolve, is that a week or a quarter?") is aimed at
|
||||||
|
the wrong target. And it is outside the five-screen milestone regardless.
|
||||||
|
|
||||||
|
## What the probe measured
|
||||||
|
|
||||||
|
### ✅ It is 2D. This is not a 3D room with a UI overlay.
|
||||||
|
|
||||||
|
The corpus already holds a framebuffer capture of the running screen,
|
||||||
|
[`captures/ready-room-extra-mode.png`](captures/ready-room-extra-mode.png): a flat
|
||||||
|
menu — a `READY ROOM` wordmark, six labelled items (`TAKE OFF`, `BRIEFINGS`,
|
||||||
|
`HANGAR`, `PILOT LOG`, `OPTIONS`, `MISSION CHANGE`), a focus ring, and an
|
||||||
|
`Ⓐ : OK / Ⓑ : Mission change` footer — in exactly the visual language of the
|
||||||
|
title menus. There is no model viewport. The pak's own entry list carries **zero
|
||||||
|
3D containers** (no `XPR2`, no `XBG7`).
|
||||||
|
|
||||||
|
So the mission's own no-go-by-definition criterion is **not** met. That part is a
|
||||||
|
go.
|
||||||
|
|
||||||
|
### ✅ Enumeration by content works — 60 builds
|
||||||
|
|
||||||
|
`sylpheed-cli screen list` finds **60 screen builds**. The 1 106 entries break
|
||||||
|
down as **902 RATC bundles, 192 IDXD objects, 6 `ttf`, 6 `LSTA`**. The worry that
|
||||||
|
unresolvable *paths* would mean unrenderable *screens* is unfounded here.
|
||||||
|
|
||||||
|
### 🔴 But none of it is the Ready Room menu
|
||||||
|
|
||||||
|
Two independent signals, and they agree.
|
||||||
|
|
||||||
|
**Rendered composites are briefing panels.** Four of the 60 builds — the largest
|
||||||
|
and most element-rich (18, 26, 44 and 9 elements) — composite to a planet
|
||||||
|
information panel, unit markers and direction arrows:
|
||||||
|
[`ready-room-pak-builds.png`](captures/ready-room-probe/ready-room-pak-builds.png).
|
||||||
|
Tactical-map material, not a six-item menu. ⚠️ **4 of 60 sampled** — this alone
|
||||||
|
does not close it.
|
||||||
|
|
||||||
|
**The name census does close it much harder.** The 902 bundles declare **317
|
||||||
|
distinct element names**, all under a `pb*` prefix family:
|
||||||
|
`pbfriendly` (330), `pbacropolis` (168), `pbrhino` (48), `pbsub` (204),
|
||||||
|
`pbtab` (48), `pbcm` (132)… Searching all 317 for any of the six visible labels —
|
||||||
|
`takeoff`, `take_off`, `brief`, `hangar`, `pilot`, `option`, `mission`, `ready` —
|
||||||
|
returns **nothing**. The only `main` hits are `pbmain_btn1..3.t32`,
|
||||||
|
`pbmain_window.t32` and their `_eff` glows, which appear in 9- and 4-element
|
||||||
|
*fragments*, not a screen, and are declared **kind 0** (decoration).
|
||||||
|
|
||||||
|
Read together: this pak is the **briefing / tactical-map** content that sits
|
||||||
|
*behind* the Ready Room's `BRIEFINGS` item, and the Ready Room's own menu lives
|
||||||
|
somewhere else. Note the GamePart table has both `10 GP_BUNK` and
|
||||||
|
`11 GP_READY_ROOM` ([`challenge-mission-gate.md`](challenge-mission-gate.md) §3),
|
||||||
|
and `GP_BUNK.pak` is a separate archive that was not examined here.
|
||||||
|
|
||||||
|
### 🔴 And a caveat that reaches back into the delivered corpus
|
||||||
|
|
||||||
|
**`kind == 0x3002` finds zero buttons in this pak.** All 902 bundles, 0 hits —
|
||||||
|
while `GP_TITLE` has 16. What `GP_READY_ROOM` uses instead:
|
||||||
|
|
||||||
|
| kind | count |
|
||||||
|
|---|---|
|
||||||
|
| `0x3000` | 491 |
|
||||||
|
| `0x3004` | 406 |
|
||||||
|
| `0x300c` | 135 |
|
||||||
|
| `0x3008` | 60 |
|
||||||
|
| `0x3001` | 6 |
|
||||||
|
| `0x3002` | **0** |
|
||||||
|
|
||||||
|
So `0x3002` is one member of a `0x3000` **family** with sub-bits, and "element
|
||||||
|
kind `0x3002` is a button" — a ✅ row in [`HANDOFF.md`](../port/HANDOFF.md) — is
|
||||||
|
sound for the title screens and **must not be applied as an equality test
|
||||||
|
elsewhere**. For the five-screen milestone nothing changes; for anything beyond
|
||||||
|
it, test the family.
|
||||||
|
|
||||||
|
*(The kind field is the 4th `u32` of the 60-byte declaration entry, at `+40`.
|
||||||
|
Calibrated on `GP_TITLE` build 6, where it reads `0x3002` for exactly the three
|
||||||
|
`EXTRAS` buttons and `0x0` for the decorations.)*
|
||||||
|
|
||||||
|
## If it is ever picked up
|
||||||
|
|
||||||
|
The first job is **not** naming this pak's keys. It is finding where the Ready
|
||||||
|
Room's six-button screen actually lives — `GP_BUNK.pak` is the obvious first
|
||||||
|
place to look, and the same content-based `screen list` / name-census pass costs
|
||||||
|
one iteration there.
|
||||||
170
docs/re/screen-transitions.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# What happens between two screens — a fade through black, and where its timing lives
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED`. The quad and its ramp are **decoded** (a keyframe
|
||||||
|
group on the disc, with a disc-wide check); the wall-clock timings are
|
||||||
|
**measured** off the running game at 30 fps. One piece is **undecodable from this
|
||||||
|
field** and is called out below.
|
||||||
|
|
||||||
|
Answers [MISSION Q7](../port/MISSION.md).
|
||||||
|
|
||||||
|
## The mechanism — decoded
|
||||||
|
|
||||||
|
Every title-side screen carries a full-screen untextured primitive that is
|
||||||
|
**black, and paints last**: `pteff00.prm` in `GP_TITLE`, `pfeff00.prm` in
|
||||||
|
`GP_SAVE_LOAD`. That it sorts last was already established
|
||||||
|
([`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md)); what is
|
||||||
|
new here is that its **keyframe group is the transition**.
|
||||||
|
|
||||||
|
The group is always four blocks, and always this shape:
|
||||||
|
|
||||||
|
| block | alpha | meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `0xff` at `t = T0` | the screen starts **black** |
|
||||||
|
| 2 | `0x00` at `t = T1` | ramp to fully clear — the screen **fades in** |
|
||||||
|
| 3 | `0x00` at `t = T2` | clear; this is the resting pose, the quad is invisible |
|
||||||
|
| 4 | `0xff`, **no time** | ramp back to black — the screen **fades out** on exit |
|
||||||
|
|
||||||
|
Read with the corpus's rule that a keyframe is the *start* of a ramp
|
||||||
|
([`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)).
|
||||||
|
|
||||||
|
```
|
||||||
|
$ tools/re-capture/fade_quads.py 4 5 6 # GP_TITLE
|
||||||
|
build 4 (title) pteff00.prm t=16 α=255 t=261 α=0 t=269 α=0 t=— α=255
|
||||||
|
build 5 (main menu) pteff00.prm t=12 α=255 t= 70 α=0 t= 80 α=0 t=— α=255
|
||||||
|
build 6 (EXTRAS) pteff00.prm t=12 α=255 t= 64 α=0 t= 74 α=0 t=— α=255
|
||||||
|
```
|
||||||
|
|
||||||
|
Under [Q1](ui-keyframe-time-unit.md)'s `1 unit = 1/60 s`: the screen holds black
|
||||||
|
for **0.20 s**, then fades in over **0.87 s** (`EXTRAS`), **0.97 s** (main menu)
|
||||||
|
or **4.08 s** (the title).
|
||||||
|
|
||||||
|
### ❔ The fade-OUT duration is not in this field
|
||||||
|
|
||||||
|
The fourth block has **no time** — a group's last block stops 4 bytes short and
|
||||||
|
that word is already the next group's element index
|
||||||
|
([`ui_layout.rs`](../../crates/sylpheed-formats/src/ui_layout.rs) documents the
|
||||||
|
packing). So the disc gives the ramp's *target* (black) and not its length. That
|
||||||
|
duration is **measured** below, and the port is authoring it.
|
||||||
|
|
||||||
|
### The disc-wide check, and what it shows about overlays
|
||||||
|
|
||||||
|
Over every `GP_*.pak`, counting bundles that have a `.prm` element **and** ≥8
|
||||||
|
elements (screen-sized rather than a two-element fragment):
|
||||||
|
|
||||||
|
> **40 of 97** carry a 255 → 0 → 255 quad; 56 of the 60 found disc-wide have
|
||||||
|
> exactly 4 keyframes.
|
||||||
|
|
||||||
|
41 % sounds weak until it is read per pak, where it is nearly all-or-nothing:
|
||||||
|
|
||||||
|
| pak | with quad / screen-sized |
|
||||||
|
|---|---|
|
||||||
|
| `GP_MISSION_SELECT`, `GP_MOVIE_THEATER`, `GP_OPTIONS`, `GP_TUTORIAL`, `GP_SYSTEM` | 2/2 each |
|
||||||
|
| `GP_BUNK`, `GP_CHALLENGE` | 6/6 |
|
||||||
|
| `GP_STAGE_CLEAR` | 4/4 |
|
||||||
|
| `GP_SAVE_LOAD` | 10/12 |
|
||||||
|
| **`GP_TITLE`** | **6/12** |
|
||||||
|
| `GP_DIALOG` | 0/133 |
|
||||||
|
| `GP_DEBRIEFING_PILOTLOG` | 4/102 |
|
||||||
|
|
||||||
|
**`GP_TITLE`'s 6 of 12 is the interesting row, and it is not a gap.** The six
|
||||||
|
that carry the quad are exactly the six *screen* builds — title, main menu and
|
||||||
|
`EXTRAS`, English and Japanese. The six that do not are exactly the six
|
||||||
|
**overlays**: the `PRESS Ⓐ BUTTON` plate and the two `DELTASABER` plates
|
||||||
|
([`ui-title-build-map.md`](ui-title-build-map.md)). An overlay composited onto a
|
||||||
|
screen has no transition of its own, so it has no fade quad — which is
|
||||||
|
independent corroboration that those builds are overlays rather than screens.
|
||||||
|
`GP_DIALOG`'s 0/133 says the same thing about dialog boxes.
|
||||||
|
|
||||||
|
## The timing — measured
|
||||||
|
|
||||||
|
Recorded with `ffmpeg -f x11grab -framerate 30` over the game surface, mean
|
||||||
|
frame luminance per frame; raw data in
|
||||||
|
[`captures/transitions/transition-luminance.csv`](captures/transitions/transition-luminance.csv),
|
||||||
|
filmstrip in
|
||||||
|
[`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png).
|
||||||
|
|
||||||
|
| | main menu → `EXTRAS` (Ⓐ) | `EXTRAS` → main menu (Ⓑ) |
|
||||||
|
|---|---|---|
|
||||||
|
| press → first visible change | 0.07 s | 0.37 s |
|
||||||
|
| **fade-out to black** | **0.367 s** | **0.400 s** |
|
||||||
|
| **pure black** (luminance 0.02) | 0.233 s | 0.167 s |
|
||||||
|
| luminance rise until settled | 0.567 s | 1.467 s |
|
||||||
|
|
||||||
|
The fade-out is the number the file cannot give, and it comes out the same both
|
||||||
|
ways: **~0.4 s**, i.e. ~24 units under Q1's rule.
|
||||||
|
|
||||||
|
The black hold is *consistent with* the file's 12 units (0.20 s) but does not
|
||||||
|
confirm it — the plateau spans the tail of the outgoing screen's fade-out and the
|
||||||
|
head of the incoming screen's black, and this measurement cannot separate them.
|
||||||
|
|
||||||
|
### ⚠️ The luminance rise is **not** the quad's ramp
|
||||||
|
|
||||||
|
The two columns differ by 2.6× where the quad's declared ramps differ by only
|
||||||
|
1.12× (58 units vs 52). The filmstrip says why: the background reappears *first*
|
||||||
|
and the labels arrive after it, so what the luminance curve is timing is the
|
||||||
|
incoming screen's **own element animations**, not the fade quad. Quoting 1.47 s
|
||||||
|
as "the main menu's fade" would be wrong. The quad's ramp is decoded; the
|
||||||
|
screen's build-in is a separate, longer thing.
|
||||||
|
|
||||||
|
## For the port
|
||||||
|
|
||||||
|
* a screen change is: **fade the outgoing screen to black over ~0.4 s**, hold
|
||||||
|
black briefly, then **fade the incoming screen in over its own declared ramp**
|
||||||
|
while its elements play their own keyframes;
|
||||||
|
* the fade-in ramp is **read from the file** (`T1 − T0` on the screen's fade
|
||||||
|
quad);
|
||||||
|
* the ~0.4 s fade-out and the black hold are **authored from this page** — the
|
||||||
|
disc does not carry them.
|
||||||
|
|
||||||
|
## ✅ Which quantity the ~0.4 s is — measured 2026-08-29
|
||||||
|
|
||||||
|
Asked by the port: is 0.4 s **(a)** the ramp from the hold to the exit pose, i.e.
|
||||||
|
exactly the missing duration of that untimed keyframe, **(b)** several keyframes'
|
||||||
|
worth, or **(c)** something the game does independently of the group?
|
||||||
|
|
||||||
|
**It is (a)** — and it is bigger than the fade quad. Two facts.
|
||||||
|
|
||||||
|
**1. There is exactly one untimed keyframe, and every element has it.** Reading
|
||||||
|
`screen info --build 5 --geometry` for the main menu, all 16 elements end on a
|
||||||
|
single timeless block; none has two. So there is one unknown duration per screen,
|
||||||
|
not a chain of them — which rules out (b) outright. And that final block is not
|
||||||
|
idle: it is where the screen *plays out*.
|
||||||
|
|
||||||
|
| elements | final untimed block | what it does |
|
||||||
|
|---|---|---|
|
||||||
|
| `pteff00.prm` (the fade quad) | `a = 255` | goes **black** |
|
||||||
|
| `pteff10`, `pteff12`, `ptbtn01…05`, `ptmsg` | `a = 0` | **fade out** |
|
||||||
|
| `ptframe1`, `ptframe2` | `a = 255` | hold, and get covered |
|
||||||
|
| `ptbase`, `pteff05`, `ptloop*`, `pteff02.prm` | single keyframe | hold |
|
||||||
|
|
||||||
|
**2. The capture shows the content fading, not just a black quad arriving.** This
|
||||||
|
has a null hypothesis that discriminates: under (c) — the game blackens the frame
|
||||||
|
independently — every region is scaled by the same `1 − α`, so the **ratio**
|
||||||
|
between a button region and a background region is *constant* through the
|
||||||
|
fade-out. Under (a) it must fall, because the buttons ramp to `a = 0` while the
|
||||||
|
background elements hold at 255 and are only dimmed.
|
||||||
|
|
||||||
|
Measured on [`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png),
|
||||||
|
button column ÷ upper-right background art, frame by frame through the fade-out:
|
||||||
|
|
||||||
|
```
|
||||||
|
frame 0 1 2 3 4 5
|
||||||
|
ratio 6.495 5.574 3.105 2.125 1.935 (black)
|
||||||
|
```
|
||||||
|
|
||||||
|
**A 3.4× monotonic fall.** Constant is refuted. The buttons really are fading
|
||||||
|
independently of the overall dim, exactly as their declared final block says.
|
||||||
|
(The incoming screen runs it in reverse, 2.22 → 3.47 over frames 7–12.)
|
||||||
|
|
||||||
|
⚠️ **Reach.** The filmstrip is downsampled and the "button" region unavoidably
|
||||||
|
contains some background, so the ratio is a direction, not a clean alpha
|
||||||
|
measurement. It refutes the constant-ratio null decisively; it does not by itself
|
||||||
|
pin the 0.4 s to ±0.05 s. And it is measured on **one** transition pair.
|
||||||
|
|
||||||
|
### For the port
|
||||||
|
|
||||||
|
Write **one** authored constant — the duration of the final untimed keyframe,
|
||||||
|
~0.4 s / ~24 units — and **play the group to its end on every element**. Do not
|
||||||
|
model the exit as a black rectangle fading over a frozen screen: the buttons and
|
||||||
|
labels ramp to transparent at the same time, and that difference is visible.
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ the encoding: **1 = not yet deployed, 2 = active, 4 = destroyed**.
|
|||||||
### ✅ The mission-over branch, observed exactly as disassembled
|
### ✅ The mission-over branch, observed exactly as disassembled
|
||||||
|
|
||||||
The phase ended at 694.9 s, but **the ordinal did not advance** — and the reason
|
The phase ended at 694.9 s, but **the ordinal did not advance** — and the reason
|
||||||
is the branch [mission-phase-advance](../mission-phase-advance.md) read out of
|
is the branch [mission-phase-advance](mission-phase-advance.md) read out of
|
||||||
`sub_82260710`:
|
`sub_82260710`:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
153
docs/re/structures/bgm-two-stems.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# A music bank is two stems of one performance, played together
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED` for the structure — **decoded**, with a disc-wide check
|
||||||
|
over all 32 BGM banks. 🟡 for the *role* of the second wave — **measured** by
|
||||||
|
signal analysis, and the two readings that survive are named below. ❔ which bank
|
||||||
|
is the menu's music is **undecodable from the disc**, with the reach stated.
|
||||||
|
|
||||||
|
Answers [MISSION Q10](../../port/MISSION.md). It also **withdraws the premise**: the
|
||||||
|
handoff said `BGM_001.slb` is *three* sub-waves of 10 KB / 4.47 MB / 4.67 MB. The
|
||||||
|
10 KB is the bank **header**, not a wave.
|
||||||
|
|
||||||
|
## The structure — decoded
|
||||||
|
|
||||||
|
```
|
||||||
|
BGM_001.slb (9 178 040 B)
|
||||||
|
0 BANK id=1001 data_size=9175992 hdr = 5 blocks (10240 B) 16bit/2ch
|
||||||
|
10240 RIFF data@14336 size=4466688 (2181 packets) 25697 B/s → 173.82 s
|
||||||
|
4481024 seek 2181 packets
|
||||||
|
4491264 RIFF data@4495360 size=4673536 (2282 packets) 26887 B/s → 173.82 s
|
||||||
|
9168896 seek 2282 packets
|
||||||
|
```
|
||||||
|
|
||||||
|
A bank is a 10 240-byte header and then **exactly two waves**, and the two always
|
||||||
|
have the **same duration** — different byte sizes and different bitrates, same
|
||||||
|
number of seconds. Duration is `data_size / PsuedoBytesPerSec` (the u32 at
|
||||||
|
`RIFF+0x20`; `RIFF+0x24` is the sample rate, 48 000 Hz except `BGM_020`–`023`
|
||||||
|
at 44 100).
|
||||||
|
|
||||||
|
### Disc-wide
|
||||||
|
|
||||||
|
`tools/re-capture/bgm_wave_census.py`, full output in
|
||||||
|
[`../data/bgm-wave-census.txt`](../data/bgm-wave-census.txt):
|
||||||
|
|
||||||
|
> **28 of the 32 BGM entries** read straight off their own pak entry as two waves
|
||||||
|
> of equal duration — equal to 0.01 s, over lengths from 37 s to 277 s.
|
||||||
|
|
||||||
|
The other four — `BGM_106`…`BGM_109` — are **not counterexamples**, they are the
|
||||||
|
known leading-region straddle ([`slb-data-offset.md`](slb-data-offset.md)): those
|
||||||
|
entry windows start mid-bank, so an entry contains the tail of the previous bank,
|
||||||
|
one whole wave of its own, and the `BANK` header of the *next* one.
|
||||||
|
`slb_segment_phase.py bank` shows it directly — the entry named `BGM_107.slb`
|
||||||
|
contains `BANK id=1108`. Realigning across the boundaries with the `seek` packet
|
||||||
|
counts, which pin each join exactly (bank 1107: a `seek` for 1164 packets and a
|
||||||
|
2 383 872-byte wave = 1164 packets), restores the same shape:
|
||||||
|
|
||||||
|
| bank | wave 0 | wave 1 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1107 | 2 383 872 B → 65.18 s | 2 418 688 B → 65.18 s |
|
||||||
|
| 1108 | 3 696 640 B → 128.91 s | 3 696 640 B → 128.91 s |
|
||||||
|
| 1109 | 2 887 680 B → 81.50 s | 2 932 736 B → 81.50 s |
|
||||||
|
|
||||||
|
Bank 1106's wave 0 begins in unclaimed bytes before its entry and is known only
|
||||||
|
by its `seek` count (1 718 packets), so it is 🟡 rather than measured.
|
||||||
|
|
||||||
|
**So: intro + loop is dead, and so is two halves of one piece.** Both would give
|
||||||
|
unequal durations; 32 banks give equal ones.
|
||||||
|
|
||||||
|
## The role of the second wave — measured
|
||||||
|
|
||||||
|
Decoded `BGM_001`'s two waves to PCM and compared them.
|
||||||
|
|
||||||
|
| | wave 0 | wave 1 |
|
||||||
|
|---|---|---|
|
||||||
|
| duration | 173.809 s | 173.809 s |
|
||||||
|
| first sound / last sound | 0.001 s / **167.663 s** | 0.003 s / **167.663 s** |
|
||||||
|
| RMS | 6 824 | 3 961 |
|
||||||
|
| L↔R correlation | 0.61 | **0.14** |
|
||||||
|
| energy below 200 Hz | 16.5 % | **6.7 %** |
|
||||||
|
| energy 1–4 kHz | 31.1 % | 42.5 % |
|
||||||
|
|
||||||
|
**They are sample-synchronous.** Transient-envelope cross-correlation searched
|
||||||
|
over ±5 s peaks at **lag +0.00 s** (0.5624), and both waves stop at the *same
|
||||||
|
millisecond*, 167.663 s. Two takes, two halves or two alternates would not do
|
||||||
|
that. They are two stems of one performance and are meant to sound **at the same
|
||||||
|
time**.
|
||||||
|
|
||||||
|
**Therefore concatenating them is wrong**, and not subtly: it plays the piece
|
||||||
|
twice, the second time as a bass-less secondary stem.
|
||||||
|
|
||||||
|
### 🟡 Which kind of second stem — two readings survive
|
||||||
|
|
||||||
|
* **The rear pair of a 4-channel mix.** Fits every number: quieter, far more
|
||||||
|
decorrelated between L and R, and with the bass managed away to the front pair.
|
||||||
|
* **A second intensity layer** to be mixed in or crossfaded.
|
||||||
|
|
||||||
|
**Not settled, and the obvious field does not settle it.** `ChannelMask` is
|
||||||
|
`0x0002` on *both* waves, and [`sound-slb.md`](sound-slb.md) already records that
|
||||||
|
this game writes meaningless channel metadata (movie voices declare 2 channels
|
||||||
|
over mono content). What would settle it is a runtime observation — whether the
|
||||||
|
game submits both waves to the mixer at once — which needs an emulator with audio
|
||||||
|
this container does not have.
|
||||||
|
|
||||||
|
## ❔ Which bank is the menu's music — undecodable, and here is the reach
|
||||||
|
|
||||||
|
The cue table binds cue names to sound ids and banks
|
||||||
|
([`sound-cue-table.md`](sound-cue-table.md)), and its **32 BGM cues are named
|
||||||
|
`BGM_001` … `BGM_109`** — pure numbers, no `TITLE`, no `MENU`, no semantic name
|
||||||
|
anywhere. Looked in: the `SOUNDS` record (5 798 cues), `FILES` (5 135 bank
|
||||||
|
paths), and the bank headers themselves (a `BANK id` that simply repeats the cue
|
||||||
|
number, `id=1001` for `BGM_001`). Nothing names a screen.
|
||||||
|
|
||||||
|
So the menu↔bank binding has to come from the running game — the same route Q8's
|
||||||
|
event bindings need — and until then the port is **choosing** a track, not
|
||||||
|
transcribing one.
|
||||||
|
|
||||||
|
## What the port needs to know
|
||||||
|
|
||||||
|
* a music bank is **two waves that play together**, not one track and not a
|
||||||
|
sequence — do not concatenate;
|
||||||
|
* both are full length; align them at sample 0;
|
||||||
|
* **the track is not a seamless loop.** `BGM_001` fades out and is followed by
|
||||||
|
**6.15 s of silence** (last sound 167.663 s of 173.809 s; the final second
|
||||||
|
before silence is at RMS 168 against 4 788 at the head). Looping the wave as
|
||||||
|
stored gives a fade-out and six seconds of nothing every cycle. No loop-point
|
||||||
|
field has been identified in the XMA header, so a menu loop is **authored**.
|
||||||
|
|
||||||
|
## ✅ Confirmed at runtime — two stereo streams decode at once
|
||||||
|
|
||||||
|
The page above said the "played together" reading needed "a runtime observation
|
||||||
|
of whether the game submits both waves to the mixer at once", and that this
|
||||||
|
container could not do it. It can: `--xma_param_probe=true` logs every XMA stream
|
||||||
|
the decoder is handed.
|
||||||
|
|
||||||
|
Sitting on the **main menu**, two stereo 48 kHz streams were decoding
|
||||||
|
simultaneously — **1 893 packets / 3 876 864 B** and **1 919 packets /
|
||||||
|
3 930 112 B**. Two stereo streams alive at the same moment on a screen with one
|
||||||
|
piece of music playing is the direct observation this page was missing: **a music
|
||||||
|
bank's two waves are simultaneous, not sequential.** Concatenating them is wrong,
|
||||||
|
now measured as well as inferred.
|
||||||
|
|
||||||
|
🟡 It still does **not** separate the two readings — surround-rear pair versus a
|
||||||
|
second intensity layer — because both predict simultaneity.
|
||||||
|
|
||||||
|
## ✅ The menu's music is `BGM_103` — three independent routes agree
|
||||||
|
|
||||||
|
An earlier version of this section said the two observed byte sizes "do not match
|
||||||
|
any bank's declared wave sizes". **That was wrong** — I had checked only the
|
||||||
|
`BGM_0xx` rows. They match `BGM_103` exactly, and the code names it:
|
||||||
|
|
||||||
|
| route | evidence |
|
||||||
|
|---|---|
|
||||||
|
| **static, code** | `GamePart_Title`'s phase handler `sub_821C5580` does `li r5, 1103` into a sound call (`sub_8217ACF8`, with `r4 = 4`) — **cue 1103 = `BGM_103`** |
|
||||||
|
| **static, disc** | [`../data/bgm-wave-census.txt`](../data/bgm-wave-census.txt): `BGM_103.slb` = two waves of **3 876 864** and **3 930 112** bytes, 87.75 s each |
|
||||||
|
| **runtime** | the XMA probe at the **main menu** saw two stereo 48 kHz streams decoding simultaneously, of **3 876 864** and **3 930 112** bytes |
|
||||||
|
|
||||||
|
Byte-for-byte on both waves. So:
|
||||||
|
|
||||||
|
* **which bank the menu plays is decoded** — `BGM_103`, from the executable, since
|
||||||
|
the cue *table*'s BGM entries are numeric and name no screen;
|
||||||
|
* and the two-stems-play-together finding is now confirmed **on a named bank**,
|
||||||
|
with the runtime stream sizes equal to the bank's declared wave sizes — so the
|
||||||
|
game hands the decoder the whole wave, not a window, which is the opposite of
|
||||||
|
what the earlier note guessed.
|
||||||
@@ -82,7 +82,7 @@ names had no text to resolve *to*, and the 100 % figure was unreachable.
|
|||||||
|
|
||||||
* **Which recording plays.** The caption is the *text*; the voice bank binding
|
* **Which recording plays.** The caption is the *text*; the voice bank binding
|
||||||
is a separate and still-unresolved question — see
|
is a separate and still-unresolved question — see
|
||||||
[voice-bank-leading-region.md](voice-bank-leading-region.md) and the
|
[voice-bank-leading-region.md](../voice-bank-leading-region.md) and the
|
||||||
known case of a generic line playing against a specific subtitle.
|
known case of a generic line playing against a specific subtitle.
|
||||||
* **Which page a `MSG_DEMO_*` id belongs to.** The `DEMO` family is not called
|
* **Which page a `MSG_DEMO_*` id belongs to.** The `DEMO` family is not called
|
||||||
from the stage scripts at all — 78 of its ids are multi-page, so something
|
from the stage scripts at all — 78 of its ids are multi-page, so something
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ result of the two.
|
|||||||
## ✅ 2026-08-23 (third pass) — the counter is at `0xbdb59668` again, and the refutation above is *refined*, not reversed
|
## ✅ 2026-08-23 (third pass) — the counter is at `0xbdb59668` again, and the refutation above is *refined*, not reversed
|
||||||
|
|
||||||
Run 4, with the hunt automated end to end
|
Run 4, with the hunt automated end to end
|
||||||
([`ob_hunt.py`](../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
|
([`ob_hunt.py`](../../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
|
||||||
**exactly one** surviving address:
|
**exactly one** surviving address:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -322,7 +322,7 @@ With the per-entity searches refuted at both word and bit level
|
|||||||
counter. It sits at `0xbdb59668`, inside the entity-heap window, so the answer is
|
counter. It sits at `0xbdb59668`, inside the entity-heap window, so the answer is
|
||||||
readable directly: sample ±0x200 around it across one transition and keep the
|
readable directly: sample ±0x200 around it across one transition and keep the
|
||||||
words that move **with** it
|
words that move **with** it
|
||||||
([`ob_neighbourhood.py`](../../tools/re-capture/ob_neighbourhood.py),
|
([`ob_neighbourhood.py`](../../../tools/re-capture/ob_neighbourhood.py),
|
||||||
[`captures/ob-counter-neighbourhood-stage02.json`](../captures/ob-counter-neighbourhood-stage02.json)).
|
[`captures/ob-counter-neighbourhood-stage02.json`](../captures/ob-counter-neighbourhood-stage02.json)).
|
||||||
|
|
||||||
Control first: over an 8-second interval while the counter sat still, **0 of the
|
Control first: over an 8-second interval while the counter sat still, **0 of the
|
||||||
|
|||||||
47
docs/re/structures/movie-audio-channels.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# ✅ The disc ships movies in **two** audio profiles — and one of them is 5.1
|
||||||
|
|
||||||
|
**Status:** ✅ `DECODED`, disc-wide (97 of 97 movies probed). Found while checking
|
||||||
|
a transcode, not while looking for it.
|
||||||
|
|
||||||
|
## The split
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ffprobe -select_streams a -show_entries stream=codec_name,sample_rate,channels,channel_layout
|
||||||
|
```
|
||||||
|
|
||||||
|
| profile | count | codec | rate | channels | which |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **surround** | **28** | `wmapro` | 48 kHz | **6 (5.1)** | `ADV.wmv` + every `S*.wmv` story cutscene |
|
||||||
|
| stereo | 69 | `wmav2` | 48 kHz | 2 | every `RT*.wmv` and `hokyu_*.wmv` |
|
||||||
|
|
||||||
|
Full list in [`data/movie-audio-layouts.csv`](../data/movie-audio-layouts.csv).
|
||||||
|
|
||||||
|
The split is not arbitrary: the **cinematics** are 5.1 and the **in-mission radio
|
||||||
|
chatter** is stereo. `ADV.wmv` (137.714 s) and `S00A.wmv` — the boot/attract intro
|
||||||
|
and the new-game intro, i.e. **both movies the menu milestone needs** — are in the
|
||||||
|
surround group.
|
||||||
|
|
||||||
|
## ⚠️ Why this matters to anything that transcodes
|
||||||
|
|
||||||
|
A single `ffmpeg` command over `dat/movie/` produces **two different kinds of
|
||||||
|
result** and records neither:
|
||||||
|
|
||||||
|
* the 69 stereo files pass through channel-wise unchanged;
|
||||||
|
* the 28 surround files are **downmixed 5.1 → stereo by ffmpeg's default matrix**,
|
||||||
|
folding centre-channel dialogue into L/R at a weighting nobody chose.
|
||||||
|
|
||||||
|
That default is a *content* decision — it sets how loud the dialogue is against
|
||||||
|
the music — and it is **not stable across ffmpeg versions**. A pipeline that wants
|
||||||
|
a reproducible result has to state the downmix explicitly rather than inherit it,
|
||||||
|
and record it beside the command.
|
||||||
|
|
||||||
|
🟡 Whether the game itself plays these in surround, and with what downmix when the
|
||||||
|
console is set to stereo, is **not established here**. This page is about what is
|
||||||
|
on the disc.
|
||||||
|
|
||||||
|
## How it was found, and the trap next to it
|
||||||
|
|
||||||
|
Checking a transcode against its source, `ffprobe` reported the output as 33 s
|
||||||
|
against the source's 137 s — which reads as catastrophic truncation and is not:
|
||||||
|
the file was **being written at that moment**. Measuring a live write produces a
|
||||||
|
confident wrong answer with no error. See [`../METHOD.md`](../METHOD.md).
|
||||||
141
docs/re/structures/ratc-child-names.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# A RATC child's name is stated by an `opt ` block, not by the bytes before it
|
||||||
|
|
||||||
|
**Status:** ✅ `DECODED`, with a disc-wide check. This fixes a decoder defect that
|
||||||
|
silently dropped the **full-resolution background from all five menu screens**.
|
||||||
|
|
||||||
|
## The field
|
||||||
|
|
||||||
|
Immediately before nearly every child of a `RATC` bundle — 17 942 of the disc's
|
||||||
|
18 002 — sits an `opt ` block:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||||||
|
```
|
||||||
|
|
||||||
|
Two children of the main menu bundle (`GP_TITLE.pak` entry 8), raw:
|
||||||
|
|
||||||
|
```text
|
||||||
|
opt 00 00 00 0a p t b a s e . t 3 2 00 0e 10 a4 T8aD
|
||||||
|
opt 00 00 00 0b p t e f f 0 5 . t 3 2 00 38 41 58 T8aD
|
||||||
|
^^^^^^^^
|
||||||
|
"8AX"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the **same `opt ` block** `ui_layout::opt_link` already decodes for a
|
||||||
|
button's focus link. Nothing new had to be discovered to read it — only noticed.
|
||||||
|
|
||||||
|
## The defect it fixes
|
||||||
|
|
||||||
|
`ratc::parse` named each child with `name_before`: the last printable run in the
|
||||||
|
96 bytes before the child's magic. That is a heuristic, and it is *usually* right
|
||||||
|
— `ptbase.t32`'s three trailing bytes are `0e 10 a4`, not printable, so the scan
|
||||||
|
walks back to the real name.
|
||||||
|
|
||||||
|
But `pteff05.t32`'s trailing three bytes are `38 41 58`, which is `"8AX"` in
|
||||||
|
ASCII. The scan takes those, the child is registered under the name `8AX`, the
|
||||||
|
element that declares `pteff05.t32` matches nothing in the sprite table, and
|
||||||
|
`compose` drops it through
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let Some(sprite) = el.sprite.as_ref() else { continue };
|
||||||
|
```
|
||||||
|
|
||||||
|
— *before* the arm that records a `missing` sprite. So the screen's background
|
||||||
|
vanished with **no diagnostic at all**: `screen render` reported "all resolved".
|
||||||
|
|
||||||
|
⚠️ `8AX` was carried in `docs/` as though it were a name the game uses — the old
|
||||||
|
text read "the `T8aD` behind its `opt ` link is registered under the name `8AX`".
|
||||||
|
It is not a name. It is three bytes of the preceding record's payload.
|
||||||
|
|
||||||
|
## The disc-wide check
|
||||||
|
|
||||||
|
[`examples/ratc_child_names.rs`](../../../crates/sylpheed-formats/examples/ratc_child_names.rs)
|
||||||
|
compares the two readings for every RATC child in all 33 `dat/*.pak`
|
||||||
|
([data](../data/ratc-child-name-audit.txt)):
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| RATC children scanned | **18 002** |
|
||||||
|
| carrying an `opt ` block | 17 942 |
|
||||||
|
| scanned name **agrees** with it | **17 918** |
|
||||||
|
| scanned name **differs** | **24** |
|
||||||
|
|
||||||
|
Every one of the 24 is the same failure: a 3-byte printable tail beating a real
|
||||||
|
name.
|
||||||
|
|
||||||
|
| scanned | actual | count | where |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `8AX` | `pbbg.t32` | 12 | `GP_READY_ROOM` |
|
||||||
|
| `8AX` | `pteff05.t32` | 4 | `GP_TITLE` 5, 6, 8, 9 — the menus |
|
||||||
|
| `8AX` | `pmbase.t32` | 4 | `GP_STAGE_CLEAR` |
|
||||||
|
| `8AX` | `pteff04.t32` | 2 | `GP_TITLE` 4, 7 — the title |
|
||||||
|
| `'OX` | `po_keys_win1.t32` | 2 | `GP_OPTIONS` |
|
||||||
|
|
||||||
|
⚠️ **What I checked about the 24, precisely.** That the recovered name is the one
|
||||||
|
the bundle actually wants is verified for the ten `GP_TITLE` and `GP_STAGE_CLEAR`
|
||||||
|
cases: on the title screens `pteff04.t32`/`pteff05.t32` are *declared elements*
|
||||||
|
that previously resolved to nothing and now resolve, and `pmbase.t32` is the
|
||||||
|
target of `GP_STAGE_CLEAR`'s long-standing dangling reference (below). For the 12
|
||||||
|
`GP_READY_ROOM` (`pbbg.t32`) and 2 `GP_OPTIONS` (`po_keys_win1.t32`) cases I
|
||||||
|
checked only that no element in those bundles is left unresolved afterwards —
|
||||||
|
which is consistent with, not proof of, the same story. None of the 14 is on the
|
||||||
|
five menu screens.
|
||||||
|
|
||||||
|
**The control is the 17 918 the heuristic already got right**: the `opt ` reading
|
||||||
|
reproduces every one of them. A reading that fixed the 24 but disturbed the rest
|
||||||
|
would be a different rule, not this one.
|
||||||
|
|
||||||
|
✅ **Reach — closed 2026-08-29.** 60 children (0.3 %) have **no** `opt ` block
|
||||||
|
within 128 bytes and fall back to the scan. **They are not children.** They are
|
||||||
|
the ten frames of the disc's only `.tan` resource, `pb_f15_eg_anm.tan`, in the
|
||||||
|
six language copies of one `GP_READY_ROOM` bundle — 6 × 10 = 60, the whole
|
||||||
|
population with nothing left over. One `opt ` block names the whole run, which is
|
||||||
|
why nine of the ten find no block of their own. None is on the five menu screens.
|
||||||
|
[`ratc-tan-frame-sequence.md`](ratc-tan-frame-sequence.md)
|
||||||
|
|
||||||
|
## What it changes in the composite
|
||||||
|
|
||||||
|
Resolving the name makes the element resolve, so `compose` now draws it. On all
|
||||||
|
five port screens ([before/after](../data/ratc-name-fix-render-effect.txt)):
|
||||||
|
|
||||||
|
| build | screen | mean brightness | high-frequency detail |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 4 | title | 72.77 → 72.81 | **×1.15** |
|
||||||
|
| 5 | main menu (JP) | 42.73 → 42.74 | **×1.27** |
|
||||||
|
| 6 | `EXTRAS` (JP) | 44.08 → 44.09 | **×1.27** |
|
||||||
|
| 8 | main menu | 41.57 → 41.58 | **×1.30** |
|
||||||
|
| 9 | `EXTRAS` | 42.99 → 43.00 | **×1.29** |
|
||||||
|
|
||||||
|
The brightness is unmoved and the detail is up by a quarter — which is exactly
|
||||||
|
the signature of *the same artwork at twice the resolution* replacing a 2×
|
||||||
|
upscale, and not of new content appearing. That it *should* be the full-res art
|
||||||
|
was settled separately and against the running game, in
|
||||||
|
[`ui-8ax-fullres-background.md`](ui-8ax-fullres-background.md); this page only
|
||||||
|
supplies the name that lets the renderer find it.
|
||||||
|
|
||||||
|
✅ **Nothing is covered.** The background is opaque and full-screen, and on the
|
||||||
|
menu it paints 4th of 16, so the worry is real. Measured at each element's
|
||||||
|
resting rect, before vs after: `ptbtn01` sd 59.98 → 60.03, `ptbtn03` 63.40 →
|
||||||
|
63.45, `ptbtn05` 66.54 → 66.58, `ptframe1` 51.16 → 51.20, `ptmsg` 56.11 → 56.12.
|
||||||
|
Everything survives; only the two `loop*` elements paint beneath it, and those
|
||||||
|
are excluded from the default composite anyway.
|
||||||
|
|
||||||
|
⚠️ **Both backgrounds are now drawn** — `ptbase.t32` upscaled 2×, then the
|
||||||
|
full-res one opaquely over it. Correct output, wasted fill. The port should draw
|
||||||
|
only the full-res one, and take its *timing* from `ptbase`'s element, which is
|
||||||
|
the one carrying the keyframes.
|
||||||
|
|
||||||
|
## What is NOT decoded
|
||||||
|
|
||||||
|
❔ **No declaration word points at the child.** Before reading the bytes I tested
|
||||||
|
whether the 60-byte element declaration indexes the T8aD child table. Its unread
|
||||||
|
words are `+28`, `+36`, `+44` and `+56`; the control is the resolved elements,
|
||||||
|
whose child index is known. On the main menu, of 13 controls the words reproduce
|
||||||
|
the child index **1, 0, 0 and 1** times — and both 1s are the trivial index-0
|
||||||
|
case. There is no pointer; the association is by name, and the name is the `opt `
|
||||||
|
string. [`decl_word_probe.rs`](../../../crates/sylpheed-formats/examples/decl_word_probe.rs)
|
||||||
|
|
||||||
|
✅ Incidental, from the same probe: **`+44` is a button ordinal.** It is `1…5` on
|
||||||
|
exactly the five `ptbtn0N.rat` elements of the main menu, in order, and `−1` on
|
||||||
|
every other element. Not needed for anything open, and recorded rather than
|
||||||
|
chased.
|
||||||
120
docs/re/structures/ratc-tan-frame-sequence.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# `.tan` — one name over a run of frames, and the 60 "nameless" children it explains
|
||||||
|
|
||||||
|
**Status:** ✅ `DECODED`, with a disc-wide check. This closes the 🟡 reach caveat
|
||||||
|
left open by [`ratc-child-names.md`](ratc-child-names.md): *"60 children have no
|
||||||
|
`opt ` block within 128 bytes and still fall back to the scan — whether they
|
||||||
|
genuinely lack the block or sit past the search window is not established."*
|
||||||
|
|
||||||
|
**Neither.** They are not children. They are the **ten frames of a single `.tan`
|
||||||
|
resource**, and the one `opt ` block that names the whole run sits up to 545 KB
|
||||||
|
behind the last of them.
|
||||||
|
|
||||||
|
## What was measured
|
||||||
|
|
||||||
|
[`examples/ratc_optless_children.rs`](../../../crates/sylpheed-formats/examples/ratc_optless_children.rs)
|
||||||
|
re-runs `ratc::parse`'s own guards over every child on the disc and reports, for
|
||||||
|
each rejection, *which* guard fired and whether a tag exists further back
|
||||||
|
([data](../data/ratc-tan-frame-sequence.txt)):
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| RATC children scanned | 18 002 |
|
||||||
|
| with no accepted `opt ` block | **60** |
|
||||||
|
| of those, rejected by **length**, **gap** or **charset** | **0** |
|
||||||
|
| of those, rejected because the only tag is **beyond the 128-byte window** | **60** |
|
||||||
|
| of those, that are child #0 of their bundle | **0** |
|
||||||
|
| archives involved | **1** — `GP_READY_ROOM.pak` |
|
||||||
|
| bundles involved | **6** — entries 26, 30, 159, 160, 1029, 1050, each exactly 706 609 B |
|
||||||
|
| children involved | **1…10 of each**, never 0 and never 11+ |
|
||||||
|
|
||||||
|
The distances are the tell. Within one bundle they are
|
||||||
|
|
||||||
|
```text
|
||||||
|
213, 60 813, 121 413, 182 013, 242 613, 303 213, 363 813, 424 413, 485 013, 545 613
|
||||||
|
```
|
||||||
|
|
||||||
|
— an exact arithmetic progression, step **60 600**. Ten different children all
|
||||||
|
find the **same** `opt ` tag, because there is only one. Nine of them do not have
|
||||||
|
a block that is merely far away; they have no block.
|
||||||
|
|
||||||
|
## What they are
|
||||||
|
|
||||||
|
Reading bundle 26 directly, without the Rust parser, the 15 "children" resolve:
|
||||||
|
|
||||||
|
| # | offset | kind | size | `opt ` at | name |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 0 | `0x000444` | T8aD | 1 933 | −40 | `pbf15_energie_generator2.t32` |
|
||||||
|
| **1…9** | `0x000bd1` … | T8aD | **60 600** each | −213 … −485 013 | **`pb_f15_eg_anm.tan`** |
|
||||||
|
| **10** | `0x085e49` | T8aD | **60 632** | −545 613 | **`pb_f15_eg_anm.tan`** |
|
||||||
|
| 11 | `0x094b21` | T8aD | 17 523 | −32 | `pbf15_pd_inside2.t32` |
|
||||||
|
| 12 | `0x098f94` | T8aD | 75 068 | −35 | `pbenergie_generator.t32` |
|
||||||
|
| 13 | `0x0ab4d0` | T8aD | 4 697 | −28 | `pbf15_eg_eff.t32` |
|
||||||
|
| 14 | `0x0ac729` | RATC | 264 | −25 | `pb_s15_eg.rat` |
|
||||||
|
|
||||||
|
So the format is doing something perfectly ordinary that our scan had no concept
|
||||||
|
of: **`.tan` is a frame sequence.** One `opt ` block declares the resource, and
|
||||||
|
its payload is a run of equal-size `T8aD` blocks, one per frame. The name is on
|
||||||
|
the disc and always was. What was missing was the idea that one name can cover
|
||||||
|
more than one block.
|
||||||
|
|
||||||
|
`anm` in `pb_f15_eg_anm` is the authors' own abbreviation, and it agrees.
|
||||||
|
|
||||||
|
## The disc-wide check
|
||||||
|
|
||||||
|
Every `opt ` block in every RATC bundle in all 33 `dat/*.pak`, by the extension
|
||||||
|
it names
|
||||||
|
([`tools/re-capture/ratc_opt_name_census.py`](../../../tools/re-capture/ratc_opt_name_census.py)):
|
||||||
|
|
||||||
|
| extension | count | what it is |
|
||||||
|
|---|---|---|
|
||||||
|
| `.t32` | 14 756 | a `T8aD` sprite |
|
||||||
|
| `.rat` | 3 311 | a nested RATC leaf |
|
||||||
|
| `.prm` | 367 | a primitive |
|
||||||
|
| `.tbm` | 224 | — |
|
||||||
|
| `.sbo` | 54 | — |
|
||||||
|
| **`.tan`** | **6** | **a frame sequence** |
|
||||||
|
| | **18 718** | |
|
||||||
|
|
||||||
|
A RATC bundle names exactly six kinds of resource, and **`.tan` occurs six times
|
||||||
|
on the whole disc** — all of them `pb_f15_eg_anm.tan`, one per language copy of
|
||||||
|
the same bundle, each holding **10 frames**.
|
||||||
|
|
||||||
|
**6 × 10 = 60.** That is the entire population of opt-less children, with nothing
|
||||||
|
left over. The negative is closed, not narrowed.
|
||||||
|
|
||||||
|
## ⚠️ What this says about `ratc::parse`
|
||||||
|
|
||||||
|
The child list **over-reports**. `parse` finds children by scanning for the four
|
||||||
|
child magics, so a `.tan`'s ten frames are listed as ten anonymous children of the
|
||||||
|
bundle rather than as one named resource with ten frames. The disc's "18 002
|
||||||
|
children" is therefore 18 002 *magic-delimited blocks*, of which 60 are frames.
|
||||||
|
|
||||||
|
**Not changed here**, deliberately: nothing in the menu milestone reads a `.tan`,
|
||||||
|
and a rewrite of the child model is a bigger change than the one fact it would
|
||||||
|
buy. Recorded so that a later consumer of `.tan` knows the shape it needs.
|
||||||
|
|
||||||
|
## ❔ Not established
|
||||||
|
|
||||||
|
* **The frame timing.** Ten frames of the same size is a sequence; nothing here
|
||||||
|
shows the rate, whether it loops, or whether the frames are equal-duration. No
|
||||||
|
field was looked for.
|
||||||
|
* **The pixel layout of a 60 600-byte frame.** They decode as `T8aD` like any
|
||||||
|
other sprite as far as the magic goes; their dimensions were not read.
|
||||||
|
* **What `.tbm` and `.sbo` are.** They surfaced from the same census and are
|
||||||
|
recorded above as counts only.
|
||||||
|
* **The two `opt ` totals do not reconcile exactly** and are not forced to.
|
||||||
|
This census counts **18 718** blocks; the Rust audit in
|
||||||
|
[`ratc-child-names.md`](ratc-child-names.md) counts **17 942** children *with* a
|
||||||
|
block. They apply different guards — the Rust one additionally requires the
|
||||||
|
named thing to be one of the four child magics and to follow within 8 bytes,
|
||||||
|
which `.prm` / `.tbm` / `.sbo` (645 blocks) never satisfy. That accounts for
|
||||||
|
most of the 776 difference but not all of it, and the remainder was not chased.
|
||||||
|
Each number is reported as what its own script measured.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
`GP_READY_ROOM.pak` is **out of scope** for the menu milestone ([S1 is a
|
||||||
|
no-go](../ready-room-probe.md)), and `.tan` occurs in no other archive. **None of
|
||||||
|
the five menu screens contains a `.tan`**, so nothing the port draws changes.
|
||||||
|
This closes a caveat on a decode the port *does* depend on, rather than adding a
|
||||||
|
capability.
|
||||||
@@ -40,7 +40,7 @@ resolves all twenty-four:
|
|||||||
| `stage\StageParameter_Test.tbl` | 1 — the developer stage |
|
| `stage\StageParameter_Test.tbl` | 1 — the developer stage |
|
||||||
|
|
||||||
**That is exactly the 24, and it explains 24 against the 29 stage records**
|
**That is exactly the 24, and it explains 24 against the 29 stage records**
|
||||||
([`challenge-mission-gate.md`](challenge-mission-gate.md) counts 29:
|
([`challenge-mission-gate.md`](../challenge-mission-gate.md) counts 29:
|
||||||
16 story + 6 tutorial + 6 challenge + `Test`): the six tutorials do not get one
|
16 story + 6 tutorial + 6 challenge + `Test`): the six tutorials do not get one
|
||||||
table each, they **share a single `_Tutorial` table**, and `_Test` accounts for
|
table each, they **share a single `_Tutorial` table**, and `_Test` accounts for
|
||||||
the last. 16 + 6 + 1 + 1 = 24.
|
the last. 16 + 6 + 1 + 1 = 24.
|
||||||
|
|||||||