Parse tfrag terrain blocks and decode all vertex positions
Adds gc-formats::tfrag and a gated real-disc test that decodes every terrain patch on all 27 levels. Two rules decide whether this works at all, and both were wrong on the first attempt: The vector format is encoded in the VIF command byte, not the immediate. Bits 2-3 are vn (component count minus one) and bits 0-1 are vl, giving element_size = ((32 >> vl) * (vn + 1)) / 8. Terrain positions are vn=2, vl=1 (command 0x69) at 6 bytes. Reading the format from the immediate's low nibble -- the more commonly quoted layout -- yields 12 or 16 bytes and misaligns everything after it. Unpack payloads pad to 4 bytes, and several non-unpack commands carry inline data: STROW/STCOL 16, STMASK 4, MPG num*8, DIRECT/DIRECTHL imm*16. Ignoring those desynchronises the walk, and a desynchronised walk still finds code words that look like positions. Positions are also not all in one list. vert_count is the total across three tiers, the LOD-2 list precedes the common one in offset order, and the LOD-0-only list starts at shared_ofs + lod_1_size*16 rather than at an offset of its own. Scanning only the common list matches about a quarter of patches, each exactly -- which is what made it convincing. The invariant is that every patch yields exactly its own vert_count. It caught both errors, so tfrag_positions returns None on a mismatch rather than partial geometry. All 27 levels: 18,102 patches, 821,439 vertices, zero mismatches. Still open: strips, triangle indices and the RGBA vertex-info arrays. Positions alone are not drawable.
This commit is contained in:
@@ -3,4 +3,5 @@ pub mod global_wad;
|
||||
pub mod iso9660;
|
||||
pub mod level_data;
|
||||
pub mod level_wad;
|
||||
pub mod tfrag;
|
||||
pub mod wad;
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
//! Terrain fragments: the `tfrags` block of a level's geometry.
|
||||
//!
|
||||
//! Spec: `docs/formats/tfrag.md`.
|
||||
//!
|
||||
//! A level's terrain is a set of **tfrags** — small patches of ground, each
|
||||
//! with its own bounding sphere, its own vertex set and its own index strips,
|
||||
//! organised into three levels of detail. The block is a directory followed by
|
||||
//! one data block per tfrag.
|
||||
//!
|
||||
//! # Block layout
|
||||
//!
|
||||
//! ```text
|
||||
//! +0x00 4 byte offset of the header table, from the start of the block
|
||||
//! +0x04 4 number of tfrags
|
||||
//! +0x08 4 float, LOD distance parameter
|
||||
//! +0x10 .. padding, zero, up to the table
|
||||
//! +table_offset TfragHeader[tfrag_count], 0x40 bytes each
|
||||
//! tfrag 0's data block
|
||||
//! tfrag 1's data block
|
||||
//! ...
|
||||
//! ```
|
||||
//!
|
||||
//! A tfrag header's `data` field is a byte offset **relative to the start of the
|
||||
//! header table**, not to the start of the block. Every other offset in the
|
||||
//! record is relative to that tfrag's own data block. Two different bases in one
|
||||
//! record is the sort of detail that produces silently wrong geometry.
|
||||
//!
|
||||
//! # What is recovered here
|
||||
//!
|
||||
//! Vertex **positions**: a V3_16 signed unpack inside the tfrag's VIF command
|
||||
//! list, giving 6 bytes per vertex as three `s16` world coordinates.
|
||||
//!
|
||||
//! Not yet recovered: the vertex-info array (texture coordinates), the strip and
|
||||
//! index arrays that define the triangles, and the ad-gif texture bindings. See
|
||||
//! `docs/plan/tfrag.md`.
|
||||
|
||||
use crate::buf::{Buf, Error, ErrorKind, Result};
|
||||
|
||||
/// Size of one tfrag header record.
|
||||
pub const TFRAG_HEADER_SIZE: usize = 0x40;
|
||||
/// Size of the block header before the padding.
|
||||
pub const TFRAGS_BLOCK_HEADER_SIZE: usize = 0x10;
|
||||
|
||||
/// The header at the start of a tfrags block.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TfragsBlock {
|
||||
/// Byte offset of the header table, relative to the start of the block.
|
||||
pub table_offset: u32,
|
||||
pub tfrag_count: u32,
|
||||
/// LOD distance parameter. Preserved; semantics not confirmed.
|
||||
pub lod_distance: f32,
|
||||
/// The second unnamed word. Preserved.
|
||||
pub reserved: u32,
|
||||
}
|
||||
|
||||
impl TfragsBlock {
|
||||
/// Parse the block header from the start of the tfrags region.
|
||||
pub fn parse(bytes: &[u8]) -> Result<TfragsBlock> {
|
||||
let mut b = Buf::new(bytes);
|
||||
let table_offset = b.u32("tfrags table offset")?;
|
||||
let tfrag_count = b.u32("tfrag count")?;
|
||||
let lod_distance = b.f32("tfrags LOD distance")?;
|
||||
let reserved = b.u32("tfrags reserved word")?;
|
||||
Ok(TfragsBlock {
|
||||
table_offset,
|
||||
tfrag_count,
|
||||
lod_distance,
|
||||
reserved,
|
||||
})
|
||||
}
|
||||
|
||||
/// The tfrag header at index `i`, given the whole block.
|
||||
pub fn header(&self, bytes: &[u8], i: usize) -> Result<TfragHeader> {
|
||||
let off = self.table_offset as usize + i * TFRAG_HEADER_SIZE;
|
||||
if off + TFRAG_HEADER_SIZE > bytes.len() {
|
||||
return Err(Error {
|
||||
offset: off,
|
||||
context: "tfrag header table",
|
||||
kind: ErrorKind::OutOfBounds {
|
||||
needed: TFRAG_HEADER_SIZE,
|
||||
available: bytes.len().saturating_sub(off),
|
||||
},
|
||||
});
|
||||
}
|
||||
TfragHeader::parse(&bytes[off..off + TFRAG_HEADER_SIZE])
|
||||
}
|
||||
|
||||
/// Every tfrag header in the block.
|
||||
pub fn headers(&self, bytes: &[u8]) -> Result<Vec<TfragHeader>> {
|
||||
(0..self.tfrag_count as usize)
|
||||
.map(|i| self.header(bytes, i))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// One tfrag's header record.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TfragHeader {
|
||||
/// Bounding sphere: centre xyz and radius w. The centre is a VU1 memory
|
||||
/// address on this game rather than a world position, so it is not usable
|
||||
/// as a world-space bound without further work.
|
||||
pub bsphere: [f32; 4],
|
||||
/// Byte offset of this tfrag's data block, **relative to the table**.
|
||||
pub data: i32,
|
||||
pub lod_2_ofs: u16,
|
||||
pub shared_ofs: u16,
|
||||
pub lod_1_ofs: u16,
|
||||
pub lod_0_ofs: u16,
|
||||
/// Offset of the ad-gif texture payload inside the common VIF list.
|
||||
pub tex_ofs: u16,
|
||||
/// Offset of the per-vertex RGBA array.
|
||||
pub rgba_ofs: u16,
|
||||
pub common_size: u8,
|
||||
pub lod_2_size: u8,
|
||||
pub lod_1_size: u8,
|
||||
pub lod_0_size: u8,
|
||||
pub texture_count: u8,
|
||||
pub rgba_size: u8,
|
||||
pub msphere_ofs: u16,
|
||||
pub light_ofs: u16,
|
||||
pub light_end_ofs: u16,
|
||||
pub cube_ofs: u16,
|
||||
/// Occlusion / visibility index for this tfrag.
|
||||
pub occl_index: u16,
|
||||
/// Total vertex count. `u8`, so at most 255 positions per tfrag.
|
||||
pub vert_count: u8,
|
||||
/// Triangle count at LOD 0.
|
||||
pub tri_count: u8,
|
||||
}
|
||||
|
||||
impl TfragHeader {
|
||||
/// Parse one 0x40-byte header record.
|
||||
pub fn parse(bytes: &[u8]) -> Result<TfragHeader> {
|
||||
let mut b = Buf::new(bytes);
|
||||
let mut bsphere = [0f32; 4];
|
||||
for slot in bsphere.iter_mut() {
|
||||
*slot = b.f32("tfrag bounding sphere")?;
|
||||
}
|
||||
let data = b.i32("tfrag data offset")?;
|
||||
let lod_2_ofs = b.u16("tfrag lod_2_ofs")?;
|
||||
let shared_ofs = b.u16("tfrag shared_ofs")?;
|
||||
let lod_1_ofs = b.u16("tfrag lod_1_ofs")?;
|
||||
let lod_0_ofs = b.u16("tfrag lod_0_ofs")?;
|
||||
let tex_ofs = b.u16("tfrag tex_ofs")?;
|
||||
let rgba_ofs = b.u16("tfrag rgba_ofs")?;
|
||||
// 0x20..0x2d are single bytes; 0x2d..0x2e then a u16, so read in order.
|
||||
let common_size = b.u8("tfrag common_size")?;
|
||||
let lod_2_size = b.u8("tfrag lod_2_size")?;
|
||||
let lod_1_size = b.u8("tfrag lod_1_size")?;
|
||||
let lod_0_size = b.u8("tfrag lod_0_size")?;
|
||||
let _lod_2_rgba = b.u8("tfrag lod_2_rgba_count")?;
|
||||
let _lod_1_rgba = b.u8("tfrag lod_1_rgba_count")?;
|
||||
let _lod_0_rgba = b.u8("tfrag lod_0_rgba_count")?;
|
||||
let _base_only = b.u8("tfrag base_only")?;
|
||||
let texture_count = b.u8("tfrag texture_count")?;
|
||||
let rgba_size = b.u8("tfrag rgba_size")?;
|
||||
let _rgba_verts_loc = b.u8("tfrag rgba_verts_loc")?;
|
||||
let _occl_index_stash = b.u8("tfrag occl_index_stash")?;
|
||||
let _msphere_count = b.u8("tfrag msphere_count")?;
|
||||
let _flags = b.u8("tfrag flags")?;
|
||||
let msphere_ofs = b.u16("tfrag msphere_ofs")?;
|
||||
let light_ofs = b.u16("tfrag light_ofs")?;
|
||||
let light_end_ofs = b.u16("tfrag light_end_ofs")?;
|
||||
let _dir_lights_one = b.u8("tfrag dir_lights_one")?;
|
||||
let _dir_lights_upd = b.u8("tfrag dir_lights_upd")?;
|
||||
let _point_lights = b.u16("tfrag point_lights")?;
|
||||
let cube_ofs = b.u16("tfrag cube_ofs")?;
|
||||
let occl_index = b.u16("tfrag occl_index")?;
|
||||
let vert_count = b.u8("tfrag vert_count")?;
|
||||
let tri_count = b.u8("tfrag tri_count")?;
|
||||
let _mip_dist = b.u16("tfrag mip_dist")?;
|
||||
|
||||
Ok(TfragHeader {
|
||||
bsphere,
|
||||
data,
|
||||
lod_2_ofs,
|
||||
shared_ofs,
|
||||
lod_1_ofs,
|
||||
lod_0_ofs,
|
||||
tex_ofs,
|
||||
rgba_ofs,
|
||||
common_size,
|
||||
lod_2_size,
|
||||
lod_1_size,
|
||||
lod_0_size,
|
||||
texture_count,
|
||||
rgba_size,
|
||||
msphere_ofs,
|
||||
light_ofs,
|
||||
light_end_ofs,
|
||||
cube_ofs,
|
||||
occl_index,
|
||||
vert_count,
|
||||
tri_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A VIF code word decoded into its fields.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct VifCode {
|
||||
pub irq: bool,
|
||||
/// Command field, bits 30..24.
|
||||
pub cmd: u8,
|
||||
/// Payload word count, bits 23..16. Zero means 256.
|
||||
pub num: u16,
|
||||
/// Immediate, bits 15..0.
|
||||
pub imm: u16,
|
||||
}
|
||||
|
||||
impl VifCode {
|
||||
/// Re-encode into a code word. Used by tests to build packets.
|
||||
pub fn to_word(&self) -> u32 {
|
||||
((self.irq as u32) << 31)
|
||||
| ((self.cmd as u32) << 24)
|
||||
| ((self.num as u32) << 16)
|
||||
| self.imm as u32
|
||||
}
|
||||
|
||||
pub fn decode(word: u32) -> VifCode {
|
||||
VifCode {
|
||||
irq: (word >> 31) & 1 == 1,
|
||||
cmd: ((word >> 24) & 0x7F) as u8,
|
||||
num: ((word >> 16) & 0xFF) as u16,
|
||||
imm: (word & 0xFFFF) as u16,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this an UNPACK? Recognised by the top two bits of the command.
|
||||
pub fn is_unpack(&self) -> bool {
|
||||
(self.cmd & 0b110_0000) == 0b110_0000
|
||||
}
|
||||
|
||||
/// Number of components minus one: 0..3 = 1..4 components.
|
||||
pub fn vn(&self) -> u8 {
|
||||
(self.cmd >> 2) & 3
|
||||
}
|
||||
|
||||
/// Component width: 0 = 32-bit, 1 = 16-bit, 2 = 8-bit, 3 = 5-bit.
|
||||
pub fn vl(&self) -> u8 {
|
||||
self.cmd & 3
|
||||
}
|
||||
|
||||
/// Bytes per unpacked element on disk.
|
||||
///
|
||||
/// The whole vector format is encoded in the command byte: `vn` gives the
|
||||
/// component count and `vl` the component width. Nothing about the format
|
||||
/// lives in the immediate.
|
||||
pub fn element_size(&self) -> usize {
|
||||
((32usize >> self.vl()) * (self.vn() as usize + 1)) / 8
|
||||
}
|
||||
|
||||
/// The PS2 vector format name, for diagnostics.
|
||||
pub fn format_name(&self) -> String {
|
||||
const NAMES: [&str; 4] = ["V1", "V2", "V3", "V4"];
|
||||
const WIDTHS: [&str; 4] = ["32", "16", "8", "5"];
|
||||
if self.is_unpack() {
|
||||
format!(
|
||||
"{}_{}",
|
||||
NAMES[self.vn() as usize],
|
||||
WIDTHS[self.vl() as usize]
|
||||
)
|
||||
} else {
|
||||
"not an unpack".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective element count: a `num` of zero means 256.
|
||||
pub fn count(&self) -> usize {
|
||||
if self.num == 0 {
|
||||
256
|
||||
} else {
|
||||
self.num as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline payload size in bytes, not counting the 4-byte code word.
|
||||
///
|
||||
/// An unpack's payload is padded up to 4 bytes. Only a few non-unpack
|
||||
/// commands carry inline data; everything else is a bare code word. Getting
|
||||
/// this wrong desynchronises the walk and yields plausible garbage, so the
|
||||
/// set is enumerated rather than assumed empty.
|
||||
pub fn payload_size(&self) -> usize {
|
||||
if self.is_unpack() {
|
||||
(self.count() * self.element_size() + 3) & !3
|
||||
} else {
|
||||
match self.cmd {
|
||||
STMASK => 4,
|
||||
STROW | STCOL => 16,
|
||||
MPG => self.num as usize * 8,
|
||||
DIRECT | DIRECTHL => {
|
||||
if self.imm == 0 {
|
||||
65536 * 16
|
||||
} else {
|
||||
self.imm as usize * 16
|
||||
}
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Total packet size in bytes, including the 4-byte code word.
|
||||
pub fn packet_size(&self) -> usize {
|
||||
4 + self.payload_size()
|
||||
}
|
||||
}
|
||||
|
||||
/// VIF command bytes for non-unpack commands that carry inline data.
|
||||
pub const STMASK: u8 = 0x20;
|
||||
pub const STROW: u8 = 0x30;
|
||||
pub const STCOL: u8 = 0x31;
|
||||
pub const MPG: u8 = 0x4a;
|
||||
pub const DIRECT: u8 = 0x50;
|
||||
pub const DIRECTHL: u8 = 0x51;
|
||||
|
||||
/// The VIF lists that make up a tfrag's data block, in the order the game
|
||||
/// transfers them.
|
||||
///
|
||||
/// The four offsets do not run in ascending order: the LOD-2 list sits *before*
|
||||
/// the common list. Each is a byte range from the start of the tfrag's data
|
||||
/// block. Positions appear in the common list and the two LOD-0 lists only.
|
||||
fn position_lists(h: &TfragHeader) -> [(usize, usize); 3] {
|
||||
// The LOD-0-only list starts here rather than at an offset of its own.
|
||||
let lod0_start = h.shared_ofs as usize + h.lod_1_size as usize * 0x10;
|
||||
[
|
||||
(h.shared_ofs as usize, h.lod_1_ofs as usize),
|
||||
(h.lod_0_ofs as usize, lod0_start),
|
||||
(lod0_start, h.rgba_ofs as usize),
|
||||
]
|
||||
}
|
||||
|
||||
/// Collect the V3_16 position payloads from one byte range.
|
||||
fn positions_in_list(block: &[u8], start: usize, end: usize, out: &mut Vec<[i32; 3]>) -> bool {
|
||||
if start >= end || end > block.len() {
|
||||
// Empty lists and inverted ranges (a level with no LOD 2) are normal.
|
||||
return start >= end || end > block.len();
|
||||
}
|
||||
let mut pos = start;
|
||||
while pos + 4 <= end {
|
||||
let code = u32::from_le_bytes([block[pos], block[pos + 1], block[pos + 2], block[pos + 3]]);
|
||||
let packet = VifCode::decode(code);
|
||||
if pos + packet.packet_size() > end {
|
||||
return false;
|
||||
}
|
||||
if packet.is_unpack() && packet.vl() == 1 && packet.vn() == 2 {
|
||||
let base = pos + 4;
|
||||
for v in 0..packet.count() {
|
||||
let o = base + v * 6;
|
||||
if o + 6 > end {
|
||||
return false;
|
||||
}
|
||||
out.push([
|
||||
i16::from_le_bytes([block[o], block[o + 1]]) as i32,
|
||||
i16::from_le_bytes([block[o + 2], block[o + 3]]) as i32,
|
||||
i16::from_le_bytes([block[o + 4], block[o + 5]]) as i32,
|
||||
]);
|
||||
}
|
||||
}
|
||||
pos += packet.packet_size();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Vertex positions of a tfrag, in game order.
|
||||
///
|
||||
/// `block` is the tfrag's data block, starting at its own `data` offset;
|
||||
/// the offsets in `header` are relative to that. `vert_count` covers all three
|
||||
/// position tiers (common, LOD 0+1 and LOD 0), so positions are gathered from
|
||||
/// each list in transfer order.
|
||||
///
|
||||
/// Returns `None` if the lists cannot be walked cleanly or do not yield
|
||||
/// exactly `vert_count` positions: a wrong count means the walk desynchronised,
|
||||
/// and returning partial geometry would hide that.
|
||||
pub fn tfrag_positions(block: &[u8], header: &TfragHeader) -> Option<Vec<[i32; 3]>> {
|
||||
if header.vert_count == 0 {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
let mut out = Vec::with_capacity(header.vert_count as usize);
|
||||
for (start, end) in position_lists(header) {
|
||||
if !positions_in_list(block, start, end, &mut out) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if out.len() != header.vert_count as usize {
|
||||
return None;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A block header as measured on level 0.
|
||||
fn block_header() -> Vec<u8> {
|
||||
let mut v = vec![0u8; TFRAGS_BLOCK_HEADER_SIZE];
|
||||
v[0x00..0x04].copy_from_slice(&0x40u32.to_le_bytes());
|
||||
v[0x04..0x08].copy_from_slice(&833u32.to_le_bytes());
|
||||
v[0x08..0x0c].copy_from_slice(&32.0f32.to_le_bytes());
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_the_measured_block_header() {
|
||||
let h = TfragsBlock::parse(&block_header()).unwrap();
|
||||
assert_eq!(h.table_offset, 0x40);
|
||||
assert_eq!(h.tfrag_count, 833);
|
||||
assert_eq!(h.lod_distance, 32.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_a_tfrag_header() {
|
||||
let mut v = vec![0u8; TFRAG_HEADER_SIZE];
|
||||
v[0x00..0x10].copy_from_slice(&[0u8; 16]);
|
||||
v[0x10..0x14].copy_from_slice(&53_312i32.to_le_bytes());
|
||||
v[0x14..0x16].copy_from_slice(&0u16.to_le_bytes());
|
||||
v[0x16..0x18].copy_from_slice(&208u16.to_le_bytes());
|
||||
v[0x1c..0x1e].copy_from_slice(&256u16.to_le_bytes());
|
||||
v[0x3c] = 75;
|
||||
v[0x3d] = 62;
|
||||
|
||||
let h = TfragHeader::parse(&v).unwrap();
|
||||
assert_eq!(h.data, 53_312);
|
||||
assert_eq!(h.shared_ofs, 208);
|
||||
assert_eq!(h.tex_ofs, 256);
|
||||
assert_eq!(h.vert_count, 75);
|
||||
assert_eq!(h.tri_count, 62);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_table_access_is_bounds_checked() {
|
||||
let mut bytes = block_header();
|
||||
bytes.resize(0x40 + 4 * TFRAG_HEADER_SIZE, 0);
|
||||
let block = TfragsBlock::parse(&bytes).unwrap();
|
||||
block.header(&bytes, 3).unwrap();
|
||||
assert!(block.header(&bytes, 99).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vif_code_decoding() {
|
||||
// 0x694b_8022, measured in level 0's first tfrag: a V3_16 unpack of 75
|
||||
// positions to VU address 2.
|
||||
let c = VifCode::decode(0x694b_8022);
|
||||
assert!(c.is_unpack());
|
||||
assert_eq!((c.cmd, c.num, c.imm), (0x69, 0x4b, 0x8022));
|
||||
assert_eq!((c.vn(), c.vl()), (2, 1));
|
||||
assert_eq!(c.element_size(), 6);
|
||||
assert_eq!(c.format_name(), "V3_16");
|
||||
// 75 * 6 = 450, padded up to 452, plus the code word.
|
||||
assert_eq!(c.payload_size(), 452);
|
||||
assert_eq!(c.packet_size(), 456);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_formats_come_from_the_command_byte() {
|
||||
// (code word, vn, vl, bytes per element)
|
||||
for (word, vn, vl, size) in [
|
||||
(0x6900_8009u32, 2u8, 1u8, 6usize), // V3_16
|
||||
(0x6c00_0000, 3, 0, 16), // V4_32
|
||||
(0x6d00_c012, 3, 1, 8), // V4_16
|
||||
(0x6e00_0000, 3, 2, 4), // V4_8
|
||||
(0x6a00_0000, 2, 2, 3), // V3_8
|
||||
(0x6800_0000, 2, 0, 12), // V3_32
|
||||
] {
|
||||
let c = VifCode::decode(word);
|
||||
assert_eq!(
|
||||
(c.vn(), c.vl(), c.element_size()),
|
||||
(vn, vl, size),
|
||||
"{word:#010x}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vif_num_zero_means_256() {
|
||||
assert_eq!(VifCode::decode(0x6e00_0000).count(), 256);
|
||||
assert_eq!(VifCode::decode(0x6e00_0000).payload_size(), 256 * 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_some_non_unpack_commands_carry_payload() {
|
||||
// STROW and STCOL move a quadword each; a bare code word moves none.
|
||||
assert_eq!(VifCode::decode(0x3000_0000).payload_size(), 16);
|
||||
assert_eq!(VifCode::decode(0x3100_0000).payload_size(), 16);
|
||||
assert_eq!(VifCode::decode(0x2000_0000).payload_size(), 4);
|
||||
assert_eq!(VifCode::decode(0x4a02_0000).payload_size(), 16); // MPG num=2
|
||||
assert_eq!(VifCode::decode(0x5000_0001).payload_size(), 16); // DIRECT imm=1
|
||||
assert_eq!(VifCode::decode(0x0500_0001).payload_size(), 0); // STMOD
|
||||
assert_eq!(VifCode::decode(0x1400_0000).payload_size(), 0); // MSCAL
|
||||
}
|
||||
|
||||
/// Build a data block holding a single V3_16 unpack in the common list.
|
||||
fn synthetic_tfrag(verts: &[[i16; 3]]) -> (Vec<u8>, TfragHeader) {
|
||||
let mut block = Vec::new();
|
||||
let n = verts.len() as u16;
|
||||
// NUM is 8 bits, so a 256-element unpack has to encode zero.
|
||||
let num = if n == 256 { 0u16 } else { n };
|
||||
block.extend_from_slice(
|
||||
&VifCode {
|
||||
irq: false,
|
||||
cmd: 0x69,
|
||||
num,
|
||||
imm: 0x8009,
|
||||
}
|
||||
.to_word()
|
||||
.to_le_bytes(),
|
||||
);
|
||||
for v in verts {
|
||||
for c in v {
|
||||
block.extend_from_slice(&c.to_le_bytes());
|
||||
}
|
||||
}
|
||||
// The next list starts after the padded packet, not after the raw
|
||||
// vertices, so an odd element count advances the boundary.
|
||||
let common_end = block.len().div_ceil(4) * 4;
|
||||
block.resize(128, 0);
|
||||
|
||||
let mut h = TfragHeader::parse(&[0u8; TFRAG_HEADER_SIZE]).unwrap();
|
||||
h.vert_count = verts.len() as u8;
|
||||
h.shared_ofs = 0;
|
||||
h.lod_1_ofs = common_end as u16;
|
||||
h.lod_0_ofs = common_end as u16;
|
||||
// The LOD-0-only list starts on a quadword boundary at or after the
|
||||
// common list, so round the span up rather than truncating it.
|
||||
h.lod_1_size = common_end.div_ceil(16) as u8;
|
||||
h.rgba_ofs = 128;
|
||||
(block, h)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_positions_in_a_synthetic_block() {
|
||||
let verts = [
|
||||
[99, -3795, -5203],
|
||||
[1123, -3795, -5203],
|
||||
[-715, -3072, -5203],
|
||||
[1123, -3072, -5203],
|
||||
];
|
||||
let (block, header) = synthetic_tfrag(&verts);
|
||||
let pos = tfrag_positions(&block, &header).expect("positions should be found");
|
||||
assert_eq!(pos.len(), 4);
|
||||
for (got, want) in pos.iter().zip(verts.iter()) {
|
||||
assert_eq!(*got, [want[0] as i32, want[1] as i32, want[2] as i32]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_odd_count_is_padded_to_four_bytes() {
|
||||
// 3 elements = 18 bytes, padded to 20, so the next code word sits at
|
||||
// +24 rather than +22.
|
||||
let verts = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
|
||||
let (block, header) = synthetic_tfrag(&verts);
|
||||
let c = VifCode::decode(u32::from_le_bytes(block[0..4].try_into().unwrap()));
|
||||
assert_eq!(c.payload_size(), 20);
|
||||
let pos = tfrag_positions(&block, &header).expect("positions should be found");
|
||||
assert_eq!(pos.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_desynchronised_walk_is_reported_rather_than_trusted() {
|
||||
let verts = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]];
|
||||
let (block, header) = synthetic_tfrag(&verts);
|
||||
// Claim more vertices than the block actually holds.
|
||||
let mut bad = header;
|
||||
bad.vert_count = 40;
|
||||
assert!(tfrag_positions(&block, &bad).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tfrag_with_no_unpack_returns_none() {
|
||||
let block = vec![0u8; 128];
|
||||
let mut header = TfragHeader::parse(&[0u8; TFRAG_HEADER_SIZE]).unwrap();
|
||||
header.vert_count = 4;
|
||||
header.shared_ofs = 0;
|
||||
header.lod_1_ofs = 64;
|
||||
header.lod_0_ofs = 64;
|
||||
header.rgba_ofs = 128;
|
||||
assert!(tfrag_positions(&block, &header).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_vertex_count_gives_an_empty_result() {
|
||||
let block = vec![0u8; 16];
|
||||
let mut header = TfragHeader::parse(&[0u8; TFRAG_HEADER_SIZE]).unwrap();
|
||||
header.vert_count = 0;
|
||||
assert_eq!(tfrag_positions(&block, &header), Some(Vec::new()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user