Skip to main content

rsx_reader/
fragment.rs

1//! RSX fragment instructions. Words in the big-endian microcode are rotated by
2//! 16 bits before field extraction. Inline constants occupy one further slot.
3//! This reads instructions; it neither executes nor lowers them.
4use primase_foundations::bytes::View;
5use std::fmt::Write;
6
7macro_rules! opcodes {
8    ($($name:ident = $code:literal, $arity:literal;)+) => {
9        /// A fragment instruction's opcode.
10        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
11        #[repr(u8)]
12        pub enum Opcode {
13            $(
14                #[doc = concat!("`", stringify!($name), "`, opcode ", stringify!($code), ", with ", stringify!($arity), " sources.")]
15                $name = $code,
16            )+
17        }
18        impl Opcode {
19            fn read(code: u8) -> Option<Self> { match code { $($code => Some(Self::$name),)+ _ => None } }
20            /// The opcode's name in upper case (`MOV`, `TEX`, ...).
21            pub fn mnemonic(self) -> &'static str { match self { $(Self::$name => stringify!($name),)+ } }
22            /// How many sources the instruction reads.
23            pub fn source_count(self) -> usize { match self { $(Self::$name => $arity,)+ } }
24        }
25    }
26}
27
28// The arithmetic/texture vocabulary follows NV_fragment_program[2]. The RSX
29// encodings are described by the public RSX instruction declarations.
30opcodes! {
31    NOP=0x00,0; MOV=0x01,1; MUL=0x02,2; ADD=0x03,2; MAD=0x04,3;
32    DP3=0x05,2; DP4=0x06,2; DST=0x07,2; MIN=0x08,2; MAX=0x09,2;
33    SLT=0x0a,2; SGE=0x0b,2; SLE=0x0c,2; SGT=0x0d,2; SNE=0x0e,2; SEQ=0x0f,2;
34    FRC=0x10,1; FLR=0x11,1; KIL=0x12,0; PK4=0x13,1; UP4=0x14,1;
35    DDX=0x15,1; DDY=0x16,1; TEX=0x17,1; TXP=0x18,1; TXD=0x19,3;
36    RCP=0x1a,1; RSQ=0x1b,1; EX2=0x1c,1; LG2=0x1d,1; LIT=0x1e,1; LRP=0x1f,3;
37    STR=0x20,0; SFL=0x21,0; COS=0x22,1; SIN=0x23,1; PK2=0x24,1; UP2=0x25,1;
38    POW=0x26,2; PKB=0x27,1; UPB=0x28,1; PK16=0x29,1; UP16=0x2a,1;
39    PKG=0x2c,1; UPG=0x2d,1; DP2A=0x2e,3; TXL=0x2f,2; TXB=0x31,2;
40    REFL=0x36,2; DP2=0x38,2; NRM=0x39,1; DIV=0x3a,2; DIVSQ=0x3b,2; LIF=0x3c,1;
41    FENCT=0x3d,0; FENCB=0x3e,0; BRK=0x40,0; CAL=0x41,0;
42    IFE=0x42,0; LOOP=0x43,0; REP=0x44,0; RET=0x45,0;
43}
44
45impl Opcode {
46    /// Whether the instruction samples a texture.
47    pub fn samples_texture(self) -> bool {
48        matches!(self, Self::TEX | Self::TXP | Self::TXD | Self::TXL | Self::TXB)
49    }
50    /// Whether the instruction writes a destination register or condition code.
51    pub fn writes_destination(self) -> bool {
52        !matches!(
53            self,
54            Self::NOP
55                | Self::KIL
56                | Self::FENCT
57                | Self::FENCB
58                | Self::BRK
59                | Self::CAL
60                | Self::IFE
61                | Self::LOOP
62                | Self::REP
63                | Self::RET
64        )
65    }
66}
67
68/// A fragment register.
69#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
70pub struct Register {
71    /// The register number (0 to 63).
72    pub index: u8,
73    /// Whether it is a half-precision register (`h<n>`) rather than a full one (`r<n>`).
74    pub half: bool,
75}
76
77/// What a source operand reads.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum SourceKind {
80    /// A register.
81    Register(Register),
82    /// An interpolated input, by its 4-bit input index.
83    Input(u8),
84    /// The inline constant in the slot after the instruction.
85    Constant,
86}
87
88/// A source operand.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub struct Source {
91    /// What the source reads.
92    pub kind: SourceKind,
93    /// For each lane the component (0 x to 3 w) it reads.
94    pub swizzle: [u8; 4],
95    /// The source is negated.
96    pub negate: bool,
97    /// The source's absolute value is used.
98    pub absolute: bool,
99}
100
101/// The condition code test an instruction executes under.
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub struct Predicate {
104    /// Bit 0 less, bit 1 equal, bit 2 greater. Zero never executes; seven always executes.
105    pub test: u8,
106    /// For each lane the component of the condition code it tests.
107    pub swizzle: [u8; 4],
108    /// Which of the two condition-code registers it tests.
109    pub register: u8,
110}
111
112impl Predicate {
113    /// NV condition-code test, with all three outcomes enabled meaning true.
114    pub fn condition(self) -> &'static str {
115        match self.test {
116            0 => "FL",
117            1 => "LT",
118            2 => "EQ",
119            3 => "LE",
120            4 => "GT",
121            5 => "NE",
122            6 => "GE",
123            7 => "TR",
124            _ => "invalid",
125        }
126    }
127}
128
129/// What a flow-control instruction does.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub enum Control {
132    /// `IFE`: a conditional block.
133    If {
134        /// The byte offset of the else block.
135        else_offset: usize,
136        /// The byte offset where the block ends.
137        end_offset: usize,
138    },
139    /// `LOOP` or `REP`.
140    Loop {
141        /// The iteration count.
142        count: u8,
143        /// The initial counter value.
144        initial: u8,
145        /// The counter increment.
146        increment: u8,
147        /// The byte offset where the loop ends.
148        end_offset: usize,
149    },
150    /// No call-target interpretation or execution is claimed by this reader.
151    Call {
152        /// The instruction's three operand words, as stored.
153        encoded: [u32; 3],
154    },
155    /// `BRK`.
156    Break,
157    /// `RET`.
158    Return,
159}
160
161/// A decoded instruction.
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct Instruction {
164    /// The byte offset of the instruction's first slot.
165    pub offset: usize,
166    /// Its size in bytes: 16, or 32 with an inline constant.
167    pub size: usize,
168    /// The opcode.
169    pub opcode: Opcode,
170    /// Rotated words, including fields not interpreted here.
171    pub words: [u32; 4],
172    /// The instruction is the last of the program.
173    pub end: bool,
174    /// The destination register.
175    pub destination: Register,
176    /// The components written, bit 0 x to bit 3 w.
177    pub write_mask: u8,
178    /// The result goes to the condition code only.
179    pub no_destination: bool,
180    /// The instruction updates a condition-code register.
181    pub update_condition: bool,
182    /// Which condition-code register it updates.
183    pub condition_destination: u8,
184    /// The condition under which it executes.
185    pub predicate: Predicate,
186    /// The result is clamped to 0 to 1.
187    pub saturate: bool,
188    /// The result precision code, as stored.
189    pub precision: u8,
190    /// The texture lookup expands its result (the signed-range flag), as stored.
191    pub expand_texture: bool,
192    /// The texture unit a sampling instruction reads.
193    pub texture_unit: Option<u8>,
194    /// The sources, as many as the opcode reads.
195    pub sources: Vec<Source>,
196    /// The inline constant of the slot after the instruction, as IEEE bits.
197    pub constant: Option<[u32; 4]>,
198    /// Encoded modifiers for each source, and the result scale, retained without floating-point evaluation.
199    pub source_precision: [u8; 3],
200    /// The result scale code, as stored.
201    pub scale: u8,
202    /// The inputs are addressed indirectly (bit 30 of the last operand word). Clear in every instruction of the
203    /// fragment programs on the disc.
204    pub indexed_input: bool,
205    /// Bits 19 to 29 of the last operand word, as stored. Bits 21 to 29 are all ones on every instruction of the
206    /// disc's programs that reads an interpolant and zero on every other; bits 19 and 20 are always zero. It is not an
207    /// address in any of them.
208    pub input_address: u16,
209    /// Bit 31 of the last operand word, as stored: a flag of the interpolant the instruction reads (always clear where
210    /// it reads none). Within one program every read of one interpolant has the same value in 17,689 of the disc's
211    /// 17,691 (program, interpolant) pairs; it is clear on all 10,409 texture lookups' coordinates (texture
212    /// coordinates 0 and 1) and set on most of the reads of texture coordinates 4 to 7. What it selects is not
213    /// established: a program's parameter table names no varying, so the sense (perspective-corrected or not) is not
214    /// tied to any data, and it is read as the bit it is.
215    pub interpolant_flag: bool,
216    /// What a flow-control instruction does.
217    pub control: Option<Control>,
218}
219
220/// A decoded fragment program.
221#[derive(Clone, Debug, PartialEq, Eq)]
222pub struct Program {
223    /// The instructions, in order.
224    pub instructions: Vec<Instruction>,
225    /// The program's size in bytes.
226    pub byte_len: usize,
227}
228
229/// What is wrong with a fragment program, and where.
230#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
231#[error("RSX fragment program at byte {offset}: {detail}")]
232pub struct Error {
233    /// The byte offset in the microcode where the problem is.
234    pub offset: usize,
235    /// What was expected there, or what was found instead.
236    pub detail: String,
237}
238
239fn error(offset: usize, detail: impl Into<String>) -> Error {
240    Error {
241        offset,
242        detail: detail.into(),
243    }
244}
245/// The `width` bits of `n` from bit `shift` up.
246fn bits(n: u32, shift: u32, width: u32) -> u32 {
247    let mask = 1u32.checked_shl(width).map_or(u32::MAX, |m| m.wrapping_sub(1));
248    n.checked_shr(shift).unwrap_or(0) & mask
249}
250/// [`bits`] of at most 8 bits.
251fn bits8(n: u32, shift: u32, width: u32) -> u8 {
252    let [byte, ..] = bits(n, shift, width.min(8)).to_le_bytes();
253    byte
254}
255/// [`bits`] of at most 16 bits.
256fn bits16(n: u32, shift: u32, width: u32) -> u16 {
257    let [a, b, ..] = bits(n, shift, width.min(16)).to_le_bytes();
258    u16::from_le_bytes([a, b])
259}
260fn swizzle(n: u32, shift: u32) -> [u8; 4] {
261    [0, 2, 4, 6].map(|k| bits8(n, shift.saturating_add(k), 2))
262}
263
264/// Decode exactly one big-endian fragment program. Requires END at the exact
265/// end, validates branch boundaries and refuses reserved/unimplemented opcodes.
266/// Inline constants are retained as IEEE bits, never decoded as instructions.
267pub fn decode(bytes: &[u8]) -> Result<Program, Error> {
268    if bytes.is_empty() || !bytes.len().is_multiple_of(16) {
269        return Err(error(0, "fragment microcode must contain complete 16-byte slots"));
270    }
271    let view = View::new(bytes);
272    let slot = |at| -> Result<[u32; 4], Error> {
273        let data = view.slice(at, 16).map_err(|e| error(at, e.to_string()))?;
274        let (words, _) = data.as_chunks::<4>();
275        let [d, a, b, c] = words else {
276            return Err(error(at, "a 16-byte slot"));
277        };
278        Ok([d, a, b, c].map(|w| u32::from_be_bytes(*w).rotate_left(16)))
279    };
280    let mut at = 0;
281    let mut instructions = Vec::new();
282    loop {
283        let words = slot(at)?;
284        let [d, a, b, c] = words;
285        let code = bits8(d, 24, 6) | (bits8(b, 31, 1) << 6);
286        let opcode =
287            Opcode::read(code).ok_or_else(|| error(at, format!("unsupported fragment opcode {code:#x}")))?;
288        let mut sources = Vec::new();
289        for (i, raw) in [a, b, c].into_iter().take(opcode.source_count()).enumerate() {
290            let kind = match bits(raw, 0, 2) {
291                0 => SourceKind::Register(Register {
292                    index: bits8(raw, 2, 6),
293                    half: bits(raw, 8, 1) != 0,
294                }),
295                1 => SourceKind::Input(bits8(d, 13, 4)),
296                2 => SourceKind::Constant,
297                _ => {
298                    return Err(error(
299                        at.saturating_add(4).saturating_add(i.saturating_mul(4)),
300                        "reserved source register type",
301                    ));
302                }
303            };
304            sources.push(Source {
305                kind,
306                swizzle: swizzle(raw, 9),
307                negate: bits(raw, 17, 1) != 0,
308                absolute: bits(raw, if i == 0 { 29 } else { 18 }, 1) != 0,
309            });
310        }
311        let constant = if sources.iter().any(|s| s.kind == SourceKind::Constant) {
312            Some(slot(at.saturating_add(16))?)
313        } else {
314            None
315        };
316        let size = if constant.is_some() { 32 } else { 16 };
317        let control = match opcode {
318            Opcode::IFE => Some(Control::If {
319                else_offset: usize::try_from(b & 0x7fffffff)
320                    .map_err(|_| error(at, "branch address overflow"))?
321                    .checked_mul(4)
322                    .ok_or_else(|| error(at, "branch address overflow"))?,
323                end_offset: usize::try_from(c)
324                    .map_err(|_| error(at, "branch address overflow"))?
325                    .checked_mul(4)
326                    .ok_or_else(|| error(at, "branch address overflow"))?,
327            }),
328            Opcode::LOOP | Opcode::REP => Some(Control::Loop {
329                count: bits8(b, 2, 8),
330                initial: bits8(b, 10, 8),
331                increment: bits8(b, 19, 8),
332                end_offset: usize::try_from(c)
333                    .map_err(|_| error(at, "loop address overflow"))?
334                    .checked_mul(4)
335                    .ok_or_else(|| error(at, "loop address overflow"))?,
336            }),
337            Opcode::CAL => Some(Control::Call { encoded: [a, b, c] }),
338            Opcode::BRK => Some(Control::Break),
339            Opcode::RET => Some(Control::Return),
340            _ => None,
341        };
342        let end = d & 1 != 0;
343        instructions.push(Instruction {
344            offset: at,
345            size,
346            opcode,
347            words,
348            end,
349            destination: Register {
350                index: bits8(d, 1, 6),
351                half: bits(d, 7, 1) != 0,
352            },
353            write_mask: bits8(d, 9, 4),
354            no_destination: bits(d, 30, 1) != 0,
355            update_condition: bits(d, 8, 1) != 0,
356            condition_destination: bits8(a, 30, 1),
357            predicate: Predicate {
358                test: bits8(a, 18, 3),
359                swizzle: swizzle(a, 21),
360                register: bits8(a, 31, 1),
361            },
362            saturate: bits(d, 31, 1) != 0,
363            precision: bits8(d, 22, 2),
364            expand_texture: bits(d, 21, 1) != 0,
365            texture_unit: opcode.samples_texture().then_some(bits8(d, 17, 4)),
366            sources,
367            constant,
368            source_precision: [bits8(b, 19, 3), bits8(b, 22, 3), bits8(b, 25, 3)],
369            scale: bits8(b, 28, 3),
370            indexed_input: bits(c, 30, 1) != 0,
371            input_address: bits16(c, 19, 11),
372            interpolant_flag: bits(c, 31, 1) != 0,
373            control,
374        });
375        at = at.saturating_add(size);
376        if end {
377            if at != bytes.len() {
378                return Err(error(at, "bytes after END instruction"));
379            }
380            break;
381        }
382        if at == bytes.len() {
383            return Err(error(at, "fragment program has no END instruction"));
384        }
385    }
386    let boundary = |offset| offset == bytes.len() || instructions.iter().any(|i| i.offset == offset);
387    for i in &instructions {
388        match i.control {
389            Some(Control::If {
390                else_offset,
391                end_offset,
392            }) if !boundary(else_offset)
393                || !boundary(end_offset)
394                || else_offset < i.offset.saturating_add(i.size)
395                || end_offset < else_offset =>
396            {
397                return Err(error(i.offset, "invalid IF boundaries"));
398            }
399            Some(Control::Loop { end_offset, .. })
400                if !boundary(end_offset) || end_offset < i.offset.saturating_add(i.size) =>
401            {
402                return Err(error(i.offset, "invalid loop boundary"));
403            }
404            _ => {}
405        }
406    }
407    Ok(Program {
408        instructions,
409        byte_len: bytes.len(),
410    })
411}
412
413fn register(r: Register) -> String {
414    format!("{}{}", if r.half { "h" } else { "r" }, r.index)
415}
416fn channels(s: [u8; 4]) -> String {
417    s.into_iter()
418        .map(|n| "xyzw".chars().nth(usize::from(n)).unwrap_or('?'))
419        .collect()
420}
421
422impl Program {
423    /// Stable inspection text. Full predicates and modifiers remain in the typed
424    /// instruction even where an external assembler dialect omits them.
425    pub fn disassemble(&self) -> String {
426        let mut out = String::new();
427        for i in &self.instructions {
428            let _ = write!(out, "{:04x}: {}", i.offset, i.opcode.mnemonic());
429            if i.opcode.writes_destination() {
430                let mask: String = (0..4)
431                    .zip("xyzw".chars())
432                    .filter(|&(n, _)| bits(u32::from(i.write_mask), n, 1) != 0)
433                    .map(|(_, channel)| channel)
434                    .collect();
435                let _ = write!(
436                    out,
437                    " {}.{mask}",
438                    if i.no_destination || i.destination.index == 63 {
439                        format!("cc{}", i.condition_destination)
440                    } else {
441                        register(i.destination)
442                    }
443                );
444            }
445            for s in &i.sources {
446                let source = match s.kind {
447                    SourceKind::Register(r) => register(r),
448                    SourceKind::Input(n) => format!("input[{n}]"),
449                    SourceKind::Constant => format!("{:08x?}", i.constant),
450                };
451                let _ = write!(
452                    out,
453                    ", {}{}{}.{}",
454                    if s.negate { "-" } else { "" },
455                    if s.absolute { "abs " } else { "" },
456                    source,
457                    channels(s.swizzle)
458                );
459            }
460            if let Some(unit) = i.texture_unit {
461                let _ = write!(out, ", tex{unit}");
462            }
463            if let Some(control) = &i.control {
464                let _ = write!(out, " {control:?}");
465            }
466            let _ = writeln!(
467                out,
468                " ; pred={} cc{}.{}, sat={}, precision={}, scale={}, src_precision={:?}, end={}",
469                i.predicate.test,
470                i.predicate.register,
471                channels(i.predicate.swizzle),
472                i.saturate,
473                i.precision,
474                i.scale,
475                i.source_precision,
476                i.end
477            );
478        }
479        out
480    }
481}
482
483pub mod flow;