diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 2b8fe4f..d497430 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -3433,6 +3433,82 @@ fn tie_vertex_element_is_16_bytes_with_fixed_texcoords() { } } + // Which i16 slot pair is the texcoord? Scored per pair on level 0. + let core0 = std::fs::read(data.join("level0/core_data.bin")).expect("core_data"); + let idx0 = std::fs::read(data.join("level0/core_index.bin")).expect("core_index"); + let t0 = LevelCoreHeader::parse(&idx0).expect("hdr").tie_classes; + let (mut pair_hits, mut pair_tot) = ([0usize; 4], 0usize); + for c in 0..t0.count as usize { + let o = t0.offset as usize + c * 0x20; + let e = ClassEntry::parse(&idx0[o..o + 0x20]).expect("entry"); + let base = e.offset_in_asset_wad as usize; + if base == 0 || base + 0x48 > core0.len() { + continue; + } + let Ok(hdr) = TieClassHeader::parse(&core0[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(&core0, s) else { + continue; + }; + let rr: Vec<&[u8]> = blk.header.records(&core0[s..]).collect(); + for (ri, rec) in rr.iter().enumerate() { + let Ok(tr) = TieRecord::parse(rec) else { + continue; + }; + let start = s + 16 + ri * 16 + tr.pointer as usize + tr.vert_ofs as usize * 16; + for v in 0..tr.vert_size as usize { + let q = start + v * 16; + if q + 16 > core0.len() { + break; + } + pair_tot += 1; + for p in 0..4usize { + let a = + i16::from_le_bytes(core0[q + p * 4..q + p * 4 + 2].try_into().unwrap()); + let b = i16::from_le_bytes( + core0[q + p * 4 + 2..q + p * 4 + 4].try_into().unwrap(), + ); + if a % 4096 == 0 && b % 4096 == 0 { + pair_hits[p] += 1 + } + } + } + } + } + } + // (4,5) is the texcoord field. (6,7) scores *higher* only because it is the + // near-constant 4096/0 pair, which is trivially a pair of multiples of 4096 + // -- the same artifact a constant would produce anywhere. + assert!(pair_tot > 30_000, "only {pair_tot} level-0 vertices scored"); + let r = |p: usize| pair_hits[p] as f64 / pair_tot as f64; + assert!(r(2) > 0.60, "texcoord pair (4,5) fell to {:.3}", r(2)); + assert!( + r(0) < 0.05, + "pair (0,1) looks like a texcoord at {:.3}", + r(0) + ); + assert!( + r(1) < 0.15, + "pair (2,3) looks like a texcoord at {:.3}", + r(1) + ); + + eprintln!( + "tie vertex slot pairs, level 0 (n={pair_tot}): (0,1) {:.1}% (2,3) {:.1}% \ + (4,5) {:.1}% <- texcoords (6,7) {:.1}% <- the constant 4096/0 pair", + r(0) * 100.0, + r(1) * 100.0, + r(2) * 100.0, + r(3) * 100.0 + ); + 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; diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index 74c570f..209fe91 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -1607,12 +1607,29 @@ texcoord pair to be 12-bit fixed -- both words multiples of 4,096, which is how Sixteen wins by a factor of 1.7 over the nearest alternative, so it is the element size. -**Observed layout**, 8 x `i16`: +**Observed layout**, 8 x `i16`. Scoring each adjacent slot pair for 12-bit +fixedness over 36,501 level-0 vertices locates the fields: - i16[0..3] 10,803 to 13,421 distinct values, spanning the full i16 range - i16[4], [5] the texcoords; both multiples of 4,096 on 82.5% of vertices - i16[6] reads 4,096 on 85.8% - i16[7] reads 0 on 84.0% + (i16[0], i16[1]) 1.1% not a texcoord + (i16[2], i16[3]) 4.8% not a texcoord + (i16[4], i16[5]) 73.6% <- the texcoords + (i16[6], i16[7]) 76.6% <- the near-constant 4096/0 pair + +**`(6,7)` scores higher and is not a texcoord.** It is the near-constant +`4096, 0` pair -- `4096` is 1.0 in 12-bit fixed and `0` is zero, so *both* are +trivially multiples of 4,096. That is the constant artifact again: a fixed pair +would score 100% anywhere. The field is `(4,5)` at 73.6%, with `(0,1)` and `(2,3)` +ruled out at 1.1% and 4.8%. + +The first four slots span the full `i16` range with 10,803 to 13,421 distinct +values each, so they are neither texcoords nor small fields. + +**The vertex base is `vert_ofs` alone.** Adding the shader or control region to +it makes things *worse*, so the 73.6% residual is not a mislocated base: + + vert_ofs only 73.6% + bfc + vert_ofs 69.8% + bfc + control_count*control_size + vert_ofs 66.7% **Not established:** what any of the eight fields *means*, and why the texcoord rate is 82.5% rather than 100%. The residual varies with record shape -- records diff --git a/docs/roadmap.md b/docs/roadmap.md index 387c2ad..b2fe1b7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -260,10 +260,18 @@ Phase 1 hands over the extents; this phase works out what each one contains. - [x] The tie **vertex element is 16 bytes**, measured over 95,612 vertices on levels 0-2 and settled by comparison: a 12-bit-fixed texcoord hit rate of **82.5%** at 16 bytes against 47.8% at 8 and 44.8% at 12. -- [ ] The tie **vertex fields are not named.** All eight `i16` slots span the full - range; the texcoord pair is 12-bit fixed on 82.5%, not 100%, and the - residual tracks record shape (60% to 89%), so something per-packet varies - that this reading does not capture. +- [x] The tie **texcoord field is located**: `i16[4]`/`i16[5]` of the 16-byte + element, scoring 73.6% for 12-bit fixedness over 36,501 level-0 vertices, + against 1.1% for `(0,1)` and 4.8% for `(2,3)`. The `(6,7)` pair scores + *higher* at 76.6% and is **not** a texcoord -- it is the near-constant + `4096, 0` pair, which is trivially two multiples of 4,096. +- [ ] The remaining tie vertex **fields are not named.** `i16[0..3]` span the full + `i16` range with 10,803-13,421 distinct values each, so they are neither + texcoords nor small fields; they are most likely position plus a packed + word, but that is not established. The texcoord rate is 73.6% rather than + 100% and the vertex base is confirmed to be `vert_ofs` alone (adding the + shader or control region makes it worse), so the residual is not a + mislocated base. - [~] `shrub` — class table done (48-byte entries, not 32; reading shrub at 32 still gives 231 sane-looking entries of 358, which is the hazard). Geometry header parsed and checked field by field on all 358 spans: