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]
|
||||
fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() {
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user