80 findings, not the 14 the first run showed -- clippy stops at the first failing compilation unit, so `--keep-going` is what makes the list complete. 60 were machine-applicable (`cargo clippy --fix`). The rest by hand: * five descending `sort_by` -> `sort_by_key(Reverse(..))` * `chunks_exact(4)` on both sides of four zips, so the compared items stay `[u8; 4]` rather than one array against one slice * three `type` aliases for the census maps and the captured-quad tuple * `&PathBuf` -> `&Path` in two disc tests * two range loops; one of them keeps `#[allow(needless_range_loop)]` with the reason -- the index is into a map's value, which changes each iteration * the module doc list in `invert_capture` re-indented to markdown's rules * `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed. The sibling test at :162 is the one that asserts an offset; if this one was meant to as well, that is a test change and not a lint fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
299 lines
11 KiB
Rust
299 lines
11 KiB
Rust
//! Does `T8aD +0x04` bit `0x02` predict the blend the GAME uses?
|
|
//!
|
|
//! ⚠️ **`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** — it is a claim about our renderer, and the corpus's own rule
|
|
//! says so. Since it was written, the blend has been measured off the GPU per
|
|
//! draw on three screens (`structures/ui-blend-mode-measured.md`), so the claim
|
|
//! can now be tested against the oracle instead of against a render.
|
|
//!
|
|
//! The labels below are **not** from a render. Every one is a
|
|
//! `RB_BLENDCONTROL0` value read out of the guest command stream and attributed
|
|
//! to an element by quad size:
|
|
//! `data/ui-blend-mode-measured.txt`, `data/ui-blend-title-and-replication.txt`,
|
|
//! `data/ui-blend-extras-complete.txt`.
|
|
//!
|
|
//! cargo run -p sylpheed-formats --example blend_vs_t8ad_bit
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
|
|
|
/// (build entry, sprite, measured additive?) — the oracle's verdicts, verbatim.
|
|
const MEASURED: &[(usize, &str, bool)] = &[
|
|
// --- GP_TITLE entry 4 + 2, the live title -------------------------------
|
|
(4, "ptbase2.t32", false),
|
|
(4, "ptlogo1.t32", false),
|
|
(4, "ptlogo2.t32", false),
|
|
(4, "ptlogo_tm.t32", false),
|
|
(4, "ptcopyright.t32", false),
|
|
(4, "ptlogo_back2.t32", false),
|
|
(4, "ptlogo_back2eff.t32", false),
|
|
(2, "ptbtn00.t32", false),
|
|
(2, "ptbtn00f.t32", true),
|
|
// --- entry 5, the main menu ---------------------------------------------
|
|
(5, "ptbase.t32", false),
|
|
(5, "ptmsg.t32", false),
|
|
(5, "ptbtn01f.t32", false),
|
|
(5, "ptbtneff01.t32", false),
|
|
(5, "pteff10.t32", true),
|
|
(5, "pteff12.t32", true),
|
|
(5, "ptframe1.t32", true),
|
|
(5, "ptframe2.t32", true),
|
|
(5, "pteff03.t32", true), // the rotated sweep strips, via ptloop01/02
|
|
(5, "pteff03a.t32", true),
|
|
// --- entry 6, EXTRAS ------------------------------------------------------
|
|
(6, "ptbase.t32", false),
|
|
(6, "ptmsg2.t32", false),
|
|
(6, "pttitle.t32", false),
|
|
(6, "ptbtn11f.t32", false),
|
|
(6, "ptbtn12.t32", false),
|
|
(6, "ptbtn13.t32", false),
|
|
(6, "ptbtneff02.t32", false),
|
|
(6, "pteff10.t32", true),
|
|
(6, "pteff20.t32", true),
|
|
(6, "pteff21.t32", true),
|
|
(6, "pteff22.t32", true),
|
|
(6, "pteff23.t32", true),
|
|
(6, "ptframe3.t32", true),
|
|
(6, "ptframe4.t32", true),
|
|
(6, "pteff03.t32", true),
|
|
(6, "pteff03a.t32", true),
|
|
];
|
|
|
|
fn main() {
|
|
if std::env::args().any(|a| a == "decl") {
|
|
decl_rivals();
|
|
return;
|
|
}
|
|
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 hdr: BTreeMap<(usize, String), u32> = BTreeMap::new();
|
|
for e in [2usize, 4, 5, 6] {
|
|
let by = ar.read(&ar.entries()[e]).expect("entry");
|
|
let b = ui_layout::parse_build(&by).expect("build");
|
|
for (n, &(off, size)) in &b.sprites {
|
|
let s = &by[off..(off + size).min(by.len())];
|
|
if s.len() < 8 || &s[0..4] != b"T8aD" {
|
|
continue;
|
|
}
|
|
hdr.insert((e, n.clone()), u32::from_be_bytes([s[4], s[5], s[6], s[7]]));
|
|
}
|
|
}
|
|
println!(
|
|
"{:<10} {:<22} {:<10} {:>10} measured blend",
|
|
"entry", "sprite", "+0x04", "bit 0x02"
|
|
);
|
|
let (mut tp, mut tn, mut fp, mut fnn, mut missing) = (0, 0, 0, 0, 0);
|
|
for &(e, n, additive) in MEASURED {
|
|
let Some(&w) = hdr.get(&(e, n.to_string())) else {
|
|
println!(
|
|
"{e:<10} {n:<22} {:<10} {:>10} {}",
|
|
"MISSING",
|
|
"-",
|
|
if additive { "ADDITIVE" } else { "alpha-over" }
|
|
);
|
|
missing += 1;
|
|
continue;
|
|
};
|
|
let bit = w & 0x02 != 0;
|
|
match (bit, additive) {
|
|
(true, true) => tp += 1,
|
|
(false, false) => tn += 1,
|
|
(true, false) => fp += 1,
|
|
(false, true) => fnn += 1,
|
|
}
|
|
println!(
|
|
"{e:<10} {n:<22} {:08X} {:>10} {}{}",
|
|
w,
|
|
bit,
|
|
if additive { "ADDITIVE" } else { "alpha-over" },
|
|
if bit == additive {
|
|
""
|
|
} else {
|
|
" <== DISAGREES"
|
|
}
|
|
);
|
|
}
|
|
println!("\nbit set & additive {tp}");
|
|
println!("bit clear & alpha-over {tn}");
|
|
println!("bit set & alpha-over {fp} <- false positives");
|
|
println!("bit clear & additive {fnn} <- false negatives");
|
|
println!("sprite not found {missing}");
|
|
println!(
|
|
"\n{}",
|
|
if fp == 0 && fnn == 0 && missing == 0 {
|
|
"PERFECT PARTITION on every element whose blend was measured."
|
|
} else {
|
|
"THE BIT DOES NOT PREDICT THE MEASURED BLEND."
|
|
}
|
|
);
|
|
|
|
// ── THE CONTROL THAT MATTERS ────────────────────────────────────────────
|
|
// A perfect partition is worthless if half the header partitions equally
|
|
// well: then the sample is too small to single out a field, and picking
|
|
// `+0x04` bit 0x02 out of the tie is the same mistake as picking `+0x08`
|
|
// 0x8050 was. So: how many OTHER bits of the first 12 header words separate
|
|
// the same 35 elements without error?
|
|
let mut rivals: Vec<String> = Vec::new();
|
|
let mut words: BTreeMap<(usize, String), Vec<u32>> = BTreeMap::new();
|
|
for e in [2usize, 4, 5, 6] {
|
|
let by = ar.read(&ar.entries()[e]).expect("entry");
|
|
let b = ui_layout::parse_build(&by).expect("build");
|
|
for (n, &(off, size)) in &b.sprites {
|
|
let s = &by[off..(off + size).min(by.len())];
|
|
if s.len() < 48 || &s[0..4] != b"T8aD" {
|
|
continue;
|
|
}
|
|
words.insert(
|
|
(e, n.clone()),
|
|
(0..12)
|
|
.map(|k| {
|
|
u32::from_be_bytes([s[k * 4], s[k * 4 + 1], s[k * 4 + 2], s[k * 4 + 3]])
|
|
})
|
|
.collect(),
|
|
);
|
|
}
|
|
}
|
|
for w in 0..12 {
|
|
for bit in 0..32 {
|
|
let mut ok = true;
|
|
let mut set_seen = false;
|
|
let mut clear_seen = false;
|
|
for &(e, n, additive) in MEASURED {
|
|
let Some(v) = words.get(&(e, n.to_string())) else {
|
|
ok = false;
|
|
break;
|
|
};
|
|
let on = (v[w] >> bit) & 1 == 1;
|
|
if on {
|
|
set_seen = true
|
|
} else {
|
|
clear_seen = true
|
|
}
|
|
if on != additive {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
// A constant bit trivially "agrees" with nothing; require both sides.
|
|
if ok && set_seen && clear_seen {
|
|
rivals.push(format!("+0x{:02X} bit {bit} (0x{:X})", w * 4, 1u32 << bit));
|
|
}
|
|
}
|
|
}
|
|
println!("\nRIVAL FIELDS — other bits of the first 12 header words that separate");
|
|
println!("the same 35 elements with zero errors: {}", rivals.len());
|
|
for r in &rivals {
|
|
println!(" {r}");
|
|
}
|
|
if rivals.len() == 1 {
|
|
println!(" -> the sample singles out ONE field. Nothing else in the header does it.");
|
|
} else {
|
|
println!(
|
|
" -> the sample does NOT single out a field; {} candidates tie.",
|
|
rivals.len()
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── An integrity check the published decode did NOT do ──────────────────────
|
|
// The rival sweep above covers the 48-byte T8aD header. It does NOT cover the
|
|
// 60-byte DECLARATION entry, and the earlier declaration hunt was run with
|
|
// labels taken from the port's RENDER -- which put pteff10, pteff12, pteff20 and
|
|
// pteff21..23 on the alpha-over side, where the oracle says all six are
|
|
// additive. So the declaration has never been swept with correct labels, and if
|
|
// one of its words also partitions the 35 without error, "the field is the T8aD
|
|
// bit" is underdetermined.
|
|
//
|
|
// Run as: cargo run -p sylpheed-formats --example blend_vs_t8ad_bit -- decl
|
|
#[allow(dead_code)]
|
|
fn decl_rivals() {
|
|
use std::collections::BTreeMap;
|
|
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");
|
|
const AT: usize = 0x20;
|
|
const STRIDE: usize = 60;
|
|
let mut decl: BTreeMap<(usize, String), Vec<u32>> = BTreeMap::new();
|
|
for e in [2usize, 4, 5, 6] {
|
|
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
|
continue;
|
|
};
|
|
if by.len() < 0x18 {
|
|
continue;
|
|
}
|
|
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
|
for i in 0..count {
|
|
let at = AT + i * STRIDE;
|
|
if at + STRIDE > by.len() {
|
|
break;
|
|
}
|
|
let end = by[at..at + 12].iter().position(|&c| c == 0).unwrap_or(12);
|
|
let name = String::from_utf8_lossy(&by[at..at + end]).to_string();
|
|
decl.insert(
|
|
(e, name),
|
|
(0..15)
|
|
.map(|k| {
|
|
u32::from_be_bytes([
|
|
by[at + k * 4],
|
|
by[at + k * 4 + 1],
|
|
by[at + k * 4 + 2],
|
|
by[at + k * 4 + 3],
|
|
])
|
|
})
|
|
.collect(),
|
|
);
|
|
}
|
|
}
|
|
let mut missing: Vec<String> = Vec::new();
|
|
for &(e, n, _) in MEASURED {
|
|
if !decl.contains_key(&(e, n.to_string())) {
|
|
missing.push(format!("entry {e} {n}"));
|
|
}
|
|
}
|
|
println!("\n=== DECLARATION-ENTRY RIVAL SWEEP ===");
|
|
println!(
|
|
"measured elements with NO declaration entry of their own: {} of {}",
|
|
missing.len(),
|
|
MEASURED.len()
|
|
);
|
|
for m in &missing {
|
|
println!(" {m}");
|
|
}
|
|
if !missing.is_empty() {
|
|
println!(" -> no declaration field can select the blend for these, because they");
|
|
println!(" have no declaration entry. The header is the only per-sprite home.");
|
|
}
|
|
let labelled: Vec<&(usize, &str, bool)> = MEASURED
|
|
.iter()
|
|
.filter(|(e, n, _)| decl.contains_key(&(*e, n.to_string())))
|
|
.collect();
|
|
let mut rivals = 0;
|
|
#[allow(clippy::needless_range_loop)]
|
|
for w in 0..15 {
|
|
for bit in 0..32 {
|
|
let (mut ok, mut s, mut c) = (true, false, false);
|
|
for &&(e, n, additive) in &labelled {
|
|
let on = (decl[&(e, n.to_string())][w] >> bit) & 1 == 1;
|
|
if on {
|
|
s = true
|
|
} else {
|
|
c = true
|
|
}
|
|
if on != additive {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
if ok && s && c {
|
|
println!(" RIVAL: declaration +0x{:02X} bit {bit}", w * 4);
|
|
rivals += 1;
|
|
}
|
|
}
|
|
}
|
|
println!(
|
|
"declaration bits that separate the {} labellable elements: {rivals}",
|
|
labelled.len()
|
|
);
|
|
}
|