Two class-header words are base-relative offsets pointing past the tie packets

Chasing where the AD-GIF is, since f43a1c2 established it is not in the packet.
Two of the undecoded class-header words are offsets.

base + 0x1c and base + 0x34 land inside core_data on 1,864 of 1,864 classes, so
both are relative to the class base rather than absolute, and both are non-zero
on every class. Both also land **past** that class's own tie packet blocks on
every class, so they address per-class data held elsewhere in core_data rather
than the tie geometry.

That is consistent with f43a1c2: if the packet's own control region is not a tag
stream, the per-class draw state is referenced from somewhere else.

WHAT IS THERE, AND WHAT IS NOT CLAIMED

The bytes are structured, not random -- class 3 shows a fixed-stride table with
`80 01 00` recurring, class 2's field is a monotone byte table, class 5's has
`38 38 38` and `ff 00 ff ff`. They read like compiled shader programs with
small-integer parameters, or lookup tables.

**Not identified, and deliberately not named.** None of those three fragments has
been run against a control, and all three were picked because they looked like
something. That is precisely the finding rule 4 refuses, and having spent this
session on four retractions I would rather record an honest "structured,
unidentified" than a plausible format that has not earned it. If any of these
turns out to be a shader program the next step is a control, not a better-looking
hex dump.

ALSO SETTLED

0x24 is demonstrably not an offset: only 17 of 1,864 of its values land in range
for core_data. Recorded as packed data. 0x28, 0x2c, 0x38 remain high-entropy and
unexplained.

