Resolve tfrag LOD parent linkage: guard absent tiers by their counts

The four V4_8 parent arrays are classified by VU address, but the
addresses collide: on level 0 patch 1,
unk_indices_2_lod_01_addr == parent_indices_lod_0_addr == 291, and
unk_indices_2_lod_0_addr == indices_addr == 294. Matching on address
alone cannot separate them, and it fails silently -- the arrays still
parse, they just hold the wrong numbers.

Two corrections, and the first one is worth recording because it looked
like a data problem rather than a code one:

Each V4_8 element is 4 bytes and holds four indices, so an array of N
entries is count*4 bytes, not count. An earlier analysis compared the
unpack's element count against the position counts and concluded that
15,000 patches could not hold enough parents. That was the wrong unit;
the data was fine.

unk_06 and unk_0a suppress an empty tier. When the extra vertex-info
count is zero there is no extra array and its address is simply the next
tier's, so guarding those branches on the count lets the address fall
through to the array that exists. That one change took resolved LOD-0
entries from 28,739 to 341,306.

Result: of 886,841 vertex-info entries across all 27 levels, 516,492 are
common and have no link by design, and all 370,349 LOD entries resolve --
29,043 LOD-01 and 341,306 LOD-0 -- with zero failures. Each resolves to a
real first parent, a LOD-01 parent that is always a common entry, and
three positions inside the patch. A common entry never claims a parent.

Morphing can now be completed: a patch knows which LOD to draw, how far
to blend each vertex toward its parents, and where those parents are.
This commit is contained in:
2026-10-10 02:41:18 +02:00
parent 2a5307a157
commit 71a3185fa5
4 changed files with 175 additions and 49 deletions
+42 -13
View File
@@ -837,10 +837,10 @@ fn tfrag_lod_parents_resolve_for_every_entry() {
let mut entries = 0usize;
let mut lod01 = 0usize;
let mut lod0 = 0usize;
let mut lod01_missing = 0usize;
let mut lod0_missing = 0usize;
let mut resolved_common = 0usize;
let mut common_total = 0usize;
let mut primary_failed = 0usize;
let mut extras_unresolved = 0usize;
for n in 0..27 {
let dir = data.join(format!("level{n}"));
@@ -875,6 +875,18 @@ fn tfrag_lod_parents_resolve_for_every_entry() {
.unwrap_or_else(|e| panic!("level{n} patch {i}: {e}"));
common_total += g.vinfo_tier[0];
if n == 0 && patches < 5 {
eprintln!(
"p{i}: tier={:?} n01={} n0={} p01={}b x01={}b p0={}b x0={}b",
g.vinfo_tier,
g.vu_positions_lod_01_count,
g.vu_positions_lod_0_count,
g.parent_indices_lod01.len(),
g.unk_indices_2_lod01.len(),
g.parent_indices_lod0.len(),
g.unk_indices_2_lod0.len()
);
}
for v in 0..g.vertex_info.len() {
entries += 1;
match gc_formats::tfrag::tfrag_lod_link(&g, v) {
@@ -905,10 +917,23 @@ fn tfrag_lod_parents_resolve_for_every_entry() {
}
}
None => {
if v < g.vinfo_tier[1] {
lod01_missing += 1;
} else if v < g.vinfo_tier[1] + g.vinfo_tier[2] {
lod0_missing += 1;
// Distinguish the two kinds of LOD entry. A *primary*
// entry has a real position and a recorded parent and
// must always resolve; an *extra* entry is a
// non-position bookkeeping record whose array address
// can collide with the strip index array.
let c = g.vinfo_tier[0];
let kind = if v < c {
None
} else if v < c + g.vinfo_tier[1] {
Some((g.vu_positions_lod_01_count, v - c, true))
} else {
Some((g.vu_positions_lod_0_count, v - c - g.vinfo_tier[1], false))
};
match kind {
None => {}
Some((prim, k, _)) if k < prim => primary_failed += 1,
Some(_) => extras_unresolved += 1,
}
}
}
@@ -929,15 +954,19 @@ fn tfrag_lod_parents_resolve_for_every_entry() {
lod01 > 0 && lod0 > 0,
"both LOD tiers should occur in the data"
);
assert_eq!(
primary_failed, 0,
"{primary_failed} primary LOD entries did not resolve"
);
assert_eq!(
extras_unresolved, 0,
"{extras_unresolved} extra LOD entries did not resolve"
);
// Common entries have no link; every other entry does.
assert_eq!(entries, common_total + lod01 + lod0);
// RAC2 does not record a parent for every LOD entry: the parent arrays are
// shorter than the VU header's position counts on most patches, so many
// LOD entries have no recorded parent. That is a property of the data, not
// a parse failure -- what matters is that every *recorded* parent resolves
// to real vertex info and real positions, which is asserted above.
eprintln!(
"{entries} vertex-info entries over {patches} patches: {common_total} common, \
{lod01} + {lod0} LOD entries with a recorded parent, \
{lod01_missing} LOD-01 and {lod0_missing} LOD-0 entries without one"
{lod01} LOD-01 and {lod0} LOD-0, all resolved"
);
}