psx_gte/regs.rs
1//! GTE register accessors.
2//!
3//! These are **macros**, not functions, because the GTE register
4//! index is encoded as a 5-bit immediate field in the instruction --
5//! you can't pass it as a runtime value without either stamping 32
6//! separate functions or generating code at build time. Macros keep
7//! the call site readable (`write_data!(0, value)`) while letting the
8//! assembler emit the exact encoding.
9//!
10//! Every macro emits a hand-built `.word` rather than the mnemonic
11//! (`mtc2`, `mfc2`, `ctc2`, `cfc2`). The `mipsel-sony-psx` LLVM
12//! target gates the coprocessor-2 instruction class behind a CPU
13//! feature that isn't enabled, so passing the mnemonic through the
14//! assembler fails with "instruction requires a CPU feature not
15//! currently enabled". Emitting the raw 32-bit encoding sidesteps
16//! that gate; the hardware still decodes it the same way.
17//!
18//! Encoding (per PSX-SPX, "MIPS Coprocessor 2 Opcodes"):
19//!
20//! ```text
21//! bits 31..26 = 0x12 (COP2)
22//! bits 25..21 = rs -- 0=MFC2, 2=CFC2, 4=MTC2, 6=CTC2
23//! bits 20..16 = rt -- CPU general-purpose register
24//! bits 15..11 = rd -- COP2 data/control register index
25//! bits 10..0 = 0
26//! ```
27//!
28//! We pin the CPU-side register to `$t0` (`$8`) via `in("$8")` /
29//! `out("$8")` operands, so the encoding becomes
30//! `opcode_base | 8<<16 | rd<<11`. The compiler is responsible for
31//! shuttling values into/out of `$t0`, which keeps call sites
32//! unobstructed.
33//!
34//! Register index tables (mirroring `emulator-core::gte`):
35//!
36//! | Data regs | Purpose |
37//! |-----------------|-------------------------------------------|
38//! | 0..=5 | V0, V1, V2 input vectors (packed XY / Z) |
39//! | 6 | RGBC |
40//! | 7 | OTZ (output average Z) |
41//! | 8 | IR0 (scalar accumulator) |
42//! | 9..=11 | IR1, IR2, IR3 (vector accumulators) |
43//! | 12..=15 | SXY FIFO (12..=14 slots, 15 = SXYP push) |
44//! | 16..=19 | SZ FIFO (16=SZ0 oldest … 19=SZ3 newest) |
45//! | 20..=22 | RGB FIFO |
46//! | 24 | MAC0 |
47//! | 25..=27 | MAC1, MAC2, MAC3 |
48//! | 28 / 29 | IRGB (write) / ORGB (read) 5:5:5 view |
49//! | 30 / 31 | LZCS / LZCR |
50//!
51//! | Control regs | Purpose |
52//! |-----------------|-------------------------------------------|
53//! | 0..=4 | RT rotation matrix |
54//! | 5..=7 | TR translation |
55//! | 8..=12 | LLM light-direction matrix |
56//! | 13..=15 | BK background colour (light bias) |
57//! | 16..=20 | LCM light-colour matrix |
58//! | 21..=23 | FC far colour |
59//! | 24 / 25 | OFX / OFY screen offsets |
60//! | 26 | H projection plane |
61//! | 27 / 28 | DQA / DQB depth-cue coefficients |
62//! | 29 / 30 | ZSF3 / ZSF4 AVSZ weights |
63//! | 31 | FLAG (error/saturation bits) |
64
65/// Read a GTE data register (`MFC2`). Expands to a single MFC2 instruction
66/// with the register index baked into the `.word` encoding, followed
67/// by a NOP that fills the one-cycle load-delay slot.
68///
69/// **Why the NOP**: MIPS R3000 MFC2/CFC2 commit their result to the
70/// CPU GPR at the END of the NEXT instruction -- not at the end of
71/// MFC2 itself. Rust's inline-asm output binding (`out("$8") value`)
72/// makes the compiler insert a move-from-$8 right after the `.word`
73/// block, and that move runs in the delay slot → sees the
74/// pre-MFC2 value of $8, not the coprocessor data. The emulator
75/// models this correctly (see `cpu::committing_load`), so without
76/// the NOP every `read_data!` call silently returns stale data.
77///
78/// Encoded as two `.word`s: the MFC2 then a 32-bit zero (the MIPS
79/// canonical NOP = `SLL $0, $0, 0`).
80#[doc(alias = "MFC2")]
81#[macro_export]
82macro_rules! read_data {
83 ($reg:literal) => {{
84 // Out-of-range indices would spill into other encoding fields.
85 const { assert!(($reg as u32) < 32, "GTE register index must be 0..=31") };
86 let value: u32;
87 #[cfg(target_arch = "mips")]
88 // SAFETY: MFC2 copies one GTE data register into $t0. The asm
89 // declares $t0 as its only output, touches no memory or stack
90 // (`nomem`, `nostack`), and its trailing NOP covers the load-delay
91 // slot so the compiler's read of $t0 sees the committed value.
92 // The const assert above keeps `$reg` in 0..=31, so the shifted
93 // index stays in the rd field (bits 15..11).
94 unsafe {
95 core::arch::asm!(
96 ".word {instr}",
97 ".word 0",
98 instr = const $crate::encoding::mfc2(8, $reg as u32),
99 out("$8") value,
100 options(nostack, nomem, preserves_flags)
101 );
102 }
103 #[cfg(not(target_arch = "mips"))]
104 {
105 value = $crate::host::read_data($reg);
106 }
107 value
108 }};
109}
110
111/// Write a GTE data register (`MTC2`). Expands to a single MTC2 instruction.
112#[doc(alias = "MTC2")]
113#[macro_export]
114macro_rules! write_data {
115 ($reg:literal, $value:expr) => {{
116 // Out-of-range indices would spill into other encoding fields.
117 const { assert!(($reg as u32) < 32, "GTE register index must be 0..=31") };
118 let _value: u32 = $value;
119 #[cfg(target_arch = "mips")]
120 // SAFETY: MTC2 copies $t0 into one GTE data register. The asm only
121 // reads $t0 (declared as its input), writes no CPU register, and
122 // touches no memory or stack (`nomem`, `nostack`); GTE state is not
123 // Rust-visible memory.
124 // The const assert above keeps `$reg` in 0..=31, so the shifted
125 // index stays in the rd field (bits 15..11).
126 unsafe {
127 core::arch::asm!(
128 ".word {instr}",
129 instr = const $crate::encoding::mtc2(8, $reg as u32),
130 in("$8") _value,
131 options(nostack, nomem, preserves_flags)
132 );
133 }
134 #[cfg(not(target_arch = "mips"))]
135 {
136 $crate::host::write_data($reg, _value);
137 }
138 }};
139}
140
141/// Read a GTE control register (`CFC2`). Same load-delay concern as
142/// [`read_data!`] -- emits a NOP after the CFC2 to give the result a
143/// cycle to commit to $8 before Rust's compiler-inserted move-from-$8
144/// runs.
145#[doc(alias = "CFC2")]
146#[macro_export]
147macro_rules! read_control {
148 ($reg:literal) => {{
149 // Out-of-range indices would spill into other encoding fields.
150 const { assert!(($reg as u32) < 32, "GTE register index must be 0..=31") };
151 let value: u32;
152 #[cfg(target_arch = "mips")]
153 // SAFETY: CFC2 copies one GTE control register into $t0. The asm
154 // declares $t0 as its only output, touches no memory or stack
155 // (`nomem`, `nostack`), and its trailing NOP covers the load-delay
156 // slot so the compiler's read of $t0 sees the committed value.
157 // The const assert above keeps `$reg` in 0..=31, so the shifted
158 // index stays in the rd field (bits 15..11).
159 unsafe {
160 core::arch::asm!(
161 ".word {instr}",
162 ".word 0",
163 instr = const $crate::encoding::cfc2(8, $reg as u32),
164 out("$8") value,
165 options(nostack, nomem, preserves_flags)
166 );
167 }
168 #[cfg(not(target_arch = "mips"))]
169 {
170 value = $crate::host::read_control($reg);
171 }
172 value
173 }};
174}
175
176/// Write a GTE control register (`CTC2`).
177#[doc(alias = "CTC2")]
178#[macro_export]
179macro_rules! write_control {
180 ($reg:literal, $value:expr) => {{
181 // Out-of-range indices would spill into other encoding fields.
182 const { assert!(($reg as u32) < 32, "GTE register index must be 0..=31") };
183 let _value: u32 = $value;
184 #[cfg(target_arch = "mips")]
185 // SAFETY: CTC2 copies $t0 into one GTE control register. The asm
186 // only reads $t0 (declared as its input), writes no CPU register,
187 // and touches no memory or stack (`nomem`, `nostack`); GTE state is
188 // not Rust-visible memory.
189 // The const assert above keeps `$reg` in 0..=31, so the shifted
190 // index stays in the rd field (bits 15..11).
191 unsafe {
192 core::arch::asm!(
193 ".word {instr}",
194 instr = const $crate::encoding::ctc2(8, $reg as u32),
195 in("$8") _value,
196 options(nostack, nomem, preserves_flags)
197 );
198 }
199 #[cfg(not(target_arch = "mips"))]
200 {
201 $crate::host::write_control($reg, _value);
202 }
203 }};
204}
205
206/// Renamed to [`read_data!`].
207#[deprecated(note = "renamed to `read_data!`")]
208#[macro_export]
209macro_rules! mfc2 {
210 ($($t:tt)*) => { $crate::read_data!($($t)*) };
211}
212
213/// Renamed to [`write_data!`].
214#[deprecated(note = "renamed to `write_data!`")]
215#[macro_export]
216macro_rules! mtc2 {
217 ($($t:tt)*) => { $crate::write_data!($($t)*) };
218}
219
220/// Helper: pack two i16 values into one u32 for MTC2/CTC2 of XY-pair
221/// registers (V0 X/Y, SXY slots, rotation matrix rows, …).
222///
223/// The low 16 bits hold `x`, the high 16 bits hold `y`.
224#[inline(always)]
225pub const fn pack_xy(x: i16, y: i16) -> u32 {
226 ((y as u16 as u32) << 16) | (x as u16 as u32)
227}
228
229/// Inverse of [`pack_xy`] -- split a packed word back into two `i16`s.
230#[inline(always)]
231pub const fn unpack_xy(value: u32) -> (i16, i16) {
232 (value as i16, (value >> 16) as i16)
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn pack_xy_round_trips_positive_values() {
241 assert_eq!(pack_xy(0x100, 0x200), 0x0200_0100);
242 assert_eq!(unpack_xy(0x0200_0100), (0x100, 0x200));
243 }
244
245 #[test]
246 fn pack_xy_preserves_negative_sign() {
247 // -1 packs to 0xFFFF in the low or high half without
248 // contaminating the opposite half.
249 assert_eq!(pack_xy(-1, 5), 0x0005_FFFF);
250 assert_eq!(pack_xy(5, -1), 0xFFFF_0005);
251 assert_eq!(unpack_xy(0xFFFF_0005), (5, -1));
252 assert_eq!(unpack_xy(0x0005_FFFF), (-1, 5));
253 }
254
255 #[test]
256 fn pack_xy_extreme_values() {
257 assert_eq!(pack_xy(i16::MIN, i16::MAX), 0x7FFF_8000);
258 assert_eq!(unpack_xy(0x7FFF_8000), (i16::MIN, i16::MAX));
259 }
260
261 /// Verify the MFC2/MTC2/CFC2/CTC2 encoding tables we hand-coded
262 /// into the macros. If these ever drift from PSX-SPX, every GTE
263 /// operation silently targets the wrong register.
264 #[test]
265 fn cop2_opcode_encodings_match_psx_spx() {
266 // COP2 prefix = 0x12 << 26 = 0x4800_0000.
267 // MFC2: rs=0; MTC2: rs=4; CFC2: rs=2; CTC2: rs=6.
268 // All macros pin rt=$8 which contributes `8 << 16 = 0x0008_0000`.
269 assert_eq!(0x4808_0000u32, (0x12u32 << 26) | (8 << 16));
270 assert_eq!(0x4888_0000u32, (0x12u32 << 26) | (4 << 21) | (8 << 16));
271 assert_eq!(0x4848_0000u32, (0x12u32 << 26) | (2 << 21) | (8 << 16));
272 assert_eq!(0x48C8_0000u32, (0x12u32 << 26) | (6 << 21) | (8 << 16));
273 }
274}