Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

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}