use std::fmt::Write;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("vertex program at byte {offset}: {detail}")]
pub struct Error {
pub offset: usize,
pub detail: String,
}
fn error(offset: usize, detail: impl Into<String>) -> Error {
Error {
offset,
detail: detail.into(),
}
}
fn bits(n: u32, shift: u32, width: u32) -> u32 {
let mask = 1u32.checked_shl(width).map_or(u32::MAX, |m| m.wrapping_sub(1));
n.checked_shr(shift).unwrap_or(0) & mask
}
fn bits8(n: u32, shift: u32, width: u32) -> u8 {
let [byte, ..] = bits(n, shift, width.min(8)).to_le_bytes();
byte
}
fn bits16(n: u32, shift: u32, width: u32) -> u16 {
let [a, b, ..] = bits(n, shift, width.min(16)).to_le_bytes();
u16::from_le_bytes([a, b])
}
fn flag(n: u32, bit: u32) -> bool {
bits(n, bit, 1) != 0
}
macro_rules! opcodes {
($kind:ident, $doc:literal; $($name:ident = $code:literal;)+) => {
#[doc = $doc]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum $kind {
$(
#[doc = concat!("`", stringify!($name), "`, opcode ", stringify!($code), ".")]
$name = $code,
)+
}
impl $kind {
fn read(code: u8) -> Option<Self> { match code { $($code => Some(Self::$name),)+ _ => None } }
pub fn mnemonic(self) -> &'static str { match self { $(Self::$name => stringify!($name),)+ } }
}
}
}
opcodes! { VectorOpcode, "A vector operation: four lanes at once, up to three sources.";
NOP=0x00; MOV=0x01; MUL=0x02; ADD=0x03; MAD=0x04; DP3=0x05; DPH=0x06; DP4=0x07;
DST=0x08; MIN=0x09; MAX=0x0a; SLT=0x0b; SGE=0x0c; ARL=0x0d; FRC=0x0e; FLR=0x0f;
SEQ=0x10; SFL=0x11; SGT=0x12; SLE=0x13; SNE=0x14; STR=0x15; SSG=0x16; ARR=0x17;
ARA=0x18; TXL=0x19;
}
opcodes! { ScalarOpcode, "A scalar operation: one source, its result replicated to the lanes the write mask names.";
NOP=0x00; MOV=0x01; RCP=0x02; RCC=0x03; RSQ=0x04; EXP=0x05; LOG=0x06; LIT=0x07;
LG2=0x0d; EX2=0x0e; SIN=0x0f; COS=0x10; PUSHA=0x13; POPA=0x14;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SourceKind {
None,
Temporary(u8),
Input,
Constant,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Source {
pub kind: SourceKind,
pub swizzle: [u8; 4],
pub negate: bool,
pub absolute: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Output {
Position,
Color0,
Color1,
BackColor0,
BackColor1,
FogCoord,
PointSize,
TexCoord(u8),
Other(u8),
}
impl Output {
fn from_field(n: u8) -> Self {
match n {
0 => Self::Position,
1 => Self::Color0,
2 => Self::Color1,
3 => Self::BackColor0,
4 => Self::BackColor1,
5 => Self::FogCoord,
6 => Self::PointSize,
7..=16 => Self::TexCoord(n.saturating_sub(7)),
n => Self::Other(n),
}
}
pub fn field(self) -> u8 {
match self {
Self::Position => 0,
Self::Color0 => 1,
Self::Color1 => 2,
Self::BackColor0 => 3,
Self::BackColor1 => 4,
Self::FogCoord => 5,
Self::PointSize => 6,
Self::TexCoord(n) => n.saturating_add(7),
Self::Other(n) => n,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Destination {
None,
Temporary(u8),
Output(Output),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Condition {
pub test: bool,
pub code: u8,
pub swizzle: [u8; 4],
pub register: u8,
pub update: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Instruction {
pub offset: usize,
pub words: [u32; 4],
pub vector: VectorOpcode,
pub scalar: ScalarOpcode,
pub sources: [Source; 3],
pub input: u8,
pub constant: u16,
pub input_relative: bool,
pub constant_relative: bool,
pub address: (u8, u8),
pub vector_destination: Destination,
pub scalar_destination: Destination,
pub vector_mask: u8,
pub scalar_mask: u8,
pub saturate: bool,
pub condition: Condition,
pub last: bool,
pub unassigned: [u32; 4],
}
impl Instruction {
pub fn outputs(&self) -> Vec<Output> {
let mut out = Vec::new();
for (destination, mask, present) in [
(
self.vector_destination,
self.vector_mask,
self.vector != VectorOpcode::NOP,
),
(
self.scalar_destination,
self.scalar_mask,
self.scalar != ScalarOpcode::NOP,
),
] {
if let Destination::Output(o) = destination
&& mask != 0
&& present
&& !out.contains(&o)
{
out.push(o);
}
}
out
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Program {
pub instructions: Vec<Instruction>,
}
impl Program {
pub fn input_mask(&self) -> Option<u32> {
let mut mask = 0u32;
for i in &self.instructions {
if i.sources.iter().any(|s| s.kind == SourceKind::Input) {
if i.input_relative {
return None;
}
mask |= 1u32.checked_shl(u32::from(i.input)).unwrap_or(0);
}
}
Some(mask)
}
pub fn output_mask(&self) -> u32 {
let mut mask = 0u32;
let mut slot6 = 0u8;
for i in &self.instructions {
for (destination, lanes, present) in [
(i.vector_destination, i.vector_mask, i.vector != VectorOpcode::NOP),
(i.scalar_destination, i.scalar_mask, i.scalar != ScalarOpcode::NOP),
] {
let Destination::Output(output) = destination else {
continue;
};
if lanes == 0 || !present {
continue;
}
match output.field() {
0 => {}
6 => slot6 |= lanes,
n @ 1..=5 => mask |= 1u32.checked_shl(u32::from(n).saturating_sub(1)).unwrap_or(0),
n => mask |= 1u32.checked_shl(u32::from(n).saturating_add(7)).unwrap_or(0),
}
}
}
if slot6 != 0 {
mask |= if slot6 == 1 { 1 << 5 } else { 1 << 13 };
}
mask
}
pub fn register_count(&self) -> u32 {
let mut highest = 0u32;
for i in &self.instructions {
let written = [i.vector_destination, i.scalar_destination]
.into_iter()
.filter_map(|d| match d {
Destination::Temporary(n) => Some(u32::from(n)),
_ => None,
});
let read = i.sources.iter().filter_map(|s| match s.kind {
SourceKind::Temporary(n) => Some(u32::from(n)),
_ => None,
});
for n in written.chain(read) {
highest = highest.max(n.saturating_add(1));
}
}
highest.max(1)
}
}
const UNASSIGNED: [u32; 4] = [(1 << 31) | (1 << 28), 0, 0, 0];
fn source(raw: u32) -> Source {
let kind = match bits(raw, 0, 2) {
0 => SourceKind::None,
1 => SourceKind::Temporary(bits8(raw, 2, 5)),
2 => SourceKind::Input,
_ => SourceKind::Constant,
};
Source {
kind,
swizzle: [14, 12, 10, 8].map(|shift| bits8(raw, shift, 2)),
negate: flag(raw, 16),
absolute: false,
}
}
fn destination(result: bool, temporary: u8, none: u8, field: u8) -> Destination {
if result {
Destination::Output(Output::from_field(field))
} else if temporary == none {
Destination::None
} else {
Destination::Temporary(temporary)
}
}
pub fn decode(bytes: &[u8]) -> Result<Program, Error> {
if bytes.is_empty() || !bytes.len().is_multiple_of(16) {
return Err(error(0, "vertex microcode must contain complete 16-byte slots"));
}
let (slots, _) = bytes.as_chunks::<16>();
let mut instructions = Vec::with_capacity(slots.len());
for (n, slot) in slots.iter().enumerate() {
let offset = n.saturating_mul(16);
let (words, _) = slot.as_chunks::<4>();
let [w0, w1, w2, w3] = words else {
return Err(error(offset, "a slot is four words"));
};
let [w0, w1, w2, w3] = [w0, w1, w2, w3].map(|w| u32::from_be_bytes(*w));
let vec_code = bits8(w1, 22, 5);
let sca_code = bits8(w1, 27, 5);
let vector = VectorOpcode::read(vec_code)
.ok_or_else(|| error(offset, format!("vector opcode {vec_code:#04x} has no entry")))?;
let scalar = ScalarOpcode::read(sca_code)
.ok_or_else(|| error(offset, format!("scalar opcode {sca_code:#04x} has no entry")))?;
let raw0 = bits(w1, 0, 8).wrapping_shl(9) | bits(w2, 23, 9);
let raw1 = bits(w2, 6, 17);
let raw2 = bits(w2, 0, 6).wrapping_shl(11) | bits(w3, 21, 11);
let mut sources = [raw0, raw1, raw2].map(source);
for (s, bit) in sources.iter_mut().zip([21, 22, 23]) {
s.absolute = flag(w0, bit);
}
let dest_field = bits8(w3, 2, 5);
let mask = |w: u32, top: u32| -> u8 {
(0..4).fold(0u8, |m, lane| {
let set = flag(w, top.saturating_sub(lane));
m | u8::from(set).wrapping_shl(lane)
})
};
instructions.push(Instruction {
offset,
words: [w0, w1, w2, w3],
vector,
scalar,
sources,
input: bits8(w1, 8, 4),
constant: bits16(w1, 12, 10),
input_relative: flag(w0, 27),
constant_relative: flag(w3, 1),
address: (bits8(w0, 24, 1), bits8(w0, 0, 2)),
vector_destination: destination(flag(w0, 30), bits8(w0, 15, 6), 0x3f, dest_field),
scalar_destination: destination(flag(w3, 12), bits8(w3, 7, 5), 0x1f, dest_field),
vector_mask: mask(w3, 16),
scalar_mask: mask(w3, 20),
saturate: flag(w0, 26),
condition: Condition {
test: flag(w0, 13),
code: bits8(w0, 10, 3),
swizzle: [8, 6, 4, 2].map(|shift| bits8(w0, shift, 2)),
register: bits8(w0, 25, 1),
update: flag(w0, 29),
},
last: flag(w3, 0),
unassigned: [
w0 & UNASSIGNED[0],
w1 & UNASSIGNED[1],
w2 & UNASSIGNED[2],
w3 & UNASSIGNED[3],
],
});
}
for (n, i) in instructions.iter().enumerate() {
let is_final = n.saturating_add(1) == instructions.len();
if i.last != is_final {
return Err(error(
i.offset,
if is_final {
"the last slot is not flagged as the program's last"
} else {
"a slot before the end is flagged as the program's last"
},
));
}
}
Ok(Program { instructions })
}
fn channels(mask: u8) -> String {
"xyzw"
.chars()
.enumerate()
.filter(|&(n, _)| bits(u32::from(mask), u32::try_from(n).unwrap_or(0), 1) != 0)
.map(|(_, c)| c)
.collect()
}
fn lanes(s: [u8; 4]) -> String {
s.into_iter()
.map(|n| "xyzw".chars().nth(usize::from(n)).unwrap_or('?'))
.collect()
}
impl Program {
pub fn disassemble(&self) -> String {
let mut out = String::new();
for i in &self.instructions {
let _ = write!(out, "{:04x}:", i.offset);
let render = |d: Destination| match d {
Destination::None => "-".to_owned(),
Destination::Temporary(n) => format!("R{n}"),
Destination::Output(o) => format!("o[{o:?}]"),
};
if i.vector != VectorOpcode::NOP {
let _ = write!(
out,
" {} {}.{}",
i.vector.mnemonic(),
render(i.vector_destination),
channels(i.vector_mask)
);
}
if i.scalar != ScalarOpcode::NOP {
let _ = write!(
out,
" {} {}.{}",
i.scalar.mnemonic(),
render(i.scalar_destination),
channels(i.scalar_mask)
);
}
for s in &i.sources {
let text = match s.kind {
SourceKind::None => continue,
SourceKind::Temporary(n) => format!("R{n}"),
SourceKind::Input if i.input_relative => {
format!("v[A{}.{}+{}]", i.address.0, i.address.1, i.input)
}
SourceKind::Input => format!("v[{}]", i.input),
SourceKind::Constant if i.constant_relative => {
format!("c[A{}.{}+{}]", i.address.0, i.address.1, i.constant)
}
SourceKind::Constant => format!("c[{}]", i.constant),
};
let _ = write!(
out,
", {}{}{}.{}",
if s.negate { "-" } else { "" },
if s.absolute { "|" } else { "" },
text,
lanes(s.swizzle)
);
}
let _ = writeln!(
out,
" ; sat={} cond={}{} last={}",
i.saturate,
if i.condition.test { "on " } else { "off " },
i.condition.code,
i.last
);
}
out
}
}