281 passing with the disc present (23 + 19 + 61 + 156 + 22), clippy clean.
This commit is contained in:
2026-10-14 05:03:34 +02:00
parent ff9699c3c7
commit 2ff2abbd32
3 changed files with 128 additions and 2 deletions
+82
View File
@@ -6539,3 +6539,85 @@ fn tie_class_header_tail_words() {
); );
eprintln!("0x3c histogram: {w3c:?}"); eprintln!("0x3c histogram: {w3c:?}");
} }
/// `0x1c` and `0x34` of the tie class header are base-relative offsets, and they
/// land past the class's own tie packets.
#[test]
fn tie_class_tail_offsets_point_past_the_tie_packets() {
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::{ClassEntry, LevelCoreHeader};
use gc_formats::tie::TieClassHeader;
let (mut classes, mut in_range, mut past_packets, mut both_nonzero) =
(0usize, 0usize, 0usize, 0usize);
for lv in 0..27usize {
let (Ok(idx), Ok(core)) = (
std::fs::read(data.join(format!("level{lv}/core_index.bin"))),
std::fs::read(data.join(format!("level{lv}/core_data.bin"))),
) else {
continue;
};
let t = LevelCoreHeader::parse(&idx).expect("hdr").tie_classes;
for c in 0..t.count as usize {
let o = t.offset as usize + c * 0x20;
let Ok(e) = ClassEntry::parse(&idx[o..o + 0x20]) else {
continue;
};
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;
};
classes += 1;
let word = |k: usize| -> u32 {
let at = base + 0x1c + k * 4;
u32::from_le_bytes(core[at..at + 4].try_into().unwrap())
};
let (o1c, o34) = (word(0) as usize, word(6) as usize);
if usize::from(o1c) + 32 <= core.len() && o34 + 32 <= core.len() {
in_range += 1;
}
if word(0) != 0 && word(6) != 0 {
both_nonzero += 1;
}
// Past the class's own tie packet blocks.
let last_packet = hdr
.packets
.iter()
.filter(|p| **p > 0)
.max()
.copied()
.unwrap_or(0);
if o1c as i32 > last_packet && o34 as i32 > last_packet {
past_packets += 1;
}
}
}
assert!(classes > 1_800, "only {classes} tie classes read");
eprintln!("0x1c/0x34 in range {in_range}/{classes}, past own packets {past_packets}/{classes}");
// Both are relative to the class base, not absolute: base + offset lands in
// core_data, and absolute offsets would not.
assert!(
in_range * 100 >= classes * 99,
"only {in_range}/{classes} offsets in range"
);
assert_eq!(both_nonzero, classes, "a class has a zero offset");
// They are never inside the class's own tie data.
assert_eq!(
past_packets, classes,
"{classes} offsets fall inside the tie packets"
);
}
+8 -2
View File
@@ -111,8 +111,14 @@ impl TieClassHeader {
// bfc_distance is flat across every bucket. // bfc_distance is flat across every bucket.
// 0x40 a float -- 0.00208 to 10.84, no negatives. The bounding // 0x40 a float -- 0.00208 to 10.84, no negatives. The bounding
// sphere radius or the class scale; not distinguished. // sphere radius or the class scale; not distinguished.
// 0x1c, 0x34 look like offsets (0x1e0..0x3cd60, 0x190..0x31e20) and // 0x1c and 0x34 are base-relative offsets: base + offset lands inside
// 0x24, 0x28, 0x2c, 0x38 are high-entropy. Unidentified. // core_data on 1,864 of 1,864 classes, and always *past* the class's own
// tie packet blocks, so they address per-class data stored elsewhere.
// The bytes there are structured -- repeating small-integer records,
// occasional `ff 00 ff ff` -- but are not VIF or GIF DMA tags and are
// not identified. See tie_class_tail_offsets_point_past_the_tie_packets.
// 0x24, 0x28, 0x2c, 0x38 are high-entropy and unidentified; 0x24 is
// demonstrably not an offset (17 of 1,864 land in range).
b.skip(0x44 - 0x1c, "tie class fields not decoded yet")?; b.skip(0x44 - 0x1c, "tie class fields not decoded yet")?;
let o_class = b.i32("tie class number")?; let o_class = b.i32("tie class number")?;
Ok(TieClassHeader { Ok(TieClassHeader {
+38
View File
@@ -1849,3 +1849,41 @@ vertices, scissor. `bfc_distance` is a per-record quantity relating to the shade
count whose meaning is not established, and the "quadwords of ad-gif data" reading count whose meaning is not established, and the "quadwords of ad-gif data" reading
of it is wrong. The field doc said so in terms before this measurement; it does of it is wrong. The field doc said so in terms before this measurement; it does
not now. not now.
## Where the AD-GIF is not: class-header offsets 0x1c and 0x34
Two words in the class header's undecoded tail are offsets, and they are the
best remaining lead on the AD-GIF.
`base + 0x1c` and `base + 0x34` land inside `core_data` on **1,864 of 1,864
classes**, so both are relative to the class base rather than absolute, and both
are non-zero on every class. And both land **past** that class's own tie packet
blocks on every class -- they address per-class data stored somewhere else in
core_data, not the tie geometry.
What is there is structured, not random. Class 0 at 0x1c:
54 5d 5f 0c 44 44 44 01 03 11 19 00 1c 00 60 0a 30 08 60 0a
ff 00 ff ff 20 0c 50 02 44 38 2b 50
and class 3 at the same field:
20 05 c5 00 00 80 01 00 26 05 cb 00 21 88 01 00 2c 05 c0 00
00 80 01 00 32 05 c0 00 00 80 01 00
Class 3 is a fixed-stride table with `80 01 00` recurring. Class 5's 0x34 field
has `38 38 38` and `ff 00 ff ff`. Class 2's 0x1c field is a monotone byte table:
`26 27 0e 0f 10 11 12 13 24 25 15 1e 17 1f 14 15 16 17 20 21 22 23`.
These read like compiled shader programs with small-integer parameters, or as
lookup tables. **They are not identified**, and the resemblance to anything
specific is exactly the kind of finding rule 4 exists to refuse -- none of these
three fragments has been shown to beat a control, and they were picked because
they looked like something.
What is established is structural: two base-relative offsets, always in range,
always past the tie packets, pointing at repeated structured records rather than
DMA tags. That is consistent with the earlier finding that the packet's own
control region is not a tag stream -- the game's per-class draw state is stored
elsewhere and referenced, not inlined.