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:
@@ -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,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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user