use std::collections::BTreeMap;
use d3d9_reader::{Comment, Declared, Item, Literal, Modifier, RegisterType, TextureType};
use iridia_interface::{Stage, TextureDim};
use d3d9_reader::ConstantTable;
use crate::{Result, error};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Register {
Temporary(u32),
Input(u32),
Constant(u32),
Sampler(u32),
Target(u32),
Output(u32),
Address,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Source {
pub register: Register,
pub swizzle: [u8; 4],
pub absolute: bool,
pub negate: bool,
pub relative: Option<u8>,
}
impl Source {
pub fn read_mask(self, needed: u8) -> u8 {
self.swizzle
.iter()
.enumerate()
.filter(|(n, _)| needed & (1 << n) != 0)
.fold(0, |mask, (_, lane)| mask | (1 << lane))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Destination {
pub register: Register,
pub mask: u8,
pub saturate: bool,
pub partial_precision: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Opcode {
Nop,
Mov,
Add,
Sub,
Mad,
Mul,
Rcp,
Rsq,
Dp3,
Dp4,
Min,
Max,
Slt,
Sge,
Exp,
Log,
Lrp,
Frc,
Pow,
Abs,
Nrm,
SinCos,
Mova,
TexKill,
Tex,
Cmp,
Dp2Add,
}
impl Opcode {
fn decode(op: u16, at: usize) -> Result<Self> {
use Opcode::*;
Ok(match op {
0 => Nop,
1 => Mov,
2 => Add,
3 => Sub,
4 => Mad,
5 => Mul,
6 => Rcp,
7 => Rsq,
8 => Dp3,
9 => Dp4,
10 => Min,
11 => Max,
12 => Slt,
13 => Sge,
14 => Exp,
15 => Log,
18 => Lrp,
19 => Frc,
32 => Pow,
35 => Abs,
36 => Nrm,
37 => SinCos,
46 => Mova,
65 => TexKill,
66 => Tex,
88 => Cmp,
90 => Dp2Add,
_ => {
let name = match op {
25 => "CALL",
26 => "CALLNZ",
27 => "LOOP",
28 => "RET",
29 => "ENDLOOP",
30 => "LABEL",
33 => "CRS",
34 => "SGN",
38 => "REP",
39 => "ENDREP",
40 => "IF",
41 => "IFC",
42 => "ELSE",
43 => "ENDIF",
44 => "BREAK",
45 => "BREAKC",
47 => "DEFB",
48 => "DEFI",
91 => "DSX",
92 => "DSY",
93 => "TEXLDD",
94 => "SETP",
95 => "TEXLDL",
96 => "BREAKP",
_ => "unrecognized",
};
return Err(error(
at,
format!("opcode {op} {name} outside the supported SM3 subset"),
));
}
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Instruction {
pub word: usize,
pub opcode: Opcode,
pub destination: Option<Destination>,
pub sources: Vec<Source>,
pub control: u8,
}
impl Instruction {
pub fn source(&self, n: usize) -> Result<Source> {
self.sources.get(n).copied().ok_or_else(|| {
error(
self.word,
format!(
"{:?} reads source {n}, where it has {}",
self.opcode,
self.sources.len()
),
)
})
}
pub fn source_mask(&self, operand: usize) -> u8 {
let mask = self.destination.map_or(0, |d| d.mask);
match self.opcode {
Opcode::Rcp | Opcode::Rsq | Opcode::Exp | Opcode::Log | Opcode::Pow | Opcode::SinCos => 1,
Opcode::Dp3 => 7,
Opcode::Dp4 => 15,
Opcode::Nrm => 7 | mask,
Opcode::Dp2Add if operand == 2 => 1,
Opcode::Dp2Add => 3,
_ => mask,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Input {
pub register: u32,
pub usage: u8,
pub index: u8,
pub mask: u8,
pub centroid: bool,
pub partial_precision: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Output {
pub register: u32,
pub usage: u8,
pub index: u8,
pub mask: u8,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Shader {
pub stage: Stage,
pub outputs: Vec<Output>,
pub inputs: Vec<Input>,
pub samplers: BTreeMap<u32, TextureDim>,
pub definitions: BTreeMap<u32, [u32; 4]>,
pub instructions: Vec<Instruction>,
pub comments: Vec<Comment>,
pub constants: Option<ConstantTable>,
}
fn token(bytes: &[u8], word: usize) -> u32 {
word.checked_mul(4)
.and_then(|at| bytes.get(at..at.checked_add(4)?))
.and_then(|b| <[u8; 4]>::try_from(b).ok())
.map_or(0, u32::from_le_bytes)
}
fn register(r: d3d9_reader::Register, at: usize, stage: Stage) -> Result<Register> {
let n = r.number;
let vertex = stage == Stage::Vertex;
Ok(match r.kind {
RegisterType::Temp if n < 32 => Register::Temporary(n),
RegisterType::Input if n < if vertex { 16 } else { 10 } => Register::Input(n),
RegisterType::Const if n < if vertex { 256 } else { 224 } => Register::Constant(n),
RegisterType::Address if vertex => Register::Address,
RegisterType::Output if vertex && n < 12 => Register::Output(n),
RegisterType::ColorOut if !vertex && n < 4 => Register::Target(n),
RegisterType::Sampler if n < if vertex { 4 } else { 16 } => Register::Sampler(n),
kind => {
return Err(error(
at,
format!(
"register type {}, index {n} outside the supported {stage:?} SM3 banks",
kind.code()
),
));
}
})
}
fn destination(
d: &d3d9_reader::Destination,
bytes: &[u8],
declaration: bool,
stage: Stage,
) -> Result<Destination> {
if d.relative.is_some() {
return Err(error(
d.word,
"relative addressing of a destination (not supported)",
));
}
let register = register(d.register, d.word, stage)?;
let allowed = if declaration { !d.saturate } else { !d.centroid };
if d.mask == 0 || d.shift != 0 || !allowed {
return Err(error(
d.word,
format!(
"destination mask/shift/modifier {:#010x}, where the mask is not empty and the shift is 0",
token(bytes, d.word)
),
));
}
Ok(Destination {
register,
mask: d.mask,
saturate: d.saturate,
partial_precision: d.partial_precision,
})
}
fn source(s: &d3d9_reader::Source, stage: Stage) -> Result<Source> {
let register = register(s.register, s.word, stage)?;
let relative = match s.relative {
Some(_) if !(stage == Stage::Vertex && matches!(register, Register::Constant(_))) => {
return Err(error(
s.word,
"parameter marker/reserved bits or relative addressing (not supported)",
));
}
Some(r) => {
let at = r.word.unwrap_or(s.word);
if r.register.kind != RegisterType::Address {
return Err(error(
at,
"relative address token other than the vertex address register a0 (a loop counter aL is not supported)",
));
}
let [x, y, z, w] = r.swizzle;
if !(x == y && y == z && z == w) {
return Err(error(
at,
"relative address token with a modifier or a swizzle that does not replicate one lane",
));
}
Some(x)
}
None => None,
};
let (absolute, negate) = match s.modifier {
Modifier::None => (false, false),
Modifier::Negate => (false, true),
Modifier::Abs => (true, false),
Modifier::AbsNegate => (true, true),
other => {
return Err(error(
s.word,
format!("source modifier {} outside NONE/NEG/ABS/ABSNEG", other.code()),
));
}
};
Ok(Source {
register,
swizzle: s.swizzle,
absolute,
negate,
relative,
})
}
pub fn read(bytes: &[u8]) -> Result<Shader> {
let mut records = d3d9_reader::records(bytes).map_err(|e| error(e.word, e.detail))?;
let stage = match records.version().token() {
0xffff_0300 => Stage::Pixel,
0xfffe_0300 => Stage::Vertex,
_ => {
return Err(error(
0,
format!("version {}: only ps_3_0 and vs_3_0 are lifted", records.version()),
));
}
};
let mut shader = Shader {
stage,
outputs: Vec::new(),
inputs: Vec::new(),
samplers: BTreeMap::new(),
definitions: BTreeMap::new(),
instructions: Vec::new(),
comments: Vec::new(),
constants: None,
};
let mut executable = false;
while let Some(item) = records.next() {
let item = item.map_err(|e| error(e.word, e.detail))?;
let word = item.word();
let instruction = token(bytes, word);
match item {
Item::Comment(comment) => {
if comment.data.first() == Some(&u32::from_le_bytes(*b"CTAB"))
&& let Some(table) = records.constants()
{
for c in &table.constants {
let vertex = stage == Stage::Vertex;
let limit = match c.register_set {
0 | 1 => 16,
3 if vertex => 4,
3 => 16,
_ if vertex => 256,
_ => 224,
};
if u32::from(c.register).saturating_add(u32::from(c.count)) > limit {
return Err(error(
c.word,
format!(
"CTAB register range c{}+{} is empty or out-of-bank (the bank has {limit} registers)",
c.register, c.count
),
));
}
}
shader.constants = Some(table.clone());
}
shader.comments.push(comment);
}
Item::Declaration(dcl) => {
if instruction & 0xf0ff_0000 != 0 {
return Err(error(
word,
format!("instruction controls/predication/coissue {instruction:#010x}"),
));
}
if executable {
return Err(error(word, "DCL after executable code"));
}
let d = destination(&dcl.destination, bytes, true, stage)?;
let at = dcl.destination.word;
if stage == Stage::Vertex && (d.partial_precision || dcl.destination.centroid) {
return Err(error(at, "vertex DCL partial precision or centroid modifier"));
}
let semantic = |declared: Declared| match declared {
Declared::Semantic { usage, index } => Ok((usage, index)),
Declared::Sampler(_) => Err(error(
word,
"DCL of a texture type for a register that is not a sampler",
)),
};
match d.register {
Register::Output(register) => {
let (usage, index) = semantic(dcl.declared)?;
if usage == 4 {
return Err(error(
word.saturating_add(1),
"vertex output usage outside the subset (0 to 13, PSIZE not supported)",
));
}
if usage == 0 && index == 0 && d.mask != 15 {
return Err(error(at, "vertex POSITION0 must declare all four lanes"));
}
if shader.outputs.iter().any(|o| {
(o.register == register && o.mask & d.mask != 0)
|| (o.usage == usage && o.index == index)
}) {
return Err(error(word, "overlapping output masks or duplicate semantic"));
}
shader.outputs.push(Output {
register,
usage,
index,
mask: d.mask,
});
}
Register::Input(register) => {
let (usage, index) = semantic(dcl.declared)?;
if shader
.inputs
.iter()
.any(|i| i.register == register || (i.usage == usage && i.index == index))
{
return Err(error(word, "duplicate input register or semantic"));
}
shader.inputs.push(Input {
register,
usage,
index,
mask: d.mask,
centroid: dcl.destination.centroid,
partial_precision: d.partial_precision,
});
}
Register::Sampler(n) => {
let Declared::Sampler(texture) = dcl.declared else {
return Err(error(word, "sampler DCL without a texture type"));
};
let dim = match texture {
TextureType::D2 => TextureDim::D2,
TextureType::Cube => TextureDim::Cube,
TextureType::Volume => TextureDim::D3,
};
if d.mask != 15
|| d.partial_precision
|| dcl.destination.centroid
|| shader.samplers.insert(n, dim).is_some()
{
return Err(error(word, "duplicate or modified sampler declaration"));
}
}
_ => {
return Err(error(
at,
"DCL destination is neither input, vertex output nor sampler",
));
}
}
}
Item::Definition(def) => {
if instruction & 0xf0ff_0000 != 0 {
return Err(error(
word,
format!("instruction controls/predication/coissue {instruction:#010x}"),
));
}
let Literal::Float(values) = def.literal else {
let (op, name) = match def.literal {
Literal::Integer(_) => (48, "DEFI"),
_ => (47, "DEFB"),
};
return Err(error(
word,
format!("opcode {op} {name} outside the supported SM3 subset"),
));
};
if executable {
return Err(error(word, "DEF after executable code"));
}
let d = destination(&def.destination, bytes, false, stage)?;
let Register::Constant(n) = d.register else {
return Err(error(def.destination.word, "DEF destination is not cN"));
};
if d.mask != 15
|| d.saturate
|| d.partial_precision
|| shader.definitions.insert(n, values).is_some()
{
return Err(error(word, "duplicate or modified DEF"));
}
}
Item::Instruction(i) => {
let op = i.opcode.code();
if i.predicate.is_some()
|| i.coissue
|| (i.control != 0 && i.opcode != d3d9_reader::Opcode::Tex)
{
return Err(error(
word,
format!("instruction controls/predication/coissue {instruction:#010x}"),
));
}
let opcode = Opcode::decode(op, word)?;
let destination = match &i.destination {
Some(d) => Some(destination(d, bytes, false, stage)?),
None => None,
};
if let (Some(d), Some(raw)) = (destination, &i.destination) {
let at = raw.word;
let allowed = if opcode == Opcode::Mova {
d.register == Register::Address
} else {
matches!(
d.register,
Register::Temporary(_) | Register::Target(_) | Register::Output(_)
) || (opcode == Opcode::TexKill && matches!(d.register, Register::Input(_)))
};
if !allowed {
return Err(error(at, format!("{opcode:?} destination {:?}", d.register)));
}
if let Register::Output(n) = d.register {
let declared = shader
.outputs
.iter()
.filter(|o| o.register == n)
.fold(0, |m, o| m | o.mask);
if d.mask & !declared != 0 {
return Err(error(
at,
format!(
"write to undeclared vertex output lanes {:#x} of o{n}, which declares {declared:#x}",
d.mask & !declared
),
));
}
}
if stage == Stage::Vertex && d.partial_precision {
return Err(error(at, "vertex partial precision modifier not supported"));
}
if opcode == Opcode::TexKill && (d.saturate || d.partial_precision) {
return Err(error(at, "modified TEXKILL destination"));
}
}
let sources = i
.sources
.iter()
.map(|s| source(s, stage))
.collect::<Result<Vec<_>>>()?;
shader.instructions.push(Instruction {
word,
opcode,
destination,
sources,
control: i.control,
});
executable = true;
}
}
}
Ok(shader)
}