re: the splash blur is a TEXTURE -- palogo_*_eff is a baked 10-px glow
Play-test finding 4, answered as a mechanism and from the disc, so it generalises instead of describing one boot. ui-splash-draw-pass.md excluded a post-process from GPU state and closed with "that softness is in the texture or in which quads are drawn, not in a pass", leaving the two unseparated. It is both, and they are one fact: each logo ships a second texture that IS the blur -- the same artwork outset by exactly 10 px per side, concentric to <=1.5 px, drawn as its own alpha-over quad. Three results, each with its control: * The capture's eight anonymous quads are NAMED from the disc. Predicting each NDC rect from declared position + decoded sprite size matches all eight bijectively; every match <=0.0061, every runner-up >=0.0272, a 4.5-8.9x margin. That margin is the control -- eight similar boxes would match anything. * REFUTES splash-quad-timeline.txt's "the same three rects scaled slightly larger" (my own earlier wording). The x and y scale factors differ by up to 0.28; a uniform scale cannot do that, a fixed 10-px border can. The conclusion it supported (draw all six quads) stands; the model was wrong, and the wrong model tells a port to scale a sprite. * The T8aD blend bit tested OUT of sample on entries 10/11, which were not in its 35-row fit and are the screens under complaint. Pre-registered additive=false for all eight against 0 additive draws in 1048; held 8/8, with the control still reporting 9 additive on entry 6. Also resolves a REFUTED.md 🟡 <our-reader> in the reader's favour: the prediction is ours and the target is the oracle, so the agreement is evidence about the reader rather than a claim resting on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
This commit is contained in:
75
crates/sylpheed-formats/examples/splash_blend_check.rs
Normal file
75
crates/sylpheed-formats/examples/splash_blend_check.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! Out-of-sample test of the `T8aD +0x04` blend bit on the **boot splashes**.
|
||||
//!
|
||||
//! `ui-blend-mode-decoded.md` established the field on 35 elements over three
|
||||
//! screens (GP_TITLE entries 2, 4, 5, 6). The two splashes -- entries 10 and 11 --
|
||||
//! were NOT in that sample, and they are the screens the 2026-09-01 play-test
|
||||
//! says are wrong.
|
||||
//!
|
||||
//! The oracle for these two screens is `data/splash-draw-pass-census.txt`: over
|
||||
//! **1048 draws of frames 4..226**, covering both splashes end to end, the only
|
||||
//! blend states submitted are `0x00010001` (the clear) and `0x07010701`
|
||||
//! (source-over). Additive, `0x01010101`, appears **zero** times.
|
||||
//!
|
||||
//! PRE-REGISTERED PREDICTION, written before reading the disc: if the bit is the
|
||||
//! blend selector and it generalises off its training screens, then every sprite
|
||||
//! in entries 10 and 11 must report `additive = false`. Any `true` is a
|
||||
//! discrepancy the field has to answer for.
|
||||
//!
|
||||
//! cargo run --release -p sylpheed-formats --example splash_blend_check
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
|
||||
let mut additive = 0usize;
|
||||
let mut alpha_over = 0usize;
|
||||
let mut no_header = 0usize;
|
||||
|
||||
for entry in [10usize, 11] {
|
||||
let by = ar.read(&ar.entries()[entry]).expect("entry");
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
println!("entry {entry}: not a build");
|
||||
continue;
|
||||
};
|
||||
println!("\n## GP_TITLE entry {entry} -- {} elements, {} sprites",
|
||||
b.elements.len(), b.sprites.len());
|
||||
|
||||
// Every sprite the bundle carries, not only those a top-level element
|
||||
// names: a focused variant is reached through `focus_link` and would
|
||||
// otherwise be invisible to this check.
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
match ui_layout::blend_additive_by_name(&b, &by, n) {
|
||||
Some(true) => { additive += 1;
|
||||
println!(" {n:<26} word04=0x{:08X} ADDITIVE",
|
||||
ui_layout::header_word_04_by_name(&b, &by, n).unwrap()); }
|
||||
Some(false) => { alpha_over += 1;
|
||||
println!(" {n:<26} word04=0x{:08X} alpha-over",
|
||||
ui_layout::header_word_04_by_name(&b, &by, n).unwrap()); }
|
||||
None => { no_header += 1; println!(" {n:<26} (no T8aD header)"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nadditive={additive} alpha-over={alpha_over} no-header={no_header}");
|
||||
println!("oracle (splash-draw-pass-census.txt, 1048 draws): additive draws = 0");
|
||||
if additive == 0 {
|
||||
println!("PREDICTION HELD -- the bit agrees with the capture on both splashes");
|
||||
} else {
|
||||
println!("PREDICTION FAILED -- {additive} sprite(s) claim additive, \
|
||||
the capture submits none");
|
||||
}
|
||||
|
||||
// Control: this harness must be able to REPORT additive, or "additive=0"
|
||||
// is a property of the harness and not of the splashes. Entry 6 is a screen
|
||||
// the oracle measures as mixed.
|
||||
let by6 = ar.read(&ar.entries()[6]).expect("entry 6");
|
||||
let b6 = ui_layout::parse_build(&by6).expect("build 6");
|
||||
let c_add = b6.sprites.keys()
|
||||
.filter(|n| ui_layout::blend_additive_by_name(&b6, &by6, n) == Some(true)).count();
|
||||
println!("control -- entry 6 through the SAME code path reports additive={c_add} \
|
||||
(must be > 0): {}", if c_add > 0 { "PASS" } else { "FAIL" });
|
||||
}
|
||||
141
crates/sylpheed-formats/examples/splash_quad_names.rs
Normal file
141
crates/sylpheed-formats/examples/splash_quad_names.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! Name the capture's eight anonymous splash quads, from the disc.
|
||||
//!
|
||||
//! `data/splash-quad-timeline.txt` recorded eight distinct NDC rects off the
|
||||
//! guest's vertex stream and could only call them Q0..Q7 -- a draw capture sees
|
||||
//! geometry, not names. This predicts each rect from the DECLARED position and
|
||||
//! the DECODED sprite size in `GP_TITLE.pak` entries 10 and 11, and matches.
|
||||
//!
|
||||
//! x_ndc = 2*x/1280 - 1 y_ndc = 1 - 2*y/720 (y down on screen)
|
||||
//!
|
||||
//! Instruments: the prediction is ⟨disc⟩ -- `parse_build` + `t8ad::parse`, i.e.
|
||||
//! OUR reader. The target is ⟨capture⟩ -- the oracle's vertex stream. So an
|
||||
//! agreement here is not a claim resting on the reader: it VALIDATES the reader
|
||||
//! on the two splash bundles, which is what `REFUTED.md`'s 🟡 `⟨our-reader⟩`
|
||||
//! entry on the splash timeline asks for. A disagreement would indict the reader.
|
||||
//!
|
||||
//! CONTROL: an assignment is only meaningful if the rects are separable, so this
|
||||
//! reports the runner-up distance for every sprite. If the best and second-best
|
||||
//! were comparable, "8/8 matched" would be an artefact of eight similar boxes.
|
||||
//!
|
||||
//! cargo run --release -p sylpheed-formats --example splash_quad_names
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, t8ad, ui_layout};
|
||||
|
||||
/// The oracle. Verbatim from the header of `docs/re/data/splash-quad-timeline.txt`,
|
||||
/// which read them off the vertex buffer. Quantised to 0.01 by that file.
|
||||
const CAPTURED: &[(&str, f64, f64, f64, f64, usize)] = &[
|
||||
// name, x0, x1, y0, y1, draws submitted in
|
||||
("Q0", -0.520, 0.520, -0.100, 0.080, 111),
|
||||
("Q1", -0.390, 0.390, 0.350, 0.550, 87),
|
||||
("Q2", -0.190, 0.190, -0.120, 0.120, 87),
|
||||
("Q3", -0.300, 0.300, -0.620, -0.250, 87),
|
||||
("Q4", -0.410, 0.410, 0.320, 0.570, 21),
|
||||
("Q5", -0.200, 0.210, -0.150, 0.150, 21),
|
||||
("Q6", -0.320, 0.310, -0.650, -0.220, 21),
|
||||
("Q7", -0.530, 0.540, -0.130, 0.120, 8),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
|
||||
// Predict a rect for every sprite-bearing element of the two splash builds.
|
||||
let mut predicted: Vec<(String, f64, f64, f64, f64)> = Vec::new();
|
||||
for entry in [10usize, 11] {
|
||||
let by = ar.read(&ar.entries()[entry]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for el in &b.elements {
|
||||
let Some(sprite) = el.sprite.as_ref() else { continue };
|
||||
let Some(&(off, size)) = b.sprites.get(sprite) else { continue };
|
||||
let Some(img) = t8ad::parse(&by[off..off + size]) else { continue };
|
||||
// Declared placement is constant across every keyframe on these
|
||||
// eight elements, so any keyframe gives the same rect; take the first.
|
||||
let Some(kf) = el.keyframes.first() else { continue };
|
||||
let (x, y) = (kf.x as f64, kf.y as f64);
|
||||
let (w, h) = (img.width as f64, img.height as f64);
|
||||
predicted.push((
|
||||
sprite.clone(),
|
||||
2.0 * x / 1280.0 - 1.0,
|
||||
2.0 * (x + w) / 1280.0 - 1.0,
|
||||
1.0 - 2.0 * (y + h) / 720.0,
|
||||
1.0 - 2.0 * y / 720.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let dist = |p: &(String, f64, f64, f64, f64), c: &(&str, f64, f64, f64, f64, usize)| {
|
||||
(p.1 - c.1).abs().max((p.2 - c.2).abs())
|
||||
.max((p.3 - c.3).abs()).max((p.4 - c.4).abs())
|
||||
};
|
||||
|
||||
// The capture rounds to 0.01, so a correct prediction must land inside half
|
||||
// a step plus the pixel grid: 0.01 NDC is 6.4 px in x, 3.6 px in y.
|
||||
const TOL: f64 = 0.010;
|
||||
println!("{:<26} {:<4} {:>8} {:>9} {:>7} {}",
|
||||
"sprite (disc)", "quad", "max|d|", "runner-up", "draws", "verdict");
|
||||
let (mut ok, mut bad) = (0, 0);
|
||||
let mut used: Vec<&str> = Vec::new();
|
||||
for p in &predicted {
|
||||
let mut ds: Vec<(f64, &(&str, f64, f64, f64, f64, usize))> =
|
||||
CAPTURED.iter().map(|c| (dist(p, c), c)).collect();
|
||||
ds.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
let (d0, best) = (ds[0].0, ds[0].1);
|
||||
let d1 = ds[1].0;
|
||||
let good = d0 <= TOL && !used.contains(&best.0);
|
||||
if good { ok += 1; used.push(best.0) } else { bad += 1 }
|
||||
println!("{:<26} {:<4} {:>8.4} {:>9.4} {:>7} {}",
|
||||
p.0, best.0, d0, d1, best.5,
|
||||
if good { "OK" } else { "MISMATCH" });
|
||||
}
|
||||
println!("\n{ok} named, {bad} unmatched, of {} predicted / {} captured",
|
||||
predicted.len(), CAPTURED.len());
|
||||
println!("CONTROL: every runner-up above must be far outside the {TOL} tolerance; \
|
||||
if it is not, the rects are not separable and the naming is luck.");
|
||||
}
|
||||
|
||||
/// The second question this data answers, appended as its own pass: what IS an
|
||||
/// `_eff` companion? `splash-quad-timeline.txt` called them "the same rects
|
||||
/// scaled slightly larger", which is an inference from four rounded NDC numbers.
|
||||
/// The disc says otherwise, and the difference matters to anyone drawing them.
|
||||
#[test]
|
||||
fn eff_is_a_concentric_outset_not_a_scale() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
for (entry, pairs) in [
|
||||
(10usize, &[("palogo_sqex", "palogo_sqex_eff")][..]),
|
||||
(11, &[("palogo_gamearts", "palogo_gamearts_eff"),
|
||||
("palogo_seta", "palogo_seta_eff"),
|
||||
("palogo_anima", "palogo_anima_eff")][..]),
|
||||
] {
|
||||
let by = ar.read(&ar.entries()[entry]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for (logo, eff) in pairs {
|
||||
let g = |n: &str| {
|
||||
let el = b.elements.iter()
|
||||
.find(|e| e.sprite.as_deref() == Some(&format!("{n}.t32"))).unwrap();
|
||||
let &(off, size) = b.sprites.get(&format!("{n}.t32")).unwrap();
|
||||
let img = t8ad::parse(&by[off..off + size]).unwrap();
|
||||
let kf = el.keyframes.first().unwrap();
|
||||
(kf.x as f64, kf.y as f64, img.width as f64, img.height as f64)
|
||||
};
|
||||
let (lx, ly, lw, lh) = g(logo);
|
||||
let (ex, ey, ew, eh) = g(eff);
|
||||
// concentric?
|
||||
let dcx = (ex + ew / 2.0) - (lx + lw / 2.0);
|
||||
let dcy = (ey + eh / 2.0) - (ly + lh / 2.0);
|
||||
assert!(dcx.abs() <= 2.0 && dcy.abs() <= 2.0,
|
||||
"{eff}: centres differ by ({dcx},{dcy}) -- not concentric");
|
||||
// uniform outset, NOT a uniform scale?
|
||||
let (ox, oy) = ((ew - lw) / 2.0, (eh - lh) / 2.0);
|
||||
assert!((ox - oy).abs() <= 2.0,
|
||||
"{eff}: outset ({ox},{oy}) is not uniform");
|
||||
assert!(ox >= 8.0 && ox <= 12.0, "{eff}: outset {ox} px outside 8..12");
|
||||
// and the scale reading it displaces
|
||||
let (sx, sy) = (ew / lw, eh / lh);
|
||||
assert!((sx - sy).abs() > 0.05,
|
||||
"{eff}: scales {sx:.3}/{sy:.3} ARE uniform -- 'scaled larger' would stand");
|
||||
println!("{eff:<26} outset {ox:.0}x{oy:.0} px, centre off by \
|
||||
({dcx:.1},{dcy:.1}), scale {sx:.3}/{sy:.3} (NOT uniform)");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user