Establish the tie vertex element is 16 bytes, and correct the roadmap's tie entry

With TieRecord identified as RAC1's TiePacketHeader, each record's leading word
points at its packet in the block payload. Following it and stepping vert_size
elements from vert_ofs reaches the vertices: 95,612 of them on levels 0-2.

Element size settled by comparison, not assumed. Requiring the texcoord pair to be
12-bit fixed -- both words multiples of 4096, which is how 0x1000 = 1.0 reads:

  element 16 bytes : 78,919 / 95,612 = 82.5%
  element  8 bytes : 45,661 / 95,612 = 47.8%
  element 12 bytes : 44,834 / 95,612 = 44.8%

Sixteen wins by a factor of 1.7 over the nearest alternative.

Observed layout, 8 x i16: the first three slots span the full i16 range with
10,803-13,421 distinct values each; i16[4] and [5] are the texcoords and are both
multiples of 4096 on 82.5%; i16[6] reads 4096 on 85.8% and i16[7] reads 0 on
84.0%.

Not established: what any field means, and why the texcoord rate is 82.5% rather
than 100%. The residual tracks record shape -- (shaders 2, control 7, size 2)
hits 60%, (shaders 1, control 18, size 5) hits 89% -- so it is not one bad block
but something per-packet this reading does not capture.

Worth recording that this is the OPPOSITE of what the disc alone concluded two
commits ago. Reading the header word at +0x00 as an element count gave 0 of 922
blocks dividing; it is the byte offset to the payload, so the table it bounds has
16-byte records. The number was right and the reading was wrong, which is what
the packet-header identification corrected.

The roadmap's tie entry was also stale -- it still said "a 16-byte record is
withdrawn", which the packet-header work had already reversed. Rewritten from the
commits: both indirections, self-delimiting blocks, the TiePacketHeader
identification with its 15,219/15,219 rule, the 16-byte vertex element, and what
remains open. A test asserts the element size against both alternatives so the
82.5% figure cannot be quietly replaced by an assumption.
This commit is contained in:
2026-10-13 04:27:54 +02:00
parent 54862b64ee
commit 35b087c882
3 changed files with 183 additions and 20 deletions
+122
View File
@@ -3333,3 +3333,125 @@ fn moby_class_ids_sit_in_one_slot_per_chunk_type() {
&all[..all.len().min(6)]
);
}
/// The tie vertex element is 16 bytes, with 12-bit fixed-point texcoords.
///
/// Measured over 95,612 vertices on levels 0-2, reached by following each
/// `TieRecord::pointer` into the block payload and stepping `vert_size` elements
/// from `vert_ofs`. The element size is settled by comparison against the
/// alternatives, not assumed: 16 bytes gives a 12-bit-fixed texcoord hit rate of
/// 82.5%, against 47.8% at 8 bytes and 45.4% at 12.
///
/// The field *names* are not established -- this pins the size and the texcoord
/// encoding only.
#[test]
fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() {
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::level_core::{ClassEntry, LevelCoreHeader};
use gc_formats::tie::{parse_tie_block, TieClassHeader, TieRecord};
let mut verts = 0usize;
let mut fixed16 = 0usize;
let mut fixed8 = 0usize;
let mut fixed12 = 0usize;
for lv in 0..3usize {
let idx =
std::fs::read(data.join(format!("level{lv}/core_index.bin"))).expect("core_index");
let core = std::fs::read(data.join(format!("level{lv}/core_data.bin"))).expect("core_data");
let h = LevelCoreHeader::parse(&idx).expect("hdr");
let t = h.tie_classes;
for c in 0..t.count as usize {
let o = t.offset as usize + c * 0x20;
let e = ClassEntry::parse(&idx[o..o + 0x20]).expect("entry");
let base = e.offset_in_asset_wad as usize;
if base == 0 || base + 0x48 > core.len() {
continue;
}
let Ok(hdr) = TieClassHeader::parse(&core[base..base + 0x48]) else {
continue;
};
for lod in [1usize, 2] {
let off = hdr.packets[lod] as usize;
if off == 0 {
continue;
}
let s = base + off;
let Some(blk) = parse_tie_block(&core, s) else {
continue;
};
let recs: Vec<&[u8]> = blk.header.records(&core[s..]).collect();
for (ri, rec) in recs.iter().enumerate() {
let Ok(r) = TieRecord::parse(rec) else {
continue;
};
let start = s + 16 + ri * 16 + r.pointer as usize + r.vert_ofs as usize * 16;
for v in 0..r.vert_size as usize {
let o = start + v * 16;
if o + 16 > core.len() {
break;
}
verts += 1;
// Compare element sizes properly: each strides its own
// way through the payload, with its texcoords at that
// element's own offset.
for stride in [8usize, 12, 16] {
let q = start + v * stride;
if q + stride > core.len() {
continue;
}
let tc = if stride >= 16 { (8, 10) } else { (4, 6) };
let a = i16::from_le_bytes(
core[q + tc.0..q + tc.0 + 2].try_into().unwrap(),
);
let b = i16::from_le_bytes(
core[q + tc.1..q + tc.1 + 2].try_into().unwrap(),
);
if a % 4096 == 0 && b % 4096 == 0 {
match stride {
16 => fixed16 += 1,
12 => fixed12 += 1,
_ => fixed8 += 1,
}
}
}
}
}
}
}
}
assert!(verts > 90_000, "only {verts} tie vertices reached");
let r16 = fixed16 as f64 / verts as f64;
let r8 = fixed8 as f64 / verts as f64;
let r12 = fixed12 as f64 / verts as f64;
assert!(r16 > 0.80, "16-byte element hit rate fell to {r16:.3}");
assert!(
r16 > r8 * 1.5,
"16-byte element ({r16:.3}) no longer beats 8-byte ({r8:.3})"
);
assert!(
r16 > r12 * 1.5,
"16-byte element ({r16:.3}) no longer beats 12-byte ({r12:.3})"
);
eprintln!(
"tie vertices: {verts} on levels 0-2; 12-bit-fixed texcoord hit rate \
16B {:.1}%, 8B {:.1}%, 12B {:.1}% -- element size is 16 bytes",
r16 * 100.0,
r8 * 100.0,
r12 * 100.0
);
}