diff --git a/crates/gc-formats/src/tie.rs b/crates/gc-formats/src/tie.rs index 9f5afcf..d8df87d 100644 --- a/crates/gc-formats/src/tie.rs +++ b/crates/gc-formats/src/tie.rs @@ -252,15 +252,33 @@ pub struct TieRecord { pub control_count: u8, /// `+0x07`: 1..5 on disc, never above `control_count`. pub control_size: u8, - /// `+0x08`: 3..7 on disc, never above `vert_size`. + /// `+0x08`: **quadwords of control data before the vertices**, 3..8 on disc + /// (`3`: 5 records, `4`: 3,364, `5`: 4,571, `6`: 3,469, `7`: 1,632, + /// `8`: 1 of 15,219). Peak at 5. + /// + /// **Not a vertex offset.** The name is the one thing about this record that + /// is wrong: the value is the *length* of the leading region, not where the + /// vertices start within some larger array. The vertex data begins at this + /// many quadwords into the packet. Renamed in intent, not in the field name, + /// because the on-disc order is fixed and `vert_ofs` is what every existing + /// test and doc says. pub vert_ofs: u8, - /// `+0x09`: 4..60 on disc. + /// `+0x09`: quadwords of vertex data; 4..60 on disc. pub vert_size: u8, /// `+0x0a`: equals `vert_size` on every record measured. pub color_ofs: u8, /// `+0x0b`: zero on every record measured. pub color_count: u8, - /// `+0x0c`: 7..67 on disc, never below `scissor_size`. + /// `+0x0c`: quadword offset of the scissor data — and **exactly + /// `vert_ofs + vert_size`, on 15,219 of 15,219 records across all 27 + /// levels**, including every record whose `strip_count` is non-zero. + /// + /// It carries no information the other two fields do not. It is the end of + /// the vertex region, stored twice. The universality is itself the useful + /// part: it is the check that showed the packet really does partition as + /// control / vertices / scissor, since a derived quantity only lands on the + /// same value everywhere if those three regions are laid out the way the + /// length formula already assumed. pub scissor_ofs: u8, /// `+0x0d`: 1..5 on disc. pub scissor_size: u8, @@ -329,6 +347,10 @@ impl TieRecord { && self.vert_ofs <= self.vert_size && self.scissor_size <= self.scissor_ofs && self.control_size <= self.control_count + // Universal on 15,219 of 15,219 records, so unlike the first four -- + // which are the large-partition rule plus three smaller ones -- this + // one can be a hard check. See `scissor_ofs`. + && u16::from(self.scissor_ofs) == u16::from(self.vert_ofs) + u16::from(self.vert_size) } /// Quadwords of packet data: `vert_ofs + vert_size + scissor_size`. @@ -557,7 +579,7 @@ mod tests { 7, 2, // control_count, control_size 4, 0x20, // vert_ofs, vert_size 0x20, 0, // color_ofs (= vert_size), color_count - 0x30, 0x20, // scissor_ofs, scissor_size + 0x24, 0x20, // scissor_ofs (= vert_ofs + vert_size), scissor_size 0, 0, // strip_count, strip_vertex_count ]); } @@ -580,6 +602,8 @@ mod tests { let second = TieRecord::parse(records[1]).unwrap(); assert_eq!(second.shader_count, 1); assert_eq!(second.bfc_distance, 5); + // scissor_ofs is vert_ofs + vert_size: 4 + 0x20 = 0x24. + assert_eq!(second.scissor_ofs, 0x24); assert!(second.invariants_hold()); } diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index aac9a3d..15c1ee3 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -1766,3 +1766,47 @@ than two independent tables that happen to agree. named. The Moby class-ID enrichment from earlier stands separately: 9,493 hits against 246.1 expected is real, and the class arrays are contiguous per type, but that identifies *where classes are referenced*, not which chunk is `moby_anim`. + + +## The tie control region is not a tag stream (negative result) + +Went looking for the GS's GIF packets, which the tie packet header calls AD-GIF +(`shader_count` at `+0x04`, and `bfc_distance == 5 * shader_count` at `+0x05`). +Tried the first `5 * shader_count` quadwords of each packet as VIF tags (command +in bits 0-6, size in 7-14, mark in 15) and as GIF tags (type in the top byte of +word 1, length in bits 0-2 of word 0). + +**Both came out uniform.** Over 13,042 records, 10,443 distinct values of word 0 +and no GIF tag type above a count of 2. By rule 4 that is the control result: +the region is not a tag stream, and no further effort should go into decoding it +as one. + +The first version of that probe was wrong and worth recording, because it reached +the *opposite* conclusion twice. It scanned `bfc_distance` quadwords, which for +most records overruns `vert_ofs` and runs into the vertex data. Pooling a +control region with vertex positions -- two populations with different +structure -- produced the uniform answer, and "not a tag stream" was the right +conclusion for the wrong reason. Re-measuring quadword 0 alone on its own gives +the same uniformity, so the conclusion survives; the reasoning did not. + +What the probe did establish is the **packet partition**: + + q[0 .. vert_ofs) control 3..8 quadwords (peak 5) + q[vert_ofs .. +vert_size) vertices + q[vert_ofs+vert_size ..] scissor + +and a new **universal invariant**: `scissor_ofs == vert_ofs + vert_size` on +**15,219 of 15,219** records across all 27 levels, including every record whose +`strip_count` is non-zero. That is a derived quantity stored explicitly, and it +holds everywhere -- which is the check that confirms the three regions really are +laid out as the packet-length formula assumed. + +It also found `vert_ofs` mis-named. It is not a vertex offset; it is the *length* +of the leading control region. The vertex data begins at that many quadwords into +the packet. The field name is left alone because the on-disc order is fixed and +everything already written about this record says `vert_ofs`, but the doc now +says what the value is. + +**Still not known:** what the control region actually contains. It is not +standard VIF or GIF DMA tags. It may be pre-merged GS register writes in a +different framing, or per-class state rather than per-primitive draw data. diff --git a/docs/roadmap.md b/docs/roadmap.md index e05a03d..e620415 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -742,7 +742,12 @@ lookup, and it is the main thing inherited from ReRAC. - [ ] VU0/VU1 skinning and lighting on the PS2 float model - [ ] LOD/morph/cull per `TfragProc` and `TieProc` - [ ] Occlusion culling, fog, shadows -- [ ] GIF/VIF packet stream interpretation +- [~] GIF/VIF packet stream interpretation. **The tie control region is not a VIF or GIF + tag stream** -- measured uniform on both framings across 13,042 records, so + the GS's draw packets are not stored there in standard DMA form. What the + probe did establish is the packet partition (control / vertices / scissor) + and a universal invariant, `scissor_ofs == vert_ofs + vert_size`, on 15,219 + of 15,219 records. See `docs/plan/level-assets.md`. **Gate:** CPU and GPU paths agree on every vertex of a reference level — the same identity check ReRAC runs.