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 ([`InputSource::from_index`] names it).
83    Input(u8),
84    /// The inline constant in the slot after the instruction.
85    Constant,
86}
87
88/// The interpolant an input operand reads, from its 4-bit input index ([`SourceKind::Input`]).
89///
90/// On the programs of Tony Hawk's SHRED (3,267 distinct fragment programs) the indices read are 1 (9 programs), 4 to 11
91/// and 13. Index `4 + k` is texture coordinate set `k`: the program header's `tex_coords_input_mask` has bit `k` set
92/// exactly when index `4 + k` is read, in all 3,267 programs for `k` 0 to 7 and 9 (`k` 8 is never read and its bit is
93/// never set). Index 1 is the first colour: the header's `attribute_input_mask` has bits 0 and 2 set in the nine
94/// programs that read it. Indices 0, 2, 3, 12 and 14 are never read there: [`InputSource::TexCoord`] names 12 by the
95/// same rule, the other four are left as [`InputSource::Other`].
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum InputSource {
98    /// Input index 1, the first colour.
99    Color0,
100    /// Input index `4 + k`, texture coordinate set `k` (0 to 9).
101    TexCoord(u8),
102    /// An index that is neither (0, 2, 3, 14, 15 and the 4-bit values nothing names).
103    Other(u8),
104}
105
106impl InputSource {
107    /// The first input index of the texture coordinate sets.
108    const TEXCOORD_BASE: u8 = 4;
109    /// How many texture coordinate sets the indices reach.
110    const TEXCOORDS: u8 = 10;
111
112    /// The interpolant input index `index` (the 4-bit field of the instruction) reads.
113    pub fn from_index(index: u8) -> Self {
114        match index {
115            1 => Self::Color0,
116            i => match i.checked_sub(Self::TEXCOORD_BASE) {
117                Some(k) if k < Self::TEXCOORDS => Self::TexCoord(k),
118                _ => Self::Other(i),
119            },
120        }
121    }
122
123    /// The input index of this interpolant: the inverse of [`InputSource::from_index`].
124    pub fn index(self) -> u8 {
125        match self {
126            Self::Color0 => 1,
127            Self::TexCoord(k) => Self::TEXCOORD_BASE.saturating_add(k),
128            Self::Other(i) => i,
129        }
130    }
131}
132
133impl SourceKind {
134    /// The interpolant this operand reads, if it is an input operand.
135    pub fn input_source(self) -> Option<InputSource> {
136        match self {
137            Self::Input(index) => Some(InputSource::from_index(index)),
138            _ => None,
139        }
140    }
141}
142
143/// A source operand.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct Source {
146    /// What the source reads.
147    pub kind: SourceKind,
148    /// For each lane the component (0 x to 3 w) it reads.
149    pub swizzle: [u8; 4],
150    /// The source is negated.
151    pub negate: bool,
152    /// The source's absolute value is used.
153    pub absolute: bool,
154}
155
156/// The condition code test an instruction executes under.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct Predicate {
159    /// Bit 0 less, bit 1 equal, bit 2 greater. Zero never executes; seven always executes.
160    pub test: u8,
161    /// For each lane the component of the condition code it tests.
162    pub swizzle: [u8; 4],
163    /// Which of the two condition-code registers it tests.
164    pub register: u8,
165}
166
167impl Predicate {
168    /// NV condition-code test, with all three outcomes enabled meaning true.
169    pub fn condition(self) -> &'static str {
170        match self.test {
171            0 => "FL",
172            1 => "LT",
173            2 => "EQ",
174            3 => "LE",
175            4 => "GT",
176            5 => "NE",
177            6 => "GE",
178            7 => "TR",
179            _ => "invalid",
180        }
181    }
182}
183
184/// What a flow-control instruction does.
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub enum Control {
187    /// `IFE`: a conditional block.
188    If {
189        /// The byte offset of the else block.
190        else_offset: usize,
191        /// The byte offset where the block ends.
192        end_offset: usize,
193    },
194    /// `LOOP` or `REP`.
195    Loop {
196        /// The iteration count.
197        count: u8,
198        /// The initial counter value.
199        initial: u8,
200        /// The counter increment.
201        increment: u8,
202        /// The byte offset where the loop ends.
203        end_offset: usize,
204    },
205    /// No call-target interpretation or execution is claimed by this reader.
206    Call {
207        /// The instruction's three operand words, as stored.
208        encoded: [u32; 3],
209    },
210    /// `BRK`.
211    Break,
212    /// `RET`.
213    Return,
214}
215
216/// A decoded instruction.
217#[derive(Clone, Debug, PartialEq, Eq)]
218pub struct Instruction {
219    /// The byte offset of the instruction's first slot.
220    pub offset: usize,
221    /// Its size in bytes: 16, or 32 with an inline constant.
222    pub size: usize,
223    /// The opcode.
224    pub opcode: Opcode,
225    /// Rotated words, including fields not interpreted here.
226    pub words: [u32; 4],
227    /// The instruction is the last of the program.
228    pub end: bool,
229    /// The destination register.
230    pub destination: Register,
231    /// The components written, bit 0 x to bit 3 w.
232    pub write_mask: u8,
233    /// The result goes to the condition code only.
234    pub no_destination: bool,
235    /// The instruction updates a condition-code register.
236    pub update_condition: bool,
237    /// Which condition-code register it updates.
238    pub condition_destination: u8,
239    /// The condition under which it executes.
240    pub predicate: Predicate,
241    /// The result is clamped to 0 to 1.
242    pub saturate: bool,
243    /// The result precision code, as stored.
244    pub precision: u8,
245    /// The texture lookup expands its result (the signed-range flag), as stored.
246    pub expand_texture: bool,
247    /// The texture unit a sampling instruction reads.
248    pub texture_unit: Option<u8>,
249    /// The sources, as many as the opcode reads.
250    pub sources: Vec<Source>,
251    /// The inline constant of the slot after the instruction, as IEEE bits.
252    pub constant: Option<[u32; 4]>,
253    /// Encoded modifiers for each source, and the result scale, retained without floating-point evaluation.
254    pub source_precision: [u8; 3],
255    /// The result scale code, as stored.
256    pub scale: u8,
257    /// The inputs are addressed indirectly (bit 30 of the last operand word). Clear in every instruction of the
258    /// fragment programs on the disc.
259    pub indexed_input: bool,
260    /// Bits 19 to 29 of the last operand word, as stored. Bits 21 to 29 are all ones on every instruction of the
261    /// disc's programs that reads an interpolant and zero on every other; bits 19 and 20 are always zero. It is not an
262    /// address in any of them.
263    pub input_address: u16,
264    /// Bit 31 of the last operand word, as stored: a flag of the interpolant the instruction reads (always clear where
265    /// it reads none). Within one program every read of one interpolant has the same value in 17,689 of the disc's
266    /// 17,691 (program, interpolant) pairs; it is clear on all 10,409 texture lookups' coordinates (texture
267    /// coordinates 0 and 1) and set on most of the reads of texture coordinates 4 to 7. What it selects is not
268    /// established: a program's parameter table names no varying, so the sense (perspective-corrected or not) is not
269    /// tied to any data, and it is read as the bit it is.
270    pub interpolant_flag: bool,
271    /// What a flow-control instruction does.
272    pub control: Option<Control>,
273}
274
275/// A decoded fragment program.
276#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct Program {
278    /// The instructions, in order.
279    pub instructions: Vec<Instruction>,
280    /// The program's size in bytes.
281    pub byte_len: usize,
282}
283
284/// What is wrong with a fragment program, and where.
285#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
286#[error("RSX fragment program at byte {offset}: {detail}")]
287pub struct Error {
288    /// The byte offset in the microcode where the problem is.
289    pub offset: usize,
290    /// What was expected there, or what was found instead.
291    pub detail: String,
292}
293
294fn error(offset: usize, detail: impl Into<String>) -> Error {
295    Error {
296        offset,
297        detail: detail.into(),
298    }
299}
300/// The `width` bits of `n` from bit `shift` up.
301fn bits(n: u32, shift: u32, width: u32) -> u32 {
302    let mask = 1u32.checked_shl(width).map_or(u32::MAX, |m| m.wrapping_sub(1));
303    n.checked_shr(shift).unwrap_or(0) & mask
304}
305/// [`bits`] of at most 8 bits.
306fn bits8(n: u32, shift: u32, width: u32) -> u8 {
307    let [byte, ..] = bits(n, shift, width.min(8)).to_le_bytes();
308    byte
309}
310/// [`bits`] of at most 16 bits.
311fn bits16(n: u32, shift: u32, width: u32) -> u16 {
312    let [a, b, ..] = bits(n, shift, width.min(16)).to_le_bytes();
313    u16::from_le_bytes([a, b])
314}
315fn swizzle(n: u32, shift: u32) -> [u8; 4] {
316    [0, 2, 4, 6].map(|k| bits8(n, shift.saturating_add(k), 2))
317}
318
319/// Decode exactly one big-endian fragment program. Requires END at the exact
320/// end, validates branch boundaries and refuses reserved/unimplemented opcodes.
321/// Inline constants are retained as IEEE bits, never decoded as instructions.
322pub fn decode(bytes: &[u8]) -> Result<Program, Error> {
323    if bytes.is_empty() || !bytes.len().is_multiple_of(16) {
324        return Err(error(0, "fragment microcode must contain complete 16-byte slots"));
325    }
326    let view = View::new(bytes);
327    let slot = |at| -> Result<[u32; 4], Error> {
328        let data = view.slice(at, 16).map_err(|e| error(at, e.to_string()))?;
329        let (words, _) = data.as_chunks::<4>();
330        let [d, a, b, c] = words else {
331            return Err(error(at, "a 16-byte slot"));
332        };
333        Ok([d, a, b, c].map(|w| u32::from_be_bytes(*w).rotate_left(16)))
334    };
335    let mut at = 0;
336    let mut instructions = Vec::new();
337    loop {
338        let words = slot(at)?;
339        let [d, a, b, c] = words;
340        let code = bits8(d, 24, 6) | (bits8(b, 31, 1) << 6);
341        let opcode =
342            Opcode::read(code).ok_or_else(|| error(at, format!("unsupported fragment opcode {code:#x}")))?;
343        let mut sources = Vec::new();
344        for (i, raw) in [a, b, c].into_iter().take(opcode.source_count()).enumerate() {
345            let kind = match bits(raw, 0, 2) {
346                0 => SourceKind::Register(Register {
347                    index: bits8(raw, 2, 6),
348                    half: bits(raw, 8, 1) != 0,
349                }),
350                1 => SourceKind::Input(bits8(d, 13, 4)),
351                2 => SourceKind::Constant,
352                _ => {
353                    return Err(error(
354                        at.saturating_add(4).saturating_add(i.saturating_mul(4)),
355                        "reserved source register type",
356                    ));
357                }
358            };
359            sources.push(Source {
360                kind,
361                swizzle: swizzle(raw, 9),
362                negate: bits(raw, 17, 1) != 0,
363                absolute: bits(raw, if i == 0 { 29 } else { 18 }, 1) != 0,
364            });
365        }
366        let constant = if sources.iter().any(|s| s.kind == SourceKind::Constant) {
367            Some(slot(at.saturating_add(16))?)
368        } else {
369            None
370        };
371        let size = if constant.is_some() { 32 } else { 16 };
372        let control = match opcode {
373            Opcode::IFE => Some(Control::If {
374                else_offset: usize::try_from(b & 0x7fffffff)
375                    .map_err(|_| error(at, "branch address overflow"))?
376                    .checked_mul(4)
377                    .ok_or_else(|| error(at, "branch address overflow"))?,
378                end_offset: usize::try_from(c)
379                    .map_err(|_| error(at, "branch address overflow"))?
380                    .checked_mul(4)
381                    .ok_or_else(|| error(at, "branch address overflow"))?,
382            }),
383            Opcode::LOOP | Opcode::REP => Some(Control::Loop {
384                count: bits8(b, 2, 8),
385                initial: bits8(b, 10, 8),
386                increment: bits8(b, 19, 8),
387                end_offset: usize::try_from(c)
388                    .map_err(|_| error(at, "loop address overflow"))?
389                    .checked_mul(4)
390                    .ok_or_else(|| error(at, "loop address overflow"))?,
391            }),
392            Opcode::CAL => Some(Control::Call { encoded: [a, b, c] }),
393            Opcode::BRK => Some(Control::Break),
394            Opcode::RET => Some(Control::Return),
395            _ => None,
396        };
397        let end = d & 1 != 0;
398        instructions.push(Instruction {
399            offset: at,
400            size,
401            opcode,
402            words,
403            end,
404            destination: Register {
405                index: bits8(d, 1, 6),
406                half: bits(d, 7, 1) != 0,
407            },
408            write_mask: bits8(d, 9, 4),
409            no_destination: bits(d, 30, 1) != 0,
410            update_condition: bits(d, 8, 1) != 0,
411            condition_destination: bits8(a, 30, 1),
412            predicate: Predicate {
413                test: bits8(a, 18, 3),
414                swizzle: swizzle(a, 21),
415                register: bits8(a, 31, 1),
416            },
417            saturate: bits(d, 31, 1) != 0,
418            precision: bits8(d, 22, 2),
419            expand_texture: bits(d, 21, 1) != 0,
420            texture_unit: opcode.samples_texture().then_some(bits8(d, 17, 4)),
421            sources,
422            constant,
423            source_precision: [bits8(b, 19, 3), bits8(b, 22, 3), bits8(b, 25, 3)],
424            scale: bits8(b, 28, 3),
425            indexed_input: bits(c, 30, 1) != 0,
426            input_address: bits16(c, 19, 11),
427            interpolant_flag: bits(c, 31, 1) != 0,
428            control,
429        });
430        at = at.saturating_add(size);
431        if end {
432            if at != bytes.len() {
433                return Err(error(at, "bytes after END instruction"));
434            }
435            break;
436        }
437        if at == bytes.len() {
438            return Err(error(at, "fragment program has no END instruction"));
439        }
440    }
441    let boundary = |offset| offset == bytes.len() || instructions.iter().any(|i| i.offset == offset);
442    for i in &instructions {
443        match i.control {
444            Some(Control::If {
445                else_offset,
446                end_offset,
447            }) if !boundary(else_offset)
448                || !boundary(end_offset)
449                || else_offset < i.offset.saturating_add(i.size)
450                || end_offset < else_offset =>
451            {
452                return Err(error(i.offset, "invalid IF boundaries"));
453            }
454            Some(Control::Loop { end_offset, .. })
455                if !boundary(end_offset) || end_offset < i.offset.saturating_add(i.size) =>
456            {
457                return Err(error(i.offset, "invalid loop boundary"));
458            }
459            _ => {}
460        }
461    }
462    Ok(Program {
463        instructions,
464        byte_len: bytes.len(),
465    })
466}
467
468fn register(r: Register) -> String {
469    format!("{}{}", if r.half { "h" } else { "r" }, r.index)
470}
471fn channels(s: [u8; 4]) -> String {
472    s.into_iter()
473        .map(|n| "xyzw".chars().nth(usize::from(n)).unwrap_or('?'))
474        .collect()
475}
476
477impl Program {
478    /// Stable inspection text. Full predicates and modifiers remain in the typed
479    /// instruction even where an external assembler dialect omits them.
480    pub fn disassemble(&self) -> String {
481        let mut out = String::new();
482        for i in &self.instructions {
483            let _ = write!(out, "{:04x}: {}", i.offset, i.opcode.mnemonic());
484            if i.opcode.writes_destination() {
485                let mask: String = (0..4)
486                    .zip("xyzw".chars())
487                    .filter(|&(n, _)| bits(u32::from(i.write_mask), n, 1) != 0)
488                    .map(|(_, channel)| channel)
489                    .collect();
490                let _ = write!(
491                    out,
492                    " {}.{mask}",
493                    if i.no_destination || i.destination.index == 63 {
494                        format!("cc{}", i.condition_destination)
495                    } else {
496                        register(i.destination)
497                    }
498                );
499            }
500            for s in &i.sources {
501                let source = match s.kind {
502                    SourceKind::Register(r) => register(r),
503                    SourceKind::Input(n) => format!("input[{n}]"),
504                    SourceKind::Constant => format!("{:08x?}", i.constant),
505                };
506                let _ = write!(
507                    out,
508                    ", {}{}{}.{}",
509                    if s.negate { "-" } else { "" },
510                    if s.absolute { "abs " } else { "" },
511                    source,
512                    channels(s.swizzle)
513                );
514            }
515            if let Some(unit) = i.texture_unit {
516                let _ = write!(out, ", tex{unit}");
517            }
518            if let Some(control) = &i.control {
519                let _ = write!(out, " {control:?}");
520            }
521            let _ = writeln!(
522                out,
523                " ; pred={} cc{}.{}, sat={}, precision={}, scale={}, src_precision={:?}, end={}",
524                i.predicate.test,
525                i.predicate.register,
526                channels(i.predicate.swizzle),
527                i.saturate,
528                i.precision,
529                i.scale,
530                i.source_precision,
531                i.end
532            );
533        }
534        out
535    }
536}
537
538pub mod flow;