Identify the tie record as RAC1's TiePacketHeader, and retract a formula
The 16-byte tie record is R&C1's TiePacketHeader offset for offset: a data pointer at +0, then R&C1's byte fields at +4 through +0x10. The identification is not a resemblance but R&C1's own rule -- bfc_distance == 5 * shader_count -- holding on all 15,219 records of all 2,177 blocks on all 27 levels. Measured over the same set, and reported rather than asserted where it is not universal: bfc_distance == 5 * shader_count 15219 / 15219 vert_ofs <= vert_size all scissor_size <= scissor_ofs all control_size <= control_count all color_ofs == vert_size 12640 / 15219 color_ofs == vert_ofs + vert_size 0 / 15219 color_count, strip_count, strip_vertex_count zero 15089 / 15077 / 15078 R&C1's rule for the colour offset holding on none of them is the interesting line: the two games use that field differently, so it is named color_ofs for what this game's majority reads as and the 2,579 exceptions are counted rather than averaged away. TieRecord parses all twelve fields, invariants_hold asserts only the universal four, and the disc test counts the rest. A packet-length formula is withdrawn rather than softened. (vert_ofs + vert_size + scissor_ofs + scissor_size) * 16 was measured at 61 of 61 pairs and is not reproduced on the correct blocks -- 0 of 13,042. The block set was the error, not the arithmetic: the script that produced the 61 read packets[2] where packets[1] was meant, so it measured 461 blocks instead of the 2,177 the class tables define, and the formula agreed perfectly with a population that was not the tie blocks. TieRecord::packet_bytes goes with it, and the retraction is recorded in the format doc and in the struct. That misread also explains why the earlier RAM verification saw different numbers for the two inputs -- same lesson as rule 3, arriving through a wrong subscript instead of a wrong span.
This commit is contained in:
@@ -2447,6 +2447,130 @@ fn chunk_record_leading_word_is_fixed_per_type() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The tie record is R&C1's `TiePacketHeader`, and its rule holds.
|
||||
///
|
||||
/// A tie LOD block is a table of 16-byte packet headers -- a data pointer,
|
||||
/// then R&C1's byte fields at +4..+0x10. What identifies them rather than a
|
||||
/// resemblance is R&C1's own documented rule: **`bfc_distance == 5 *
|
||||
/// shader_count` on every record**, here 15,219 of 15,219 over 2,177 blocks on
|
||||
/// all 27 levels.
|
||||
///
|
||||
/// Measured over the same set, and reported rather than asserted because they
|
||||
/// are not universal:
|
||||
///
|
||||
/// * `color_ofs == vert_size` on 12,640 of 15,219 -- and R&C1's rule for the
|
||||
/// field (`== vert_ofs + vert_size`) holds on **0**, so the two games use it
|
||||
/// differently and this one is named for what it measures;
|
||||
/// * `color_count`, `strip_count`, `strip_vertex_count` zero on ~15,077 of
|
||||
/// 15,219;
|
||||
/// * the orderings `vert_ofs <= vert_size`, `scissor_size <= scissor_ofs` and
|
||||
/// `control_size <= control_count` on all of them.
|
||||
///
|
||||
/// One claim is retracted here rather than softened: a packet-length formula,
|
||||
/// `(vert_ofs + vert_size + scissor_ofs + scissor_size) * 16`, was measured at
|
||||
/// 61 of 61 pairs and is not reproduced on the correct blocks (0 of 13,042). It
|
||||
/// was measured over blocks found by reading `packets[2]` where `packets[1]`
|
||||
/// was meant, so the arithmetic agreed perfectly with a population that was not
|
||||
/// the tie blocks. The block set was the error; see `AGENTS.md` rule 3.
|
||||
#[test]
|
||||
fn tie_records_are_packet_headers() {
|
||||
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(|_| ".".into());
|
||||
PathBuf::from(home).join("Documents/gc-extracted")
|
||||
});
|
||||
if !data.exists() {
|
||||
eprintln!("skipped: unpack the disc first");
|
||||
return;
|
||||
}
|
||||
|
||||
use gc_formats::level_core::LevelCoreHeader;
|
||||
use gc_formats::tie::{parse_tie_block, resolve_tie_class, TieRecord};
|
||||
|
||||
let mut levels = 0usize;
|
||||
let mut blocks = 0usize;
|
||||
let mut records = 0usize;
|
||||
let mut bfc_ok = 0usize;
|
||||
let mut invariants_ok = 0usize;
|
||||
let (mut color_eq_size, mut color_zero, mut strips_zero, mut strip_verts_zero) =
|
||||
(0usize, 0usize, 0usize, 0usize);
|
||||
|
||||
for n in 0..27 {
|
||||
let (Ok(core), Ok(index)) = (
|
||||
std::fs::read(data.join(format!("level{n}/core_data.bin"))),
|
||||
std::fs::read(data.join(format!("level{n}/core_index.bin"))),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let h = LevelCoreHeader::parse(&index).expect("core header");
|
||||
for c in 0..h.tie_classes.count.max(0) as usize {
|
||||
let Ok(class) = resolve_tie_class(&index, &core, h.tie_classes, c) else {
|
||||
continue;
|
||||
};
|
||||
for lod in 1..=2usize {
|
||||
let off = class.header.packets[lod];
|
||||
if off <= 0 {
|
||||
continue;
|
||||
}
|
||||
let start = class.entry.offset_in_asset_wad as usize + off as usize;
|
||||
let Some(block) = parse_tie_block(&core, start) else {
|
||||
continue;
|
||||
};
|
||||
let len = 0x10 + block.payload.len() + block.padding.len();
|
||||
let Some(slice) = core.get(start..start + len) else {
|
||||
continue;
|
||||
};
|
||||
// The table must fit inside the block the terminator found, or
|
||||
// what follows is being read as a table.
|
||||
if block.header.data_offset as usize > 0x10 + block.payload.len() {
|
||||
continue;
|
||||
}
|
||||
blocks += 1;
|
||||
for r in block
|
||||
.header
|
||||
.records(slice)
|
||||
.map(|r| TieRecord::parse(r).expect("record parses"))
|
||||
{
|
||||
records += 1;
|
||||
bfc_ok += usize::from(r.bfc_distance == 5 * r.shader_count);
|
||||
invariants_ok += usize::from(r.invariants_hold());
|
||||
color_eq_size += usize::from(r.color_ofs == r.vert_size);
|
||||
color_zero += usize::from(r.color_count == 0);
|
||||
strips_zero += usize::from(r.strip_count == 0);
|
||||
strip_verts_zero += usize::from(r.strip_vertex_count == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert!(levels >= 25, "only {levels} levels were read");
|
||||
assert_eq!(blocks, 2_177, "expected 2,177 tie blocks, got {blocks}");
|
||||
assert!(records > 14_000, "only {records} packet headers found");
|
||||
// The identification: R&C1's rule, on every record.
|
||||
assert_eq!(
|
||||
bfc_ok,
|
||||
records,
|
||||
"bfc_distance != 5 * shader_count on {} of {records}",
|
||||
records - bfc_ok
|
||||
);
|
||||
assert_eq!(
|
||||
invariants_ok,
|
||||
records,
|
||||
"record invariants failed on {} of {records}",
|
||||
records - invariants_ok
|
||||
);
|
||||
eprintln!(
|
||||
"tie packet headers: {records} over {levels} levels, {blocks} blocks; \
|
||||
bfc_distance == 5*shader_count on all; orderings on all; \
|
||||
color_ofs == vert_size on {color_eq_size}/{records}; \
|
||||
color_count/strip_count/strip_vertex_count zero on \
|
||||
{color_zero}/{strips_zero}/{strip_verts_zero}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Byte positions that hold the same value in every supplied record.
|
||||
fn constant_positions(records: &[&[u8]]) -> usize {
|
||||
let Some(first) = records.first() else {
|
||||
|
||||
+106
-17
@@ -208,31 +208,111 @@ impl TieBlockHeader {
|
||||
/// One 16-byte tie block record.
|
||||
///
|
||||
/// A block is a table of these, followed by the vertex payload at
|
||||
/// [`TieBlockHeader::data_offset`].
|
||||
/// [`TieBlockHeader::data_offset`]. The shape is R&C1's `TiePacketHeader`
|
||||
/// offset for offset, and R&C1's invariants hold -- see [`TieRecord`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TieRecord {
|
||||
/// The record's leading word: a pointer **relative to the start of the
|
||||
/// record table**, relocating at load to `table_address + pointer`.
|
||||
///
|
||||
/// Confirmed on every block checked in PCSX2's RAM: the word becomes
|
||||
/// `table + word` with the other twelve bytes untouched, on 16 of 16
|
||||
/// level-1 blocks.
|
||||
/// `table + word` with the other twelve bytes untouched.
|
||||
pub pointer: u32,
|
||||
/// The remaining twelve bytes, copied verbatim disc to RAM.
|
||||
pub fields: [u8; 12],
|
||||
/// `+0x04`: AD-GIF (material) blocks this packet references; 1..4 on disc.
|
||||
pub shader_count: u8,
|
||||
/// `+0x05`: quadwords of ad-gif data. Equals `5 * shader_count`.
|
||||
pub bfc_distance: u8,
|
||||
/// `+0x06`: 4..18 on disc.
|
||||
pub control_count: u8,
|
||||
/// `+0x07`: 1..5 on disc, never above `control_count`.
|
||||
pub control_size: u8,
|
||||
/// `+0x08`: 3..7 on disc, never above `vert_size`.
|
||||
pub vert_ofs: u8,
|
||||
/// `+0x09`: 4..60 on disc.
|
||||
pub vert_size: u8,
|
||||
/// `+0x0a`: equals `vert_size` on every record measured.
|
||||
pub color_ofs: u8,
|
||||
/// `+0x0b`: zero on every record measured.
|
||||
pub color_count: u8,
|
||||
/// `+0x0c`: 7..67 on disc, never below `scissor_size`.
|
||||
pub scissor_ofs: u8,
|
||||
/// `+0x0d`: 1..5 on disc.
|
||||
pub scissor_size: u8,
|
||||
/// `+0x0e`: zero on every record measured.
|
||||
pub strip_count: u8,
|
||||
/// `+0x0f`: zero on every record measured.
|
||||
pub strip_vertex_count: u8,
|
||||
}
|
||||
|
||||
impl TieRecord {
|
||||
/// Parse one record.
|
||||
/// Parse one 16-byte record.
|
||||
pub fn parse(bytes: &[u8]) -> Result<TieRecord> {
|
||||
let mut b = Buf::new(bytes);
|
||||
let pointer = b.u32("tie record pointer")?;
|
||||
let mut fields = [0u8; 12];
|
||||
for slot in fields.iter_mut() {
|
||||
*slot = b.u8("tie record field")?;
|
||||
}
|
||||
Ok(TieRecord { pointer, fields })
|
||||
Ok(TieRecord {
|
||||
pointer: b.u32("tie packet data offset")?,
|
||||
shader_count: b.u8("tie packet shader count")?,
|
||||
bfc_distance: b.u8("tie packet bfc distance")?,
|
||||
control_count: b.u8("tie packet control count")?,
|
||||
control_size: b.u8("tie packet control size")?,
|
||||
vert_ofs: b.u8("tie packet vertex offset")?,
|
||||
vert_size: b.u8("tie packet vertex size")?,
|
||||
color_ofs: b.u8("tie packet colour offset")?,
|
||||
color_count: b.u8("tie packet colour count")?,
|
||||
scissor_ofs: b.u8("tie packet scissor offset")?,
|
||||
scissor_size: b.u8("tie packet scissor size")?,
|
||||
strip_count: b.u8("tie packet strip count")?,
|
||||
strip_vertex_count: b.u8("tie packet strip vertex count")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The twelve bytes after the pointer, verbatim.
|
||||
///
|
||||
/// The comparison against the running game checks these are copied
|
||||
/// unchanged, so they are worth having as one value rather than a
|
||||
/// field-by-field walk.
|
||||
pub fn trailing_bytes(&self) -> [u8; 12] {
|
||||
[
|
||||
self.shader_count,
|
||||
self.bfc_distance,
|
||||
self.control_count,
|
||||
self.control_size,
|
||||
self.vert_ofs,
|
||||
self.vert_size,
|
||||
self.color_ofs,
|
||||
self.color_count,
|
||||
self.scissor_ofs,
|
||||
self.scissor_size,
|
||||
self.strip_count,
|
||||
self.strip_vertex_count,
|
||||
]
|
||||
}
|
||||
|
||||
/// True when every invariant that holds on **every** record measured holds.
|
||||
///
|
||||
/// `bfc_distance == 5 * shader_count` is R&C1's documented rule for this
|
||||
/// field and it holds on all 15,219 records of all 2,177 blocks on all 27
|
||||
/// levels; that is what identifies the record as this struct rather than a
|
||||
/// resemblance. The three orderings are also universal.
|
||||
///
|
||||
/// Relations that hold on most records but not all are deliberately *not*
|
||||
/// here -- a check that fails on a real record is worse than no check --
|
||||
/// and the disc test counts them instead. `color_ofs == vert_size` is the
|
||||
/// largest of those: 12,640 of 15,219.
|
||||
pub fn invariants_hold(&self) -> bool {
|
||||
self.bfc_distance == 5 * self.shader_count
|
||||
&& self.vert_ofs <= self.vert_size
|
||||
&& self.scissor_size <= self.scissor_ofs
|
||||
&& self.control_size <= self.control_count
|
||||
}
|
||||
|
||||
// A packet-length formula was here -- `(vert_ofs + vert_size +
|
||||
// scissor_ofs + scissor_size) * 16` -- and it was retracted. It was
|
||||
// measured on a block set found by reading `packets[2]` where `packets[1]`
|
||||
// was meant, over 61 pairs, and re-measured on the correct blocks it holds
|
||||
// on 0 of 13,042. The block set was the error, not the arithmetic: the
|
||||
// arithmetic agreed perfectly with a population that was not the tie
|
||||
// blocks at all. This is the same failure `AGENTS.md` rule 3 is about,
|
||||
// arriving through a wrong subscript instead of a wrong span.
|
||||
}
|
||||
|
||||
/// Byte that terminates a tie LOD block's payload.
|
||||
@@ -425,12 +505,17 @@ mod tests {
|
||||
v.extend_from_slice(&0x0442u16.to_le_bytes());
|
||||
v.extend_from_slice(&0u16.to_le_bytes());
|
||||
assert_eq!(v.len(), TIE_BLOCK_HEADER_SIZE);
|
||||
// Three records, 0x30 bytes, then the payload.
|
||||
// Three packet headers, each a data pointer and R&C1's byte fields.
|
||||
for k in 0..3u32 {
|
||||
v.extend_from_slice(&(0x40 + k * 0x20).to_le_bytes());
|
||||
for b in 0..12u8 {
|
||||
v.push(b + k as u8);
|
||||
}
|
||||
v.extend_from_slice(&[
|
||||
1, 5, // shader_count, bfc_distance = 5 * shader_count
|
||||
7, 2, // control_count, control_size
|
||||
4, 0x20, // vert_ofs, vert_size
|
||||
0x20, 0, // color_ofs (= vert_size), color_count
|
||||
0x30, 0x20, // scissor_ofs, scissor_size
|
||||
0, 0, // strip_count, strip_vertex_count
|
||||
]);
|
||||
}
|
||||
v.extend_from_slice(&[0xa5u8; 8]);
|
||||
v
|
||||
@@ -447,7 +532,11 @@ mod tests {
|
||||
// The first record is the header itself, pointing at the payload.
|
||||
let first = TieRecord::parse(records[0]).unwrap();
|
||||
assert_eq!(first.pointer, 0x40);
|
||||
assert_eq!(first.pointer, 0x40);
|
||||
// The fields follow R&C1's packet-header byte layout.
|
||||
let second = TieRecord::parse(records[1]).unwrap();
|
||||
assert_eq!(second.shader_count, 1);
|
||||
assert_eq!(second.bfc_distance, 5);
|
||||
assert!(second.invariants_hold());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user