SBlk head characterised: four constants and one word that is not a count

sound_bank's head was parsed but never characterised. Across all 27 levels the
SBlk region opens identically for its first twelve bytes:

    +0x00 "SBlk"   +0x04 1   +0x08 4   +0x0c "DAW\0"   +0x10 0

and +0x14 is the only word that varies. It varies with its low sixteen bits zero
on all 27, leaving 0x0092..0x0103 -- 146 to 259.

That is the shape of a record count, and it tracks the body length at close to
100 bytes per unit. It is not one:

    body / field_14     91.2 .. 110.8 across 27 levels
    any stride 0..80 with body = stride*word + constant exactly:  no

So the test asserts the negative too. If a stride ever makes the relation exact
on all 27, sound_bank_sblk_head_is_constant_apart_from_one_word fails and asks
for the record size to be revisited. A near-miss at 100 bytes/record is exactly
what hardens into a name the next time somebody glances at it; the constants are
also checked individually so a real deviation names which one moved.

The DAW region begins with sixteen zero bytes then block-structured data with
16-byte periodicity and slowly varying nibbles -- consistent with block
compressed audio, not decoded. Recorded as such.

Two mistakes, both mine, both the same shape as earlier ones:

1. The constructor bound `format_tag` with a `let` BEFORE the two words that
   precede it in the file, consuming bytes 4..8 and shifting every field after
   it. The struct literal listed the names in the right order; the statements did
   not. A scratch script reading the same bytes gave the opposite answer, which
   is what exposed it. Fixed, with a comment, because "read the lets in file
   order" gets undone otherwise.

2. Inserting the new test ahead of an existing one stranded that test's #[test]
   on my doc comment -- silently unregistering
   tie_vertex_element_is_16_bytes_with_fixed_texcoords. This is the SAME mistake
   as two commits ago and I made it again. It again surfaced only as clippy's
   "never used" rather than as a missing test. Restored and verified by listing
   the registered tests rather than trusting the count.

