//! Xenos fetch (vertex + texture) instruction decoder. //! //! Like ALU instructions, fetches are 96 bits (3 dwords). The opcode lives //! in the low 5 bits of word0. We split them into `VertexFetch` and //! `TextureFetch` structurally because their operand layouts differ. //! //! Reference: `xenia-canary/src/xenia/gpu/ucode.h:690-877`. /// Decoded fetch instruction. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FetchInstruction { Vertex(VertexFetch), Texture(TextureFetch), /// Unknown / minor variants we don't model yet. Unknown { opcode: u8, raw: [u32; 3] }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct VertexFetch { /// Vertex fetch *const_index* (5 bits, w0[20:24]). The full fetch-constant /// index is `const_index * 3 + const_index_sel` (canary `ucode.h:700`); use /// [`VertexFetch::const_reg_offset`] for the register-region dword offset. pub fetch_const: u8, /// iterate-3X (GPUBUG-110): `const_index_sel` (2 bits, w0[25:26]) — selects /// one of the 3 two-dword vertex-fetch constants packed in each 6-dword /// register group. Dropping this read sub-slot 0 of the group, missing the /// real vertex-buffer base for shaders that use sub-slot 1/2 (the publisher /// logo uses `const_index=31, sel=2`). pub const_index_sel: u8, /// Source register index (vertex index in r#). pub src_register: u8, /// Destination register for the fetched value. pub dest_register: u8, /// 4-bit write mask. pub dest_write_mask: u8, /// iterate-3S (GPUBUG-107): `xenos::VertexFormat` (6 bits, dword1[16:21]). /// Determines how many components to read and their packing. Pre-fix the /// translator hardcoded `k_32_32_32_32_FLOAT` (4 floats, stride 4), /// over-striding 2-float UI quads (`k_32_32_FLOAT`) → wrong/clipped /// positions (the next vertex's X bled into .w, giving negative W → the /// whole rectangle was clipped behind the camera). pub format: u8, /// Dword stride between consecutive vertices (dword2[0:7]). pub stride: u8, /// iterate-3T: dword offset of THIS attribute within the vertex stride /// (dword2[16:38] in canary's `VertexFetchInstruction`; the low 23 bits). /// A 6-dword vertex with position@0 + UV@2 + extra@3 needs this so the /// three vfetches sharing one fetch-constant read different attributes /// instead of all reading offset 0. pub offset: u32, /// `is_signed` = canary `fomat_comp_all`, word1 bit 12 (ucode.h:757) — /// selects signed vs unsigned interpretation of packed integer formats. /// (GPUBUG-113: was read from word1 bit 24, which is inside `exp_adjust`.) pub is_signed: bool, /// `is_normalized` = canary `num_format_all == 0`, word1 bit 13 /// (ucode.h:758). Set bit ⇒ integer (un-normalized); clear ⇒ normalized. /// We store the normalized sense directly. (GPUBUG-113: was word1 bit 25.) pub is_normalized: bool, /// `is_mini_fetch` = canary word1 bit 30 (ucode.h:764). A mini-fetch reuses /// the address AND STRIDE of the preceding full vfetch of the same stream; /// its own `stride` field is 0. Required so a vfetch_mini color attribute /// indexes by the real vertex stride instead of its tight dword count. pub is_mini_fetch: bool, pub raw: [u32; 3], } impl VertexFetch { /// Dword offset of this fetch's 2-dword constant within the fetch-constant /// register region (`CONST_BASE_FETCH`). Vertex fetch constants are packed /// 3 per 6-dword group: `const_index * 6 + const_index_sel * 2` /// (canary `ucode.h:700` `fetch_constant_index = const_index*3 + sel`, /// each constant 2 dwords). pub fn const_reg_offset(&self) -> u32 { self.fetch_const as u32 * 6 + self.const_index_sel as u32 * 2 } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TextureFetch { /// Texture fetch constant index (0..=31). pub fetch_const: u8, pub src_register: u8, pub dest_register: u8, /// Destination swizzle — 12 bits, 3 per output component (Xenos tfetch /// dword1 bits 0..11). Per canary `ucode.h`, each 3-bit code selects: /// 0..3 = sampled X/Y/Z/W, 4 = constant 0.0, 5 = constant 1.0, /// 6/7 = keep (destination component unwritten). /// The old `dest_write_mask: (w1 & 0xF)` read collapsed this to a bogus /// 4-bit mask, so multi-plane fetches (the intro video's Y/U/V, all /// targeting r1 but each in its own lane) each did a *full* `r1 = sample` /// overwrite — only the last plane survived → wrong-color video. pub dest_swizzle: u16, /// Dimension: 0=1D, 1=2D, 2=3D/stacked, 3=cube. pub dimension: u8, pub raw: [u32; 3], } /// Opcodes (low 5 bits of word0). From `ucode.h`. pub mod op { pub const VERTEX_FETCH: u8 = 0x00; pub const TEXTURE_FETCH: u8 = 0x01; pub const GET_TEXTURE_BORDER_COLOR_FRAC: u8 = 0x16; pub const GET_TEXTURE_COMPUTED_LOD: u8 = 0x17; pub const GET_TEXTURE_WEIGHTS: u8 = 0x18; pub const GET_TEXTURE_GRADIENTS: u8 = 0x19; pub const SET_TEXTURE_LOD: u8 = 0x1A; pub const SET_TEXTURE_GRADIENTS_HORZ: u8 = 0x1B; pub const SET_TEXTURE_GRADIENTS_VERT: u8 = 0x1C; } pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction { // Fetch dword0 bitfields (Xenos `ucode.h:740-749` vfetch / `844-845` // tfetch): opcode_value:5, src_reg:6, src_reg_am:1, dst_reg:6, // dst_reg_am:1, (fetch_valid_only|must_be_one):1, const_index:5 @ bit20, // ... The prior decoder read `const_index` from bit 5 (which is actually // `src_reg`), so every fetch reported the wrong fetch-constant slot — the // logo `tfetch2D ..., tf0` was read as `tf1`, and slot 1's empty constant // failed to decode → no texture. The texture-fetch `dimension` lives in // dword2 bits 14..15, not dword1. let w0 = words[0]; let w1 = words[1]; let w2 = words[2]; let opcode = (w0 & 0x1F) as u8; match opcode { op::VERTEX_FETCH => FetchInstruction::Vertex(VertexFetch { fetch_const: ((w0 >> 20) & 0x1F) as u8, const_index_sel: ((w0 >> 25) & 0x3) as u8, src_register: ((w0 >> 5) & 0x3F) as u8, dest_register: ((w0 >> 12) & 0x3F) as u8, dest_write_mask: (w1 & 0xF) as u8, // dword1[16:21] = VertexFormat. dword2: stride[0:7], // offset (in dwords) [8:?] — empirically the attribute offset of // the textured logo VS lands in dword2[8:15] (pos@4, UV@3, // 3-float@0 in a 6-dword vertex). signed/normalized live higher. format: ((w1 >> 16) & 0x3F) as u8, stride: (w2 & 0xFF) as u8, offset: (w2 >> 8) & 0xFF, // GPUBUG-113: canary ucode.h:757-758,764 — signed=fomat_comp_all // (w1 bit12), normalized=(num_format_all==0) (w1 bit13), // mini-fetch=(w1 bit30). The previous bit24/25 reads landed inside // `exp_adjust`, so signedness/normalization were effectively random. is_signed: ((w1 >> 12) & 1) != 0, is_normalized: ((w1 >> 13) & 1) == 0, is_mini_fetch: ((w1 >> 30) & 1) != 0, raw: words, }), op::TEXTURE_FETCH => FetchInstruction::Texture(TextureFetch { fetch_const: ((w0 >> 20) & 0x1F) as u8, src_register: ((w0 >> 5) & 0x3F) as u8, dest_register: ((w0 >> 12) & 0x3F) as u8, dest_swizzle: (w1 & 0xFFF) as u16, dimension: ((w2 >> 14) & 0x3) as u8, raw: words, }), _ => FetchInstruction::Unknown { opcode, raw: words }, } } #[cfg(test)] mod tests { use super::*; #[test] fn decode_vertex_fetch() { // opcode=0 (vertex). Xenos dword0: src_reg@bit5, dst_reg@bit12, // const_index@bit20. fetch_const=5, src=2, dest=7. let w0 = 0u32 | (2 << 5) | (7 << 12) | (5 << 20); let v = decode_fetch([w0, 0, 0]); match v { FetchInstruction::Vertex(vf) => { assert_eq!(vf.fetch_const, 5); assert_eq!(vf.src_register, 2); assert_eq!(vf.dest_register, 7); } other => panic!("expected Vertex, got {other:?}"), } } #[test] fn vertex_fetch_const_index_sel_and_reg_offset() { // iterate-3X (GPUBUG-110): the real publisher-logo vfetch (w0 = // 0x2DF82000) encodes const_index=31, const_index_sel=2. Its fetch // constant lives at dword offset `31*6 + 2*2 = 190` (reg 0x48BE), not // `31*6 = 186` (reg 0x48BA, which held the unused 0x1 slot). Dropping // the sel field made the logo geometry resolve as "no vertex buffer". let v = decode_fetch([0x2DF8_2000, 0, 0]); match v { FetchInstruction::Vertex(vf) => { assert_eq!(vf.fetch_const, 31, "const_index"); assert_eq!(vf.const_index_sel, 2, "const_index_sel"); assert_eq!(vf.const_reg_offset(), 190, "reg offset = 31*6 + 2*2"); } other => panic!("expected Vertex, got {other:?}"), } // sel=0 collapses to the legacy `fetch_const*6` offset (back-compat). let v0 = decode_fetch([0u32 | (5 << 20), 0, 0]); if let FetchInstruction::Vertex(vf) = v0 { assert_eq!(vf.const_index_sel, 0); assert_eq!(vf.const_reg_offset(), 30); } else { panic!("expected Vertex"); } } #[test] fn vertex_fetch_signed_normalized_mini_bits() { // GPUBUG-113: canary ucode.h:757-758,764 — is_signed=fomat_comp_all // (w1 bit12), is_normalized=(num_format_all==0) (w1 bit13), // is_mini_fetch=(w1 bit30). Validate each bit independently. let mk = |w1: u32| match decode_fetch([0, w1, 0]) { FetchInstruction::Vertex(vf) => vf, _ => panic!("vertex"), }; // No bits: unsigned, normalized, full fetch. let v = mk(0); assert!(!v.is_signed); assert!(v.is_normalized); assert!(!v.is_mini_fetch); // bit12 → signed. assert!(mk(1 << 12).is_signed); // bit13 (num_format_all=1) → NOT normalized. assert!(!mk(1 << 13).is_normalized); // bit30 → mini fetch. assert!(mk(1 << 30).is_mini_fetch); // The old (wrong) bits 24/25 must NOT affect signed/normalized. assert!(!mk(1 << 24).is_signed); assert!(mk(1 << 25).is_normalized); } #[test] fn decode_texture_fetch() { // opcode=1 (texture). const_index@bit20=3, src@bit5=1, dst@bit12=4. // dimension lives in dword2 bits 14..15. let w0 = 1u32 | (1 << 5) | (4 << 12) | (3 << 20); let w2 = 2u32 << 14; let t = decode_fetch([w0, 0, w2]); match t { FetchInstruction::Texture(tf) => { assert_eq!(tf.fetch_const, 3); assert_eq!(tf.src_register, 1); assert_eq!(tf.dest_register, 4); assert_eq!(tf.dimension, 2); } other => panic!("expected Texture, got {other:?}"), } } #[test] fn unknown_opcode_is_classified() { let v = decode_fetch([0x16, 0, 0]); // GET_TEXTURE_BORDER_COLOR_FRAC assert!(matches!(v, FetchInstruction::Unknown { opcode: 0x16, .. })); } }