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