//! PM4 packet format — header decoding + Type-3 opcode set. //! //! Xenos PM4 packet layout mirrors `xenia-canary/src/xenia/gpu/packet_disassembler.cc`: //! //! - **Type 0** (`packet >> 30 == 0`): register-write run. //! `count = ((packet >> 16) & 0x3FFF) + 1`. Total dwords = `1 + count`. //! With `(packet >> 15) & 1 == 1`, all writes target the same register. //! - **Type 1** (`packet >> 30 == 1`): two-register write. Total dwords = 3. //! - **Type 2** (`packet >> 30 == 2`): NOP — a single skipped dword. //! - **Type 3** (`packet >> 30 == 3`): command. //! `opcode = (packet >> 8) & 0x7F`, //! `count = ((packet >> 16) & 0x3FFF) + 1`. //! Total dwords = `1 + count`. /// The cookie canary writes alongside `PM4_XE_SWAP` so tooling can recognize /// swap packets. `'X','E','N','X'` big-endian (`kSwapSignature`). pub const SWAP_SIGNATURE: u32 = 0x584E_4558; // ── Named Type-3 opcodes (from xenia-canary/src/xenia/gpu/xenos.h:1617-1679) ── pub const PM4_ME_INIT: u8 = 0x48; pub const PM4_NOP: u8 = 0x10; pub const PM4_INDIRECT_BUFFER: u8 = 0x3F; pub const PM4_INDIRECT_BUFFER_PFD: u8 = 0x37; pub const PM4_WAIT_FOR_IDLE: u8 = 0x26; pub const PM4_WAIT_REG_MEM: u8 = 0x3C; pub const PM4_REG_RMW: u8 = 0x21; pub const PM4_REG_TO_MEM: u8 = 0x3E; pub const PM4_MEM_WRITE: u8 = 0x3D; pub const PM4_COND_WRITE: u8 = 0x45; pub const PM4_EVENT_WRITE: u8 = 0x46; pub const PM4_EVENT_WRITE_SHD: u8 = 0x58; pub const PM4_EVENT_WRITE_EXT: u8 = 0x5A; pub const PM4_EVENT_WRITE_ZPD: u8 = 0x5B; pub const PM4_DRAW_INDX: u8 = 0x22; pub const PM4_DRAW_INDX_2: u8 = 0x36; pub const PM4_VIZ_QUERY: u8 = 0x23; pub const PM4_SET_CONSTANT: u8 = 0x2D; pub const PM4_SET_CONSTANT2: u8 = 0x55; pub const PM4_SET_SHADER_CONSTANTS: u8 = 0x56; pub const PM4_LOAD_ALU_CONSTANT: u8 = 0x2F; pub const PM4_IM_LOAD: u8 = 0x27; pub const PM4_IM_LOAD_IMMEDIATE: u8 = 0x2B; pub const PM4_LOAD_CONSTANT_CONTEXT: u8 = 0x2E; pub const PM4_INVALIDATE_STATE: u8 = 0x3B; pub const PM4_INTERRUPT: u8 = 0x54; pub const PM4_SET_SHADER_BASES: u8 = 0x4A; pub const PM4_SET_BIN_MASK_LO: u8 = 0x60; pub const PM4_SET_BIN_MASK_HI: u8 = 0x61; pub const PM4_SET_BIN_SELECT_LO: u8 = 0x62; pub const PM4_SET_BIN_SELECT_HI: u8 = 0x63; pub const PM4_SET_BIN_MASK: u8 = 0x50; pub const PM4_SET_BIN_SELECT: u8 = 0x51; pub const PM4_CONTEXT_UPDATE: u8 = 0x5E; /// Xenia-specific: `VdSwap` writes this to trigger a present. pub const PM4_XE_SWAP: u8 = 0x64; /// Human-readable name for a Type-3 opcode. Used for tracing spans. pub fn type3_opcode_name(op: u8) -> &'static str { match op { PM4_ME_INIT => "ME_INIT", PM4_NOP => "NOP", PM4_INDIRECT_BUFFER => "INDIRECT_BUFFER", PM4_INDIRECT_BUFFER_PFD => "INDIRECT_BUFFER_PFD", PM4_WAIT_FOR_IDLE => "WAIT_FOR_IDLE", PM4_WAIT_REG_MEM => "WAIT_REG_MEM", PM4_REG_RMW => "REG_RMW", PM4_REG_TO_MEM => "REG_TO_MEM", PM4_MEM_WRITE => "MEM_WRITE", PM4_COND_WRITE => "COND_WRITE", PM4_EVENT_WRITE => "EVENT_WRITE", PM4_EVENT_WRITE_SHD => "EVENT_WRITE_SHD", PM4_EVENT_WRITE_EXT => "EVENT_WRITE_EXT", PM4_EVENT_WRITE_ZPD => "EVENT_WRITE_ZPD", PM4_DRAW_INDX => "DRAW_INDX", PM4_DRAW_INDX_2 => "DRAW_INDX_2", PM4_VIZ_QUERY => "VIZ_QUERY", PM4_SET_CONSTANT => "SET_CONSTANT", PM4_SET_CONSTANT2 => "SET_CONSTANT2", PM4_SET_SHADER_CONSTANTS => "SET_SHADER_CONSTANTS", PM4_LOAD_ALU_CONSTANT => "LOAD_ALU_CONSTANT", PM4_LOAD_CONSTANT_CONTEXT => "LOAD_CONSTANT_CONTEXT", PM4_IM_LOAD => "IM_LOAD", PM4_IM_LOAD_IMMEDIATE => "IM_LOAD_IMMEDIATE", PM4_INVALIDATE_STATE => "INVALIDATE_STATE", PM4_INTERRUPT => "INTERRUPT", PM4_SET_SHADER_BASES => "SET_SHADER_BASES", PM4_SET_BIN_MASK_LO => "SET_BIN_MASK_LO", PM4_SET_BIN_MASK_HI => "SET_BIN_MASK_HI", PM4_SET_BIN_SELECT_LO => "SET_BIN_SELECT_LO", PM4_SET_BIN_SELECT_HI => "SET_BIN_SELECT_HI", PM4_SET_BIN_MASK => "SET_BIN_MASK", PM4_SET_BIN_SELECT => "SET_BIN_SELECT", PM4_CONTEXT_UPDATE => "CONTEXT_UPDATE", PM4_XE_SWAP => "XE_SWAP", _ => "UNKNOWN", } } /// Decoded single PM4 packet header. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PacketHeader { pub kind: PacketKind, /// Total size of the packet (including header) in dwords. pub total_dwords: u32, } /// Classification of a PM4 packet. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PacketKind { /// Type-0 register-write run. `base_index` is the first register index /// (the register offset / 4). `write_one` is true if all `count` data /// dwords write to the same register. Type0 { base_index: u32, count: u32, write_one: bool, }, /// Type-1 two-register write. Type1 { reg_index_1: u32, reg_index_2: u32 }, /// Type-2 NOP (a single skipped dword). Type2, /// Type-3 command. Type3 { opcode: u8, count: u32, predicated: bool, }, } /// Build a Type-0 register-write packet header. Mirrors canary's /// `MakePacketType0` at `xenia-canary/src/xenia/gpu/xenos.h:1682`. /// `count` is the number of data dwords that follow (inclusive: 1..=0x4000). pub fn make_packet_type0(reg_index: u16, count: u16) -> u32 { debug_assert!(reg_index <= 0x7FFF); debug_assert!(count >= 1 && count as u32 <= 0x4000); (0u32 << 30) | (((count as u32 - 1) & 0x3FFF) << 16) | (reg_index as u32 & 0x7FFF) } /// Build a Type-2 NOP packet header. Single dword, no payload. pub const fn make_packet_type2() -> u32 { 2u32 << 30 } /// Build a Type-3 command packet header. Mirrors canary's `MakePacketType3`. /// `count` is the number of data dwords that follow (inclusive: 1..=0x4000). pub fn make_packet_type3(opcode: u8, count: u16) -> u32 { debug_assert!(count >= 1 && count as u32 <= 0x4000); (3u32 << 30) | (((count as u32 - 1) & 0x3FFF) << 16) | ((opcode as u32 & 0x7F) << 8) } /// Decode a single PM4 packet header. pub fn decode(header: u32) -> PacketHeader { match header >> 30 { 0 => { let count = ((header >> 16) & 0x3FFF) + 1; PacketHeader { kind: PacketKind::Type0 { base_index: header & 0x7FFF, count, write_one: (header >> 15) & 1 != 0, }, total_dwords: 1 + count, } } 1 => PacketHeader { kind: PacketKind::Type1 { reg_index_1: header & 0x7FF, reg_index_2: (header >> 11) & 0x7FF, }, total_dwords: 3, }, 2 => PacketHeader { kind: PacketKind::Type2, total_dwords: 1, }, 3 => { let count = ((header >> 16) & 0x3FFF) + 1; PacketHeader { kind: PacketKind::Type3 { opcode: ((header >> 8) & 0x7F) as u8, count, predicated: (header & 1) != 0, }, total_dwords: 1 + count, } } _ => unreachable!(), } } #[cfg(test)] mod tests { use super::*; #[test] fn type2_is_one_dword() { // 0x80000000 == type 2 header (bits 31:30 = 10) let hdr = decode(0x8000_0000); assert_eq!(hdr.kind, PacketKind::Type2); assert_eq!(hdr.total_dwords, 1); } #[test] fn type0_count_is_inclusive() { // count field (bits 29:16) = 5 → 6 data dwords. base_index = 0x100. // write_one = 0. let hdr = decode((5 << 16) | 0x100); match hdr.kind { PacketKind::Type0 { base_index, count, write_one, } => { assert_eq!(base_index, 0x100); assert_eq!(count, 6); assert!(!write_one); } _ => panic!("expected Type0"), } assert_eq!(hdr.total_dwords, 7); } #[test] fn type3_swap_packet() { // Build the exact header canary's VdSwap emits: // MakePacketType3(PM4_XE_SWAP, 4) → ((3<<30) | ((4-1)<<16) | (0x64<<8)) let hdr_word = (3u32 << 30) | ((4u32 - 1) << 16) | ((PM4_XE_SWAP as u32) << 8); let hdr = decode(hdr_word); match hdr.kind { PacketKind::Type3 { opcode, count, predicated, } => { assert_eq!(opcode, PM4_XE_SWAP); assert_eq!(count, 4); assert!(!predicated); } _ => panic!("expected Type3"), } assert_eq!(hdr.total_dwords, 5); } #[test] fn opcode_names_are_present_for_common_ops() { assert_eq!(type3_opcode_name(PM4_NOP), "NOP"); assert_eq!(type3_opcode_name(PM4_DRAW_INDX), "DRAW_INDX"); assert_eq!(type3_opcode_name(PM4_XE_SWAP), "XE_SWAP"); assert_eq!(type3_opcode_name(PM4_WAIT_REG_MEM), "WAIT_REG_MEM"); assert_eq!(type3_opcode_name(0xFE), "UNKNOWN"); } #[test] fn make_packet_helpers_round_trip_through_decode() { // Type-0: SHADER_CONSTANT_FETCH_00_0 (0x4800), count=6. let t0 = make_packet_type0(0x4800, 6); match decode(t0).kind { PacketKind::Type0 { base_index, count, write_one } => { assert_eq!(base_index, 0x4800); assert_eq!(count, 6); assert!(!write_one); } other => panic!("expected Type0, got {other:?}"), } assert_eq!(decode(t0).total_dwords, 7); // Type-3: PM4_XE_SWAP, count=4 (signature + addr + W + H). let t3 = make_packet_type3(PM4_XE_SWAP, 4); match decode(t3).kind { PacketKind::Type3 { opcode, count, predicated } => { assert_eq!(opcode, PM4_XE_SWAP); assert_eq!(count, 4); assert!(!predicated); } other => panic!("expected Type3, got {other:?}"), } assert_eq!(decode(t3).total_dwords, 5); // Type-2: NOP. let t2 = make_packet_type2(); assert_eq!(t2, 0x8000_0000); assert_eq!(decode(t2).kind, PacketKind::Type2); assert_eq!(decode(t2).total_dwords, 1); } }