REFUTED.md kills this claim: 'T8aD +0x04 bit 0x02 selects an additive blend -> mine, and refuted. Blending those sprites additively worsens every measure against the capture.' That refutation rests entirely on our renderer, which the corpus's own rule calls a hypothesis under test. The blend is now measured off the GPU, so the claim can be tested against the oracle. 35 elements over three screens, every label an RB_BLENDCONTROL0 value read from the command stream: 16 bit-set and additive, 19 bit-clear and alpha-over, zero false positives, zero false negatives. The control that makes it a decode rather than a coincidence: of every bit of the first 12 header words, EXACTLY ONE separates those 35 elements without error. Nothing ties with it. A perfect partition on a small sample is worthless if half the header partitions equally well, which is the mistake +0x08 = 0x8050 was. And the pair no confound survives: ptbtn00 = 0x0110, ptbtn00f = 0x0112 -- the PRESS (A) plate and its own highlight, same screen, differing in exactly this bit, drawn alpha-over and additive respectively. Committed alongside is a PREDICTION for GP_OPTIONS, written before the capture that tests it: a different archive, a different element set, and a MIXED prediction -- po_menu_eff01/02/03 additive, 592 elements alpha-over. Falsified if those three draw alpha-over or anything else draws additive. The developer splash was considered first and rejected as a test: both its elements predict alpha-over, so it can fail but cannot discriminate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
65 lines
3.0 KiB
Rust
65 lines
3.0 KiB
Rust
//! The main menu's sprites, by their ALPHA channel — is `ptframe1`/`ptframe2`'s
|
|
//! "no fully-opaque pixel" a property of the artwork, and does any alpha value
|
|
//! look like a scale the game expands (e.g. 0..128) rather than 0..255?
|
|
//!
|
|
//! The port measures both frames as rendering too DARK against the capture, with
|
|
//! the shortfall correlating with the BACKGROUND. Two different causes predict
|
|
//! that: a background-scaling blend selected in code, or an alpha that is too
|
|
//! LOW in our decode. This example tests the second, which is on the disc.
|
|
//!
|
|
//! cargo run -p sylpheed-formats --example frame_alpha_census
|
|
use sylpheed_formats::{pak::PakArchive, t8ad, ui_layout};
|
|
use std::path::PathBuf;
|
|
|
|
fn census(name: &str, img: &t8ad::T8adImage) {
|
|
let n = (img.width * img.height) as usize;
|
|
let mut hist = [0usize; 256];
|
|
for p in 0..n {
|
|
hist[img.rgba[p * 4 + 3] as usize] += 1;
|
|
}
|
|
let zero = hist[0];
|
|
let full = hist[255];
|
|
let max = (0..256).rev().find(|&a| hist[a] > 0).unwrap_or(0);
|
|
let nonzero = n - zero;
|
|
// the top five alpha values that actually occur, by population
|
|
let mut top: Vec<(usize, usize)> = (1..256).map(|a| (hist[a], a)).filter(|&(c, _)| c > 0).collect();
|
|
top.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
|
let top5: Vec<String> = top.iter().take(5).map(|&(c, a)| format!("{a}x{c}")).collect();
|
|
println!(
|
|
"{name:<16} {}x{:<4} px={n:<8} a=0:{:5.1}% a=255:{:5.1}% max={max:<3} \
|
|
partial(1..254)/nonzero={:5.1}% top:[{}]",
|
|
img.width, img.height,
|
|
100.0 * zero as f64 / n as f64,
|
|
100.0 * full as f64 / n as f64,
|
|
if nonzero > 0 { 100.0 * (nonzero - full) as f64 / nonzero as f64 } else { 0.0 },
|
|
top5.join(" ")
|
|
);
|
|
}
|
|
|
|
fn main() {
|
|
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
|
let argv: Vec<String> = std::env::args().skip(1).collect();
|
|
let pak = argv.iter().find(|a| a.parse::<usize>().is_err())
|
|
.cloned().unwrap_or_else(|| "GP_TITLE".to_string());
|
|
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
|
// Builds default to the two the port ships and can be overridden, so the
|
|
// same census serves the title (4) and the `PRESS (A)` plate (2).
|
|
let args: Vec<usize> = argv.iter().filter_map(|a| a.parse().ok()).collect();
|
|
let builds: Vec<usize> = if args.is_empty() { vec![5, 6] } else { args };
|
|
for build in builds {
|
|
let Ok(by) = ar.read(&ar.entries()[build]) else { continue };
|
|
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
|
println!("=== {pak} build {build} ===");
|
|
let mut names: Vec<&String> = b.sprites.keys().collect();
|
|
names.sort();
|
|
for n in names {
|
|
let (off, size) = b.sprites[n];
|
|
let s = &by[off..(off + size).min(by.len())];
|
|
match t8ad::parse(s) {
|
|
Some(img) => census(n, &img),
|
|
None => println!("{n:<16} (not a T8aD / failed to parse)"),
|
|
}
|
|
}
|
|
}
|
|
}
|