use crate::format::{OperandFormat, operand_format};
use crate::opcodes::NAMES;
use crate::{Result, error};
fn low8(v: u32) -> u8 {
let [byte, ..] = v.to_le_bytes();
byte
}
fn low16(v: u32) -> u16 {
let [a, b, ..] = v.to_le_bytes();
u16::from_le_bytes([a, b])
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Register {
Temp,
Input,
Output,
IndexableTemp,
Constant,
Immediate,
Immediate64,
Label,
FunctionBody,
FunctionTable,
Interface,
FunctionInput,
FunctionOutput,
ThisPointer,
CycleCounter,
InnerCoverage,
Texture,
Sampler,
Null,
Uav,
ImmediateBuffer,
PrimitiveId,
InputCoverage,
OutputDepth,
OutputDepthGreaterEqual,
OutputDepthLessEqual,
OutputCoverage,
Rasterizer,
OutputStencilRef,
GroupSharedMemory,
ThreadId,
ThreadGroupId,
ThreadIdInGroup,
ThreadIdInGroupFlattened,
OutputControlPointId,
ForkInstanceId,
JoinInstanceId,
InputControlPoint,
OutputControlPoint,
InputPatchConstant,
InputDomainPoint,
Stream,
GsInstanceId,
}
impl Register {
fn dimensions(self) -> u32 {
match self {
Register::Immediate
| Register::Immediate64
| Register::CycleCounter
| Register::InnerCoverage
| Register::Null
| Register::PrimitiveId
| Register::InputCoverage
| Register::OutputDepth
| Register::OutputDepthGreaterEqual
| Register::OutputDepthLessEqual
| Register::OutputCoverage
| Register::Rasterizer
| Register::OutputStencilRef
| Register::ThreadId
| Register::ThreadGroupId
| Register::ThreadIdInGroup
| Register::ThreadIdInGroupFlattened
| Register::OutputControlPointId
| Register::ForkInstanceId
| Register::JoinInstanceId
| Register::InputDomainPoint
| Register::GsInstanceId => 0,
Register::Constant
| Register::IndexableTemp
| Register::InputControlPoint
| Register::OutputControlPoint
| Register::Interface => 2,
_ => 1,
}
}
fn from_type(n: u32) -> Option<Self> {
Some(match n {
0 => Register::Temp,
1 => Register::Input,
2 => Register::Output,
3 => Register::IndexableTemp,
4 => Register::Immediate,
5 => Register::Immediate64,
10 => Register::Label,
17 => Register::FunctionBody,
18 => Register::FunctionTable,
19 => Register::Interface,
20 => Register::FunctionInput,
21 => Register::FunctionOutput,
29 => Register::ThisPointer,
40 => Register::CycleCounter,
42 => Register::InnerCoverage,
6 => Register::Sampler,
7 => Register::Texture,
8 => Register::Constant,
9 => Register::ImmediateBuffer,
11 => Register::PrimitiveId,
12 => Register::OutputDepth,
13 => Register::Null,
14 => Register::Rasterizer,
15 => Register::OutputCoverage,
16 => Register::Stream,
22 => Register::OutputControlPointId,
23 => Register::ForkInstanceId,
24 => Register::JoinInstanceId,
25 => Register::InputControlPoint,
26 => Register::OutputControlPoint,
27 => Register::InputPatchConstant,
28 => Register::InputDomainPoint,
37 => Register::GsInstanceId,
30 => Register::Uav,
31 => Register::GroupSharedMemory,
32 => Register::ThreadId,
33 => Register::ThreadGroupId,
34 => Register::ThreadIdInGroup,
36 => Register::ThreadIdInGroupFlattened,
35 => Register::InputCoverage,
38 => Register::OutputDepthGreaterEqual,
39 => Register::OutputDepthLessEqual,
41 => Register::OutputStencilRef,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Selection {
Mask(u8),
Swizzle([u8; 4]),
Scalar(u8),
None,
}
#[derive(Clone, Debug)]
pub struct Operand {
pub register: Register,
pub selection: Selection,
pub components: u8,
pub indices: Vec<Index>,
pub immediate: [u32; 4],
pub modifier: u8,
pub precision: u8,
pub non_uniform: bool,
}
#[derive(Clone, Debug)]
pub struct Index {
pub base: u32,
pub relative: Option<Box<Operand>>,
}
#[derive(Clone, Debug)]
pub struct Instruction {
pub word: usize,
pub opcode: u16,
pub control: u32,
pub extensions: Vec<u32>,
pub saturate: bool,
pub nonzero: bool,
pub operands: Vec<Operand>,
pub function: Option<u32>,
}
impl Instruction {
pub fn name(&self) -> &'static str {
opcode_name(self.opcode)
}
pub fn sample_offset(&self) -> Option<[i32; 3]> {
self.extension(1).map(|e| {
let field = |shift: u32| (((e >> shift) & 15) as i32) << 28 >> 28;
[field(9), field(13), field(17)]
})
}
pub fn resource_dimension(&self) -> Option<(u32, u32)> {
self.extension(2).map(|e| ((e >> 6) & 31, (e >> 11) & 0xf_ffff))
}
pub fn return_type(&self) -> Option<u32> {
self.extension(3).map(|e| (e >> 6) & 0xffff)
}
fn extension(&self, kind: u32) -> Option<u32> {
self.extensions.iter().copied().find(|e| e & 63 == kind)
}
pub fn format(&self) -> Option<OperandFormat> {
operand_format(self.opcode)
}
pub fn destinations_and_sources(&self) -> Option<(&[Operand], &[Operand])> {
self.operands.split_at_checked(self.format()?.destinations.len())
}
}
pub fn opcode_name(opcode: u16) -> &'static str {
NAMES
.get(usize::from(opcode))
.copied()
.unwrap_or("unknown opcode")
}
pub type ReturnCodes = [u8; 4];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegisterRange {
pub lower: u32,
pub upper: u32,
pub space: u32,
}
#[derive(Clone, Debug)]
pub struct Declaration {
pub word: usize,
pub opcode: u16,
pub control: u32,
pub kind: Declared,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Declared {
Temps(u32),
IndexableTemp {
register: u32,
size: u32,
components: u32,
},
GlobalFlags(u32),
ConstantBuffer {
slot: u32,
rows: u32,
dynamic: bool,
dimensions: usize,
range: Option<RegisterRange>,
},
Sampler {
slot: u32,
mode: u32,
range: Option<RegisterRange>,
},
Resource {
slot: u32,
dimension: u32,
samples: u32,
returns: ReturnCodes,
token: u32,
range: Option<RegisterRange>,
},
ResourceRaw {
slot: u32,
range: Option<RegisterRange>,
},
ResourceStructured {
slot: u32,
stride: u32,
range: Option<RegisterRange>,
},
UavTyped {
slot: u32,
dimension: u32,
returns: ReturnCodes,
token: u32,
control: u32,
range: Option<RegisterRange>,
},
UavRaw {
slot: u32,
control: u32,
range: Option<RegisterRange>,
},
UavStructured {
slot: u32,
stride: u32,
control: u32,
range: Option<RegisterRange>,
},
ThreadGroup([u32; 3]),
GroupSharedRaw {
register: u32,
bytes: u32,
},
GroupSharedStructured {
register: u32,
stride: u32,
count: u32,
},
Input {
operand: Operand,
interpolation: Option<u32>,
name: Option<u32>,
},
Output {
operand: Operand,
name: Option<u32>,
},
IndexRange {
operand: Operand,
count: u32,
},
Stream(Operand),
ImmediateBuffer(Vec<[u32; 4]>),
ShaderMessage {
id: u32,
format: u32,
text: Vec<u8>,
operands: Vec<Operand>,
},
CustomData {
class: u32,
},
FunctionBody(u32),
FunctionTable {
id: u32,
bodies: Vec<u32>,
},
Interface {
id: u32,
dynamic: bool,
sites: u32,
length: u32,
tables: Vec<u32>,
},
Other {
opcode: u16,
control: u32,
words: Vec<u32>,
},
}
impl Declared {
pub fn registers(&self) -> Option<RegisterRange> {
let (slot, range) = match self {
Declared::ConstantBuffer { slot, range, .. }
| Declared::Sampler { slot, range, .. }
| Declared::Resource { slot, range, .. }
| Declared::ResourceRaw { slot, range }
| Declared::ResourceStructured { slot, range, .. }
| Declared::UavTyped { slot, range, .. }
| Declared::UavRaw { slot, range, .. }
| Declared::UavStructured { slot, range, .. } => (*slot, *range),
_ => return None,
};
Some(range.unwrap_or(RegisterRange {
lower: slot,
upper: slot,
space: 0,
}))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProgramType {
Pixel,
Vertex,
Geometry,
Hull,
Domain,
Compute,
}
impl ProgramType {
pub fn name(self) -> &'static str {
match self {
ProgramType::Pixel => "ps",
ProgramType::Vertex => "vs",
ProgramType::Geometry => "gs",
ProgramType::Hull => "hs",
ProgramType::Domain => "ds",
ProgramType::Compute => "cs",
}
}
}
#[derive(Clone, Debug)]
pub struct Decoded {
pub kind: ProgramType,
pub major: u32,
pub minor: u32,
pub declarations: Vec<Declaration>,
pub instructions: Vec<Instruction>,
}
impl Decoded {
pub fn target(&self) -> String {
format!("{}_{}_{}", self.kind.name(), self.major, self.minor)
}
}
#[derive(Clone)]
struct Cursor<'a> {
words: &'a [u32],
at: usize,
origin: usize,
kind: ProgramType,
sm51: bool,
declaration: bool,
}
impl Cursor<'_> {
fn position(&self) -> usize {
self.origin.saturating_add(self.at)
}
fn take(&mut self) -> Result<u32> {
let value = *self
.words
.get(self.at)
.ok_or_else(|| error(self.position(), "truncated instruction"))?;
self.at = self.at.saturating_add(1);
Ok(value)
}
fn rest(&mut self) -> Vec<u32> {
let rest = self.words.get(self.at..).unwrap_or_default().to_vec();
self.at = self.words.len();
rest
}
fn operand(&mut self, depth: usize) -> Result<Operand> {
let pos = self.position();
if depth > 2 {
return Err(error(pos, "nested relative operand exceeds bound"));
}
let t = self.take()?;
let components = t & 3;
let selection = match components {
0 => Selection::None,
1 => Selection::Scalar(0),
2 => match (t >> 2) & 3 {
0 => Selection::Mask(low8((t >> 4) & 15)),
1 => Selection::Swizzle([4, 6, 8, 10].map(|shift| low8((t >> shift) & 3))),
2 => Selection::Scalar(low8((t >> 4) & 3)),
_ => return Err(error(pos, "reserved component selection")),
},
_ => return Err(error(pos, "unsupported N-component operand")),
};
let kind = (t >> 12) & 255;
let register = Register::from_type(kind)
.ok_or_else(|| error(pos, format!("unsupported operand register type {kind}")))?;
let (mut modifier, mut precision, mut non_uniform) = (0, 0, false);
if t >> 31 != 0 {
let mut ext = self.take()?;
loop {
if ext & 63 != 1 {
return Err(error(pos, "unsupported extended operand (precision/modifier)"));
}
modifier = low8((ext >> 6) & 255);
precision = low8((ext >> 14) & 7);
non_uniform = ext & (1 << 17) != 0;
if ext >> 31 == 0 {
break;
}
ext = self.take()?;
}
if modifier > 3 {
return Err(error(pos, "unsupported extended operand (precision/modifier)"));
}
}
let dimensions = (t >> 20) & 3;
let expected = match (register, self.kind) {
(Register::Input, ProgramType::Geometry | ProgramType::Hull | ProgramType::Domain) => 2,
(Register::Constant, _) if self.sm51 => 3,
(Register::Texture | Register::Sampler | Register::Uav, _) if self.sm51 => {
if self.declaration && depth == 0 { 3 } else { 2 }
}
_ => register.dimensions(),
};
if dimensions != expected {
return Err(error(pos, "unexpected operand index dimensions"));
}
let mut indices = Vec::new();
for shift in [22, 25, 28].into_iter().take(dimensions as usize) {
let representation = (t >> shift) & 7;
if !matches!(representation, 0 | 2 | 3) {
return Err(error(pos, "unsupported 64-bit/reserved index"));
}
let base = if representation != 2 { self.take()? } else { 0 };
let relative = if representation != 0 {
Some(Box::new(self.operand(depth.saturating_add(1))?))
} else {
None
};
indices.push(Index { base, relative });
}
let mut immediate = [0; 4];
match (register, components) {
(Register::Immediate | Register::Immediate64, 1) => immediate = [self.take()?; 4],
(Register::Immediate | Register::Immediate64, 2) => {
for x in &mut immediate {
*x = self.take()?;
}
}
(Register::Immediate | Register::Immediate64, _) => {
return Err(error(pos, "immediate requires one or four components"));
}
_ => {}
}
Ok(Operand {
register,
selection,
components: match components {
0 => 0,
1 => 1,
_ => 4,
},
indices,
immediate,
modifier,
precision,
non_uniform,
})
}
}
const INTERFACE_CALL: u16 = 120;
fn return_codes(token: u32) -> ReturnCodes {
[0, 4, 8, 12].map(|shift| low8((token >> shift) & 15))
}
fn is_declaration(opcode: u16) -> bool {
matches!(opcode, 88..=106 | 113..=116 | 143..=162 | 206)
}
fn shader_message(body: &[u32], origin: usize, kind: ProgramType, sm51: bool) -> Result<Declared> {
let [id, format, characters, count, operand_words, ..] = *body else {
return Err(error(
origin,
format!(
"a shader message of {} words, shorter than its 5-word header",
body.len()
),
));
};
let start = 5usize;
let end = usize::try_from(operand_words)
.ok()
.and_then(|n| start.checked_add(n));
let Some(words) = end.and_then(|end| body.get(start..end)) else {
return Err(error(
origin.saturating_add(4),
format!(
"a shader message's {operand_words} words of operands do not fit its {} words",
body.len()
),
));
};
let mut c = Cursor {
words,
at: 0,
origin: origin.saturating_add(start),
kind,
sm51,
declaration: false,
};
let mut operands = Vec::new();
while c.at < c.words.len() {
operands.push(c.operand(0)?);
}
if operands.len() != usize::try_from(count).unwrap_or(usize::MAX) {
return Err(error(
origin.saturating_add(3),
format!(
"a shader message of {count} operands whose {operand_words} words hold {}",
operands.len()
),
));
}
let bytes: Vec<u8> = body
.get(start.saturating_add(words.len())..)
.unwrap_or_default()
.iter()
.flat_map(|w| w.to_le_bytes())
.collect();
let Some(text) = usize::try_from(characters).ok().and_then(|n| bytes.get(..n)) else {
return Err(error(
origin.saturating_add(2),
format!(
"a shader message of {characters} characters, more than the {} bytes after its operands",
bytes.len()
),
));
};
Ok(Declared::ShaderMessage {
id,
format,
text: text.to_vec(),
operands,
})
}
pub fn decode(words: &[u32]) -> Result<Decoded> {
let (Some(&version), Some(&length)) = (words.first(), words.get(1)) else {
return Err(error(0, "token stream shorter than its header"));
};
if usize::try_from(length).ok() != Some(words.len()) {
return Err(error(
1,
format!(
"the declared length is {length} words, the stream has {}",
words.len()
),
));
}
let kind = match version >> 16 {
0 => ProgramType::Pixel,
1 => ProgramType::Vertex,
2 => ProgramType::Geometry,
3 => ProgramType::Hull,
4 => ProgramType::Domain,
5 => ProgramType::Compute,
other => {
return Err(error(
0,
format!("program type {other} is not a Shader Model 4 or 5 stage"),
));
}
};
let mut result = Decoded {
kind,
major: (version >> 4) & 15,
minor: version & 15,
declarations: Vec::new(),
instructions: Vec::new(),
};
let sm51 = (result.major, result.minor) >= (5, 1);
let mut at = 2;
while let Some(&t) = words.get(at) {
let opcode = low16(t & 0x7ff);
if opcode == 53 {
let len = words
.get(at.saturating_add(1))
.and_then(|&n| usize::try_from(n).ok())
.unwrap_or(0);
let end = at.checked_add(len).filter(|&end| len >= 2 && end <= words.len());
let (Some(end), Some(body)) = (end, words.get(at.saturating_add(2)..at.saturating_add(len)))
else {
return Err(error(
at,
format!(
"customdata of {len} words (at least 2) does not fit the stream's {} words",
words.len()
),
));
};
let class = t >> 11;
let kind = if class == 3 {
let (rows, rest) = body.as_chunks::<4>();
if !rest.is_empty() {
return Err(error(
at,
format!(
"an immediate constant buffer of {} words, expected whole rows of 4",
body.len()
),
));
}
Declared::ImmediateBuffer(rows.to_vec())
} else if class == 4 {
shader_message(body, at.saturating_add(2), kind, sm51)?
} else {
Declared::CustomData { class }
};
result.declarations.push(Declaration {
word: at,
opcode,
control: class,
kind,
});
at = end;
continue;
}
let len = usize::try_from((t >> 24) & 127).unwrap_or(0);
if len == 0 {
return Err(error(at, "instruction length 0 (at least 1)"));
}
let end = at.checked_add(len).filter(|&end| end <= words.len());
let (Some(end), Some(body)) = (end, words.get(at.saturating_add(1)..at.saturating_add(len))) else {
return Err(error(
at,
format!(
"instruction length {len} (at least 1) does not fit the stream's {} words",
words.len()
),
));
};
let mut c = Cursor {
words: body,
at: 0,
origin: at.saturating_add(1),
kind,
sm51,
declaration: is_declaration(opcode),
};
let mut extensions = Vec::new();
let mut extended = t >> 31 != 0;
while extended {
let ext = c.take()?;
extensions.push(ext);
extended = ext >> 31 != 0;
}
let control = (t >> 11) & 0x1fff;
if is_declaration(opcode) {
let kind = declaration(opcode, control, &mut c)?;
if c.at != c.words.len() {
return Err(error(
at,
format!(
"{} words after the operands of {}",
c.words.len().saturating_sub(c.at),
opcode_name(opcode)
),
));
}
result.declarations.push(Declaration {
word: at,
opcode,
control,
kind,
});
} else {
let function = (opcode == INTERFACE_CALL).then(|| c.take()).transpose()?;
let format = operand_format(opcode);
let mut operands = Vec::new();
while c.at < c.words.len() && format.is_none_or(|f| operands.len() < f.operands()) {
operands.push(c.operand(0)?);
}
if let Some(format) = format
&& c.words.get(c.at..).unwrap_or_default().iter().any(|&w| w != 0)
{
let mut extra = c.clone();
let mut count = operands.len();
while extra.at < extra.words.len() && extra.operand(0).is_ok() {
count = count.saturating_add(1);
}
if count > operands.len() && extra.at == extra.words.len() {
return Err(error(
at,
format!(
"{} has {count} operands, its format is {} destinations and {} sources",
opcode_name(opcode),
format.destinations.len(),
format.sources.len()
),
));
}
}
if let Some(format) = format
&& operands.len() != format.operands()
{
return Err(error(
at,
format!(
"{} has {} operands, its format is {} destinations and {} sources",
opcode_name(opcode),
operands.len(),
format.destinations.len(),
format.sources.len()
),
));
}
result.instructions.push(Instruction {
word: at,
opcode,
control,
extensions,
saturate: t & 0x2000 != 0,
nonzero: t & 0x4_0000 != 0,
operands,
function,
});
}
at = end;
}
Ok(result)
}
fn declaration(opcode: u16, control: u32, c: &mut Cursor<'_>) -> Result<Declared> {
let pos = c.origin.saturating_sub(1);
let slot = |o: &Operand| -> Result<u32> {
o.indices
.first()
.map(|i| i.base)
.ok_or_else(|| error(pos, "missing declaration index"))
};
let range = |o: &Operand, c: &mut Cursor<'_>| -> Result<Option<RegisterRange>> {
if !c.sm51 {
return Ok(None);
}
let [_, lower, upper] = o.indices.as_slice() else {
return Err(error(
pos,
"a Shader Model 5.1 binding declaration without its three indices",
));
};
Ok(Some(RegisterRange {
lower: lower.base,
upper: upper.base,
space: c.take()?,
}))
};
Ok(match opcode {
104 => Declared::Temps(c.take()?),
105 => Declared::IndexableTemp {
register: c.take()?,
size: c.take()?,
components: c.take()?,
},
106 => Declared::GlobalFlags(control),
88 => {
let o = c.operand(0)?;
let token = c.take()?;
Declared::Resource {
slot: slot(&o)?,
dimension: control & 31,
samples: (control >> 5) & 127,
returns: return_codes(token),
token,
range: range(&o, c)?,
}
}
89 => {
let o = c.operand(0)?;
let rows = if c.sm51 {
c.take()?
} else {
o.indices.get(1).map_or(0, |i| i.base)
};
Declared::ConstantBuffer {
slot: slot(&o)?,
rows,
dynamic: control & 1 != 0,
dimensions: o.indices.len(),
range: range(&o, c)?,
}
}
90 => {
let o = c.operand(0)?;
Declared::Sampler {
slot: slot(&o)?,
mode: control & 15,
range: range(&o, c)?,
}
}
95..=100 => {
let operand = c.operand(0)?;
let pixel = matches!(opcode, 98..=100);
let interpolation = pixel.then_some(control & 15);
let name = matches!(opcode, 96 | 97 | 99 | 100)
.then(|| c.take())
.transpose()?;
Declared::Input {
operand,
interpolation,
name,
}
}
101..=103 => {
let operand = c.operand(0)?;
let name = (opcode != 101).then(|| c.take()).transpose()?;
Declared::Output { operand, name }
}
156 => {
let o = c.operand(0)?;
let token = c.take()?;
Declared::UavTyped {
slot: slot(&o)?,
dimension: control & 31,
returns: return_codes(token),
token,
control,
range: range(&o, c)?,
}
}
157 => {
let o = c.operand(0)?;
Declared::UavRaw {
slot: slot(&o)?,
control,
range: range(&o, c)?,
}
}
158 => {
let o = c.operand(0)?;
Declared::UavStructured {
slot: slot(&o)?,
stride: c.take()?,
control,
range: range(&o, c)?,
}
}
155 => Declared::ThreadGroup([c.take()?, c.take()?, c.take()?]),
91 => Declared::IndexRange {
operand: c.operand(0)?,
count: c.take()?,
},
143 => Declared::Stream(c.operand(0)?),
159 => {
let o = c.operand(0)?;
Declared::GroupSharedRaw {
register: slot(&o)?,
bytes: c.take()?,
}
}
160 => {
let o = c.operand(0)?;
Declared::GroupSharedStructured {
register: slot(&o)?,
stride: c.take()?,
count: c.take()?,
}
}
161 => {
let o = c.operand(0)?;
Declared::ResourceRaw {
slot: slot(&o)?,
range: range(&o, c)?,
}
}
162 => {
let o = c.operand(0)?;
Declared::ResourceStructured {
slot: slot(&o)?,
stride: c.take()?,
range: range(&o, c)?,
}
}
144 => Declared::FunctionBody(c.take()?),
145 => {
let id = c.take()?;
let count = c.take()?;
let bodies = c.rest();
if bodies.len() != count as usize {
return Err(error(
pos.saturating_add(2),
format!(
"dcl_function_table word 2 counts {count} function bodies, the declaration holds {}",
bodies.len()
),
));
}
Declared::FunctionTable { id, bodies }
}
146 => {
let id = c.take()?;
let sites = c.take()?;
let lengths = c.take()?;
let tables = c.rest();
let count = lengths & 0xffff;
if tables.len() != count as usize {
return Err(error(
pos.saturating_add(3),
format!(
"dcl_interface word 3 counts {count} function tables in its low 16 bits, the declaration \
holds {}",
tables.len()
),
));
}
Declared::Interface {
id,
dynamic: control & 1 != 0,
sites,
length: lengths >> 16,
tables,
}
}
_ => Declared::Other {
opcode,
control,
words: c.rest(),
},
})
}