Skip to main content

rustyn64_rdp/
command.rs

1//! RDP command-stream decoding.
2//!
3//! The front of the DP FIFO: given a command's first word, how many 64-bit
4//! words does the whole command occupy? Getting this right for every opcode
5//! `0x00`–`0x3F` is what keeps `DPC_CURRENT` aligned, so a multi-word primitive
6//! (a triangle, a texture rectangle) is consumed whole and the stream never
7//! desyncs the pointer mid-command.
8//!
9//! Reference: `n64brew_wiki/markdown/Reality Display Processor/Commands.md`
10//! (the full opcode map). This module decodes *length* only — dispatching each
11//! opcode to a rasterizer handler is later Phase 3 work.
12
13/// The opcode field of an RDP command: bits 61:56 of the command's first 64-bit
14/// word, i.e. bits 29:24 of that word's high half. Six bits, `0x00`–`0x3F`.
15#[must_use]
16pub const fn opcode_of(word0_hi: u32) -> u8 {
17    ((word0_hi >> 24) & 0x3F) as u8
18}
19
20/// The length, in 64-bit (8-byte) words, of the RDP command with opcode
21/// `opcode` (as returned by [`opcode_of`]). Includes the header word itself.
22///
23/// Every command is a single word except:
24///
25/// - **Fill Triangle** (`0x08`–`0x0F`): a 4-word base plus optional coefficient
26///   blocks. The opcode's low three bits *are* the enable flags — bit 2 shade,
27///   bit 1 texture, bit 0 z-buffer (the very bits 58/57/56 the wiki also lists
28///   by name in word 0) — appending 8, 8, and 2 words respectively, in that
29///   order. So `0x08` (plain) is 4 words and `0x0F` (shade+texture+z) is 22.
30/// - **Texture Rectangle** / **Texture Rectangle Flip** (`0x24`/`0x25`):
31///   2 words.
32///
33/// Every other opcode — including the no-operation ranges (`0x00`–`0x07`,
34/// `0x10`–`0x23`, `0x31`) and any not-yet-handled command — is a single word,
35/// so an unrecognized command consumes exactly its header and the FIFO keeps
36/// its alignment.
37#[must_use]
38pub const fn command_len_words(opcode: u8) -> u32 {
39    match opcode & 0x3F {
40        0x08..=0x0F => {
41            // The low three opcode bits select the appended coefficient blocks.
42            let shade = ((opcode >> 2) & 1) as u32;
43            let texture = ((opcode >> 1) & 1) as u32;
44            let zbuffer = (opcode & 1) as u32;
45            4 + shade * 8 + texture * 8 + zbuffer * 2
46        }
47        0x24 | 0x25 => 2,
48        _ => 1,
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    /// `opcode_of` reads bits 61:56 of the 64-bit command word (bits 29:24 of
57    /// the high half), masking off everything above the 6-bit field.
58    #[test]
59    fn opcode_of_reads_bits_61_to_56() {
60        assert_eq!(opcode_of(0x3F << 24), 0x3F);
61        assert_eq!(opcode_of(0x08 << 24), 0x08);
62        // Upper bits (63:62) are not part of the opcode and are ignored.
63        assert_eq!(opcode_of(0xFF00_0000), 0x3F);
64    }
65
66    /// The eight triangle forms `0x08`–`0x0F` decode to the exact lengths the
67    /// N64brew command map gives: 4-word base, +8 shade, +8 texture, +2 z, with
68    /// the appended blocks selected by the opcode's low three bits.
69    #[test]
70    fn triangle_lengths_match_the_command_map() {
71        assert_eq!(command_len_words(0x08), 4, "Fill Triangle (base)");
72        assert_eq!(command_len_words(0x09), 6, "Fill Triangle (Z): +2");
73        assert_eq!(command_len_words(0x0A), 12, "Fill Triangle (T): +8");
74        assert_eq!(command_len_words(0x0B), 14, "Fill Triangle (TZ): +8+2");
75        assert_eq!(command_len_words(0x0C), 12, "Fill Triangle (S): +8");
76        assert_eq!(command_len_words(0x0D), 14, "Fill Triangle (SZ): +8+2");
77        assert_eq!(command_len_words(0x0E), 20, "Fill Triangle (ST): +8+8");
78        assert_eq!(command_len_words(0x0F), 22, "Fill Triangle (STZ): +8+8+2");
79    }
80
81    /// The texture-rectangle pair is two words; every other opcode across the
82    /// whole `0x00`–`0x3F` map — no-ops, syncs, set-state, load, fill — is a
83    /// single word. Exhaustive so a wrong length anywhere is caught.
84    #[test]
85    fn every_non_triangle_opcode_has_its_documented_length() {
86        for opcode in 0x00u8..=0x3F {
87            let expected = match opcode {
88                0x08..=0x0F => continue, // covered above
89                0x24 | 0x25 => 2,
90                _ => 1,
91            };
92            assert_eq!(
93                command_len_words(opcode),
94                expected,
95                "opcode {opcode:#04x} length"
96            );
97        }
98    }
99
100    /// The high two bits of the opcode byte (which are not part of the 6-bit
101    /// field) never change the decoded length.
102    #[test]
103    fn length_ignores_bits_above_the_opcode_field() {
104        for opcode in 0x00u8..=0x3F {
105            assert_eq!(
106                command_len_words(opcode),
107                command_len_words(opcode | 0xC0),
108                "opcode {opcode:#04x} masks to six bits"
109            );
110        }
111    }
112}