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] #[test]
fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() { fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() {
require_disc!(); 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. /// Split a sound extent into its two regions.
/// ///
/// Bounds are checked before slicing; a level whose head points outside its own /// 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 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 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. 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 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 was wrong about the cause: the decoder was right and the input was not
what it looked like. Corrected in `plan/level-assets.md`. 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 `parse_sound_bank`); `SBlk` and `DAW` bounds verified on 27/27. The
**record fields inside `SBlk` are not decoded**, and `DAW` shows no **record fields inside `SBlk` are not decoded**, and `DAW` shows no
detectable stride, consistent with ADPCM. See the note under detectable stride, consistent with ADPCM. See the note under