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}