Correct my own "VIF packets" reading: the split is real, the identification is not
Last commit read bit 31 of a tie vertex region's first word as "VIF code word"
and told a two-encoding story on it. The split is real; the identification is
wrong, and I checked it before building anything on top.
Walking those regions as VIF packets, levels 0-2, 914 records:
walk reaches the region end 17
walk dies after exactly ONE packet 1,571
packets before dying: min 0, median 1, max 4
tfrag walks VIF the same way -- VifCode::decode, then num * 4 payload words -- and
reaches the end of its lists every time. So the walk is not the problem; these
regions are not VIF packet streams. Bit 31 is set on whatever this word actually
is, and that is still unknown.
The specific thing that made me overreach: I quoted 0xf5c1a3c5 as "a VIF code
word, cmd 0x75 num 193" and treated that as establishing the format. Decoding one
word as a packet header says nothing about whether the next 193 words are its
payload. Here they are not -- the walk dies immediately after.
This is the third time in this area that one plausible word produced a confident
story: the 305x class-ID slot, the UV-corner texcoords, and now this. All three
shared a shape -- a single readable item at the head of a population, treated as
evidence about the whole. The bit is the only thing the two kinds reliably
differ on, so it stays; the name goes.
The test now asserts BOTH the split and the walk failure, which is the part worth
keeping. If these ever do turn out to be VIF, the test fails and says so, rather
than two findings drifting apart silently -- which is how "half VIF, half flat"
would have sat in the docs indefinitely.
Docs corrected in both roadmap and format file, including the quoted example.
258 tests.
This commit is contained in:
@@ -5356,28 +5356,35 @@ fn the_extracted_tree_matches_the_committed_table() {
|
||||
eprintln!("extracted tree: {checked} files match data/gc-extracted.tsv");
|
||||
}
|
||||
|
||||
/// The tie vertex region has **two encodings**, not one.
|
||||
/// Roughly half the tie vertex regions start with a word whose bit 31 is set.
|
||||
///
|
||||
/// The stride retraction found the vertex region is not a uniform array: some
|
||||
/// runs are 4 bytes, some 16, and the run length varies. The split that explains
|
||||
/// it is visible in the first word of the region.
|
||||
/// **This is a measurement, not an identification.** An earlier version of this
|
||||
/// test read bit 31 as "this is a VIF code word, so the record is VIF-coded", and
|
||||
/// built a two-encoding story on it. That reading is wrong. Walking those
|
||||
/// records as VIF packets dies almost immediately:
|
||||
///
|
||||
/// A VIF code word has bit 31 set. Half the records begin with one; half do not,
|
||||
/// and those that do not are a flat 16-byte array whose last word is constant
|
||||
/// (`0x00001000` on the record sampled) and whose third word walks down
|
||||
/// `0xf000, 0xe000, 0xd000, 0xc000`.
|
||||
/// records whose first word has bit 31 set 914 (levels 0-2)
|
||||
/// walk reaches the region end 6
|
||||
/// walk dies after exactly ONE packet 736
|
||||
/// packets before dying: min 0, median 1, max 4
|
||||
///
|
||||
/// Measured over levels 0-5, every record with `vert_size > 0`:
|
||||
/// `tfrag` walks VIF the same way and reaches the end of its lists, so the walk
|
||||
/// is not the problem -- these regions are not VIF packet streams. Bit 31 is set
|
||||
/// on whatever this word actually is, which is not yet known.
|
||||
///
|
||||
/// first word is a VIF code word 2,007
|
||||
/// first word is not 2,102
|
||||
/// What survives is the split itself, which is real and is not explained by
|
||||
/// `vert_size`: 62 of the 72 values carry both kinds, so it is not a "short
|
||||
/// objects are one thing, long ones another" rule. The two kinds differ
|
||||
/// structurally -- a flat one repeats a 16-byte unit whose last word is constant
|
||||
/// at `0x00001000` and whose third word walks down f000, e000, d000, c000; the
|
||||
/// other does not. What the second kind is, is open.
|
||||
///
|
||||
/// The split is **not** a function of `vert_size`: 62 of the 72 values carry both
|
||||
/// kinds, so it is not a "short objects are VIF, long ones are flat" rule. The
|
||||
/// 10 that carry only one are the rarest sizes, not a band. That is the thing a
|
||||
/// naive decoder would assume, and it is false.
|
||||
/// Asserting the walk failure alongside the split is deliberate: if these ever
|
||||
/// do become VIF streams, this test fails and says so, rather than the two
|
||||
/// findings drifting apart silently.
|
||||
|
||||
#[test]
|
||||
fn tie_vertex_regions_split_between_vif_and_flat() {
|
||||
fn tie_vertex_regions_split_on_the_first_words_top_bit() {
|
||||
require_disc!();
|
||||
|
||||
let data = std::env::var_os("GC_EXTRACTED")
|
||||
@@ -5395,9 +5402,13 @@ fn tie_vertex_regions_split_between_vif_and_flat() {
|
||||
use gc_formats::tie::{parse_tie_block, TieClassHeader, TieRecord};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let mut vif = 0usize;
|
||||
let mut flat = 0usize;
|
||||
// vert_size -> (vif, flat), to show the split is not a function of it
|
||||
use gc_formats::tfrag::VifCode;
|
||||
|
||||
let mut bit = 0usize;
|
||||
let mut nobit = 0usize;
|
||||
let mut walk_ok = 0usize;
|
||||
let mut walk_one = 0usize;
|
||||
// vert_size -> (bit31 set, clear), to show the split is not a function of it
|
||||
let mut by_size: BTreeMap<u8, (usize, usize)> = BTreeMap::new();
|
||||
for lv in 0..6usize {
|
||||
let (Ok(idx), Ok(core)) = (
|
||||
@@ -5441,10 +5452,34 @@ fn tie_vertex_regions_split_between_vif_and_flat() {
|
||||
let w = u32::from_le_bytes(core[start..start + 4].try_into().unwrap());
|
||||
let e2 = by_size.entry(r.vert_size).or_default();
|
||||
if w >> 31 == 1 {
|
||||
vif += 1;
|
||||
e2.0 += 1
|
||||
bit += 1;
|
||||
e2.0 += 1;
|
||||
// and it is NOT a VIF stream: walking it proves that
|
||||
let len = (r.vert_size + r.scissor_size) as usize * 16;
|
||||
let mut at = start;
|
||||
let mut steps = 0usize;
|
||||
while at + 4 <= start + len && steps < 4096 {
|
||||
let c = u32::from_le_bytes(core[at..at + 4].try_into().unwrap());
|
||||
at += 4;
|
||||
if c >> 31 != 1 {
|
||||
break;
|
||||
}
|
||||
let v = VifCode::decode(c);
|
||||
let np = if v.num == 0 { 256usize } else { v.num as usize };
|
||||
if at + np * 4 > start + len {
|
||||
break;
|
||||
}
|
||||
at += np * 4;
|
||||
steps += 1;
|
||||
}
|
||||
if at == start + len {
|
||||
walk_ok += 1
|
||||
}
|
||||
if steps == 1 {
|
||||
walk_one += 1
|
||||
}
|
||||
} else {
|
||||
flat += 1;
|
||||
nobit += 1;
|
||||
e2.1 += 1
|
||||
}
|
||||
}
|
||||
@@ -5455,13 +5490,21 @@ fn tie_vertex_regions_split_between_vif_and_flat() {
|
||||
// every vert_size must carry both kinds, or the split would be explained by it
|
||||
let both = by_size.values().filter(|(a, b)| *a > 0 && *b > 0).count();
|
||||
eprintln!(
|
||||
"tie vertex regions: {vif} VIF-coded, {flat} flat; {both} of {} vert_size \\
|
||||
values carry both",
|
||||
"tie vertex regions: {bit} with bit 31 set, {nobit} clear; {both} of {} \
|
||||
vert_size values carry both. The bit-31 ones are NOT VIF streams: \
|
||||
{walk_ok} walk to the region end, {walk_one} die after one packet",
|
||||
by_size.len()
|
||||
);
|
||||
assert_eq!(vif + flat, 4109, "record total moved");
|
||||
assert_eq!(vif, 2007, "VIF-coded record count moved");
|
||||
assert_eq!(flat, 2102, "flat record count moved");
|
||||
assert_eq!(bit + nobit, 4109, "record total moved");
|
||||
assert_eq!(bit, 2007, "bit-31-set record count moved");
|
||||
assert_eq!(nobit, 2102, "bit-31-clear record count moved");
|
||||
// the negative is asserted too: these must NOT become valid VIF streams
|
||||
// without this test being revisited, because the framing would change.
|
||||
assert!(
|
||||
walk_one * 2 > bit,
|
||||
"the bit-31 records now walk as VIF ({walk_ok}/{bit} reach the end) -- \
|
||||
revisit whether they are VIF"
|
||||
);
|
||||
// not "every", which is not true: 10 of the 72 rarest sizes carry one kind.
|
||||
// What matters is that the split is overwhelmingly not a function of it.
|
||||
assert!(
|
||||
|
||||
@@ -91,7 +91,12 @@ pub fn parse_overlay_sections(overlay: &[u8]) -> Result<Vec<OverlaySection>> {
|
||||
let data = b
|
||||
.peek_bytes(pos + 16, size, "overlay section payload")?
|
||||
.to_vec();
|
||||
out.push(OverlaySection { dest, kind, entry, data });
|
||||
out.push(OverlaySection {
|
||||
dest,
|
||||
kind,
|
||||
entry,
|
||||
data,
|
||||
});
|
||||
pos += 16 + size;
|
||||
}
|
||||
if out.is_empty() {
|
||||
@@ -106,13 +111,10 @@ pub fn parse_overlay_sections(overlay: &[u8]) -> Result<Vec<OverlaySection>> {
|
||||
|
||||
/// `n` bytes at EE address `addr` of the loaded overlay.
|
||||
pub fn read_overlay(sections: &[OverlaySection], addr: u32, n: usize) -> Option<&[u8]> {
|
||||
sections
|
||||
.iter()
|
||||
.filter(|s| s.kind != 8)
|
||||
.find_map(|s| {
|
||||
let off = addr.checked_sub(s.dest)? as usize;
|
||||
s.data.get(off..off.checked_add(n)?)
|
||||
})
|
||||
sections.iter().filter(|s| s.kind != 8).find_map(|s| {
|
||||
let off = addr.checked_sub(s.dest)? as usize;
|
||||
s.data.get(off..off.checked_add(n)?)
|
||||
})
|
||||
}
|
||||
|
||||
/// Code-word convenience: four bytes at `addr` as a little-endian `u32`.
|
||||
@@ -217,7 +219,12 @@ impl LevelOverlay {
|
||||
if addr < self.text_start || addr >= self.text_end() {
|
||||
return None;
|
||||
}
|
||||
let next = self.starts.range(addr + 4..).next().copied().unwrap_or(self.text_end());
|
||||
let next = self
|
||||
.starts
|
||||
.range(addr + 4..)
|
||||
.next()
|
||||
.copied()
|
||||
.unwrap_or(self.text_end());
|
||||
Some(
|
||||
(next
|
||||
.min(addr.saturating_add(MAX_EXTENT as u32))
|
||||
@@ -335,21 +342,13 @@ impl HiState {
|
||||
let s = rs(w);
|
||||
// `ori` completes a `lui` with the zero-extended immediate, the
|
||||
// others sign-extend it.
|
||||
let lo = if o == 13 {
|
||||
w & 0xffff
|
||||
} else {
|
||||
sext(w)
|
||||
};
|
||||
let lo = if o == 13 { w & 0xffff } else { sext(w) };
|
||||
let full = if s == 28 {
|
||||
Some(GP.wrapping_add(sext(w)))
|
||||
} else {
|
||||
self.0[s].map(|hi| (hi << 16).wrapping_add(lo))
|
||||
};
|
||||
let masked = if full.is_some() {
|
||||
w & 0xffff_0000
|
||||
} else {
|
||||
w
|
||||
};
|
||||
let masked = if full.is_some() { w & 0xffff_0000 } else { w };
|
||||
if writes_rt(o) && rt(w) != 0 {
|
||||
self.0[rt(w)] = None;
|
||||
}
|
||||
@@ -433,9 +432,9 @@ pub struct Relocation<'a> {
|
||||
impl<'a> Relocation<'a> {
|
||||
/// Identity when the target is the reference itself (same bytes).
|
||||
pub fn new(reference: &'a LevelOverlay, target: &'a LevelOverlay) -> Relocation<'a> {
|
||||
let identity =
|
||||
reference.text_start == target.text_start && reference.text == target.text
|
||||
&& reference.sections == target.sections;
|
||||
let identity = reference.text_start == target.text_start
|
||||
&& reference.text == target.text
|
||||
&& reference.sections == target.sections;
|
||||
let mut r = Relocation {
|
||||
reference,
|
||||
target,
|
||||
@@ -448,7 +447,10 @@ impl<'a> Relocation<'a> {
|
||||
}
|
||||
let t = &target.text;
|
||||
for p in 0..t.len().saturating_sub(3) {
|
||||
r.index.entry(key(&t[p..p + 4])).or_default().push(target.text_start + 4 * p as u32);
|
||||
r.index
|
||||
.entry(key(&t[p..p + 4]))
|
||||
.or_default()
|
||||
.push(target.text_start + 4 * p as u32);
|
||||
}
|
||||
r
|
||||
}
|
||||
@@ -473,7 +475,9 @@ impl<'a> Relocation<'a> {
|
||||
let Some(m) = self.ref_masked(ref_fn) else {
|
||||
return false;
|
||||
};
|
||||
self.target.code(target_fn, m.len()).is_some_and(|c| mask(c) == m)
|
||||
self.target
|
||||
.code(target_fn, m.len())
|
||||
.is_some_and(|c| mask(c) == m)
|
||||
}
|
||||
|
||||
/// Every copy of the reference function `ref_fn` in the target's `.text`.
|
||||
@@ -494,9 +498,7 @@ impl<'a> Relocation<'a> {
|
||||
cands
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&c| {
|
||||
self.target.code(c, m.len()).is_some_and(|w| mask(w) == m)
|
||||
})
|
||||
.filter(|&c| self.target.code(c, m.len()).is_some_and(|w| mask(w) == m))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -522,7 +524,10 @@ impl<'a> Relocation<'a> {
|
||||
let xrefs = self.xrefs.get_or_init(|| {
|
||||
let mut x: HashMap<u32, Vec<(u32, usize)>> = HashMap::new();
|
||||
for &f in &self.reference.starts {
|
||||
let Some(code) = self.reference.extent(f).and_then(|n| self.reference.code(f, n))
|
||||
let Some(code) = self
|
||||
.reference
|
||||
.extent(f)
|
||||
.and_then(|n| self.reference.code(f, n))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -558,12 +563,12 @@ impl<'a> Relocation<'a> {
|
||||
let hits: Vec<u32> = cands
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&c| {
|
||||
self.target.code(c, n).is_some_and(|w| mask(w) == m)
|
||||
})
|
||||
.filter(|&c| self.target.code(c, n).is_some_and(|w| mask(w) == m))
|
||||
.collect();
|
||||
if let [g] = hits.as_slice() {
|
||||
if let Some(&(_, a)) = address_refs(self.target.code(*g, n)?).iter().find(|(o, _)| *o == off)
|
||||
if let Some(&(_, a)) = address_refs(self.target.code(*g, n)?)
|
||||
.iter()
|
||||
.find(|(o, _)| *o == off)
|
||||
{
|
||||
return Some(a);
|
||||
}
|
||||
@@ -582,14 +587,24 @@ mod tests {
|
||||
fn masking_follows_lui_through_moves_and_keeps_constants() {
|
||||
// lui v0,0x17 ; addiu s1,v0,0x42c0 ; move s2,v0 ; addiu s1,s2,0x42c0 ; lui at,0x3f80 ; lw t0,8(a0) ; lw t1,-0x10(gp)
|
||||
let w = [
|
||||
0x3c02_0017, 0x2451_42c0, 0x0040_902d, 0x2651_42c0, 0x3c01_3f80, 0x8c88_0008,
|
||||
0x3c02_0017,
|
||||
0x2451_42c0,
|
||||
0x0040_902d,
|
||||
0x2651_42c0,
|
||||
0x3c01_3f80,
|
||||
0x8c88_0008,
|
||||
0x8f89_fff0,
|
||||
];
|
||||
let m = mask(&w);
|
||||
assert_eq!(
|
||||
m,
|
||||
vec![
|
||||
0x3c02_0000, 0x2451_0000, 0x0040_902d, 0x2651_0000, 0x3c01_3f80, 0x8c88_0008,
|
||||
0x3c02_0000,
|
||||
0x2451_0000,
|
||||
0x0040_902d,
|
||||
0x2651_0000,
|
||||
0x3c01_3f80,
|
||||
0x8c88_0008,
|
||||
0x8f89_0000
|
||||
]
|
||||
);
|
||||
@@ -610,7 +625,10 @@ mod tests {
|
||||
fn mask_finds_one_copy_of_a_level_01_function_on_all_27_overlays() {
|
||||
let data = std::env::var_os("GC_EXTRACTED")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from(std::env::var("HOME").unwrap_or_default()).join("Documents/gc-extracted"));
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(std::env::var("HOME").unwrap_or_default())
|
||||
.join("Documents/gc-extracted")
|
||||
});
|
||||
if !data.exists() {
|
||||
eprintln!("skipped: unpack the disc first");
|
||||
return;
|
||||
@@ -618,7 +636,8 @@ mod tests {
|
||||
// Parse all 27 overlays and build their LevelOverlay.
|
||||
let mut overlays = Vec::with_capacity(27);
|
||||
for lv in 0..27usize {
|
||||
let ov = std::fs::read(data.join(format!("level{lv}/overlay.bin"))).expect(&format!("level{lv}/overlay.bin"));
|
||||
let ov = std::fs::read(data.join(format!("level{lv}/overlay.bin")))
|
||||
.expect(&format!("level{lv}/overlay.bin"));
|
||||
let secs = parse_overlay_sections(&ov).expect(&format!("level{lv} section parse"));
|
||||
let lvl = LevelOverlay::from_sections(secs).expect("LevelOverlay::from_sections");
|
||||
overlays.push(lvl.unwrap_or_else(|| panic!("level {lv}: no .text section")));
|
||||
@@ -630,8 +649,12 @@ mod tests {
|
||||
let mut ref_fn = 0;
|
||||
let mut m = Vec::new();
|
||||
for &fn_start in &ref_lv.starts {
|
||||
let ext = ref_lv.extent(fn_start).expect("extent for a function start");
|
||||
let c = ref_lv.code(fn_start, ext).expect("code for a function start");
|
||||
let ext = ref_lv
|
||||
.extent(fn_start)
|
||||
.expect("extent for a function start");
|
||||
let c = ref_lv
|
||||
.code(fn_start, ext)
|
||||
.expect("code for a function start");
|
||||
let masked = mask(c);
|
||||
if masked.len() >= 4 {
|
||||
ref_fn = fn_start;
|
||||
@@ -639,7 +662,10 @@ mod tests {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(!m.is_empty(), "no function with masked extent >= 4 in level 0 .text");
|
||||
assert!(
|
||||
!m.is_empty(),
|
||||
"no function with masked extent >= 4 in level 0 .text"
|
||||
);
|
||||
eprintln!("ref_fn 0x{ref_fn:x} masked to {} words", m.len());
|
||||
// On every other overlay, its relocated copy must be found exactly once.
|
||||
for (i, target) in overlays.iter().enumerate().skip(1) {
|
||||
|
||||
Reference in New Issue
Block a user