240 tests, clippy clean, both tie vertex tests confirmed present.
This commit is contained in:
2026-10-13 07:29:07 +02:00
parent bc5ebe559f
commit 28ab7e094b
4 changed files with 195 additions and 1 deletions
+80
View File
@@ -3484,6 +3484,86 @@ fn tie_vertex_slots_0_to_2_are_shared_positions() {
);
}
/// The `SBlk` head's constant fields, and the one field that is not a count.
///
/// The first twelve bytes are identical on all 27 levels. The word at `+0x14`
/// varies, with its low sixteen bits always zero. It tracks the body length at
/// ~100 bytes per unit, which is the shape of a record count -- and is not one:
/// the ratio runs 91.2..110.8 and no stride makes the relation exact on all 27.
/// Both halves are asserted so the near-miss cannot quietly harden into a name.
#[test]
fn sound_bank_sblk_head_is_constant_apart_from_one_word() {
require_disc!();
let data = std::env::var_os("GC_EXTRACTED")
.map(PathBuf::from)
.unwrap_or_else(|| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join("Documents/gc-extracted")
});
if !data.exists() {
eprintln!("skipped: unpack the disc first");
return;
}
use gc_formats::sound_bank::{parse_sound_bank, SBlkHeader};
let mut values: Vec<u32> = Vec::new();
let mut bodies: Vec<usize> = Vec::new();
let mut levels = 0usize;
for lv in 0..27usize {
let Ok(bytes) = std::fs::read(data.join(format!("level{lv}/sound_bank.bin"))) else {
continue;
};
let bank = parse_sound_bank(&bytes).expect("sound bank");
let h = SBlkHeader::parse(bank.sblk).expect("SBlk head");
assert!(
h.constants_hold(),
"level {lv}: SBlk constants moved: {h:?}"
);
values.push(h.field_14);
bodies.push(bank.sblk.len() - 24);
levels += 1;
}
assert_eq!(levels, 27, "only {levels} sound banks parsed");
for &v in &values {
assert_eq!(v & 0xffff, 0, "field_14 gained low bits: {v:#x}");
assert!(
(0x92..=0x103).contains(&(v >> 16)),
"field_14 moved: {v:#x}"
);
}
// correlated with body length, but not a count at any fixed stride
let ratios: Vec<f64> = bodies
.iter()
.zip(&values)
.map(|(b, &v)| *b as f64 / (v >> 16) as f64)
.collect();
let lo = ratios.iter().cloned().fold(f64::MAX, f64::min);
let hi = ratios.iter().cloned().fold(0.0f64, f64::max);
let exact = (0..=80).any(|k| {
let rems: Vec<i64> = bodies
.iter()
.zip(&values)
.map(|(b, &v)| *b as i64 - k * (v >> 16) as i64)
.collect();
rems.windows(2).all(|w| w[0] == w[1])
});
eprintln!(
"SBlk head: constants hold on 27/27; field_14 in 0x0092..0x0103 with low 16 bits 0; \
body/word ratio {lo:.1}..{hi:.1}; any stride making it exact: {exact}"
);
assert!(
lo > 85.0 && hi < 115.0,
"body/word ratio left its band: {lo:.1}..{hi:.1}"
);
assert!(
!exact,
"field_14 now satisfies a fixed stride -- revisit the record size"
);
}
#[test]
fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() {
require_disc!();
+68
View File
@@ -68,6 +68,74 @@ impl SoundBankHeader {
}
}
/// The 24-byte head that opens the `SBlk` region.
///
/// Measured across all 27 levels: the first twelve bytes are **identical
/// everywhere** -- `"SBlk"`, then `1`, then `4`, then `"DAW\0"` -- and the word
/// at `+0x10` is zero on every level. The word at `+0x14` is the only part that
/// varies, and it varies per level with its **low sixteen bits zero in all 27**,
/// leaving values `0x0092..=0x0103` (146..=259).
///
/// That word tracks the length of the `SBlk` body at close to 100 bytes per
/// unit, but it is **not** a record count with a fixed stride: the ratio runs
/// 91.2 to 110.8 across the 27 levels, and no stride in 0..80 makes
/// `body = stride * word + constant` hold exactly on all of them. So it is
/// correlated with the body length and is not identified. Recorded rather than
/// named, because "a count that nearly works" is exactly the claim that later
/// gets asserted by someone who did not run this test.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SBlkHeader {
/// Always `"SBlk"`.
pub magic: [u8; 4],
/// Always `1`.
pub field_4: u32,
/// Always `4`.
pub field_8: u32,
/// Always `"DAW\0"`.
pub format_tag: [u8; 4],
/// Always `0`.
pub field_10: u32,
/// Varies per level; low sixteen bits always zero, `0x0092..=0x0103`.
pub field_14: u32,
}
impl SBlkHeader {
/// Parse the 24-byte head.
pub fn parse(bytes: &[u8]) -> Result<SBlkHeader> {
// Read in byte order. The struct literal names `format_tag` after two
// words, but a `let` bound for it would consume bytes 4..8 and shift
// every field after it -- which it did, once.
let mut b = Buf::new(bytes);
let magic: [u8; 4] = b.read_bytes(4, "SBlk magic")?.try_into().expect("4 bytes");
let field_4 = b.u32("SBlk field_4")?;
let field_8 = b.u32("SBlk field_8")?;
let format_tag: [u8; 4] = b
.read_bytes(4, "SBlk format tag")?
.try_into()
.expect("4 bytes");
Ok(SBlkHeader {
magic,
field_4,
field_8,
format_tag,
field_10: b.u32("SBlk field_10")?,
field_14: b.u32("SBlk field_14")?,
})
}
/// True when every constant this header carries holds.
///
/// Each is universal on all 27 levels. They are tested individually so a
/// real deviation says which one moved.
pub fn constants_hold(&self) -> bool {
&self.magic == b"SBlk"
&& self.field_4 == 1
&& self.field_8 == 4
&& &self.format_tag == b"DAW\0"
&& self.field_10 == 0
}
}
/// Split a sound extent into its two regions.
///
/// Bounds are checked before slicing; a level whose head points outside its own
+42
View File
@@ -2142,3 +2142,45 @@ field means anything. The test's power came from `i16[0..2]` being the only slot
with tens of thousands of distinct values. **A passing result from a method that
has run out of discriminative power is not a passing result**, and the 1x rows are
recorded so the table is not later mistaken for nine confirmations.
## The `SBlk` head: four constants, one unidentified word
The sound extent's 24-byte head was parsed but not characterised. Across all 27
levels the `SBlk` region opens identically for its first twelve bytes:
+0x00 "SBlk"
+0x04 1
+0x08 4
+0x0c "DAW\0"
+0x10 0
+0x14 varies
`+0x14` is the only part that differs between levels, and it differs with its
**low sixteen bits zero on all 27**, leaving `0x0092..=0x0103` -- 146 to 259.
That is the shape of a record count, and it tracks the `SBlk` body length at
close to 100 bytes per unit. It is not one:
body / field_14 91.2 .. 110.8 across the 27 levels
any stride 0..80 making body = stride * word + constant exactly: no
A near-miss at 100 bytes/record is exactly the kind of thing that hardens into a
name the next time somebody glances at it, so the test asserts the negative
alongside the positive: if a stride ever makes the relation exact on all 27, the
test fails and asks for the record size to be revisited.
Asserted in `sound_bank_sblk_head_is_constant_apart_from_one_word`, which checks
the four constants individually so a real deviation names which moved, the zero
low half, the value range, and the absence of a fixed stride.
The `DAW` region begins with sixteen zero bytes and then block-structured data
with 16-byte periodicity and slowly varying nibbles, consistent with block
compressed audio. It is not decoded.
Two notes on how this was measured. Reading the level 0 head directly from a
scratch script gave a field order that the parser disagreed with, because the
constructor bound `format_tag` with a `let` *before* the two words that precede
it in the file -- consuming bytes 4..8 and shifting everything after. The struct
literal named the fields in the right order; the statements did not. It is fixed
with a comment saying so, since "read the `let`s in file order" is the kind of
thing that gets undone.
+5 -1
View File
@@ -228,7 +228,11 @@ Phase 1 hands over the extents; this phase works out what each one contains.
with the WAD LZ77 decoder, bit order and header length ruled out" note
was wrong about the cause: the decoder was right and the input was not
what it looked like. Corrected in `plan/level-assets.md`.
- [~] `sound_bank` — container framing parsed (`SoundBankHeader::parse`,
- [~] `sound_bank` — the `SBlk` head is now characterised: `"SBlk", 1, 4,
"DAW\0", 0` constant on 27/27, and `+0x14` is the only varying word, with
its low 16 bits always zero (`0x0092..=0x0103`). It tracks body length at
~100 bytes/unit and is **not** a count -- ratio 91.2..110.8, and no stride
makes it exact. The absence of a stride is asserted too. Container framing parsed (`SoundBankHeader::parse`,
`parse_sound_bank`); `SBlk` and `DAW` bounds verified on 27/27. The
**record fields inside `SBlk` are not decoded**, and `DAW` shows no
detectable stride, consistent with ADPCM. See the note under