use primase_foundations::bytes::{Endian::Big, View};
use std::ops::Range;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("CgBinaryProgram 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(),
}
}
pub fn components(ty: u32) -> Option<usize> {
let vector = |start: u32, first: u32| ty.saturating_sub(start).saturating_add(first);
let matrix = |start: u32| {
let m = ty.saturating_sub(start);
(m / 4)
.saturating_add(1)
.saturating_mul((m % 4).saturating_add(1))
};
let n = match ty {
1025..=1028 => vector(1025, 1), 1029..=1044 => matrix(1029), 1045..=1048 => vector(1045, 1), 1049..=1064 => matrix(1049), 1070..=1073 => vector(1070, 1), 1074..=1089 => matrix(1074), 1090..=1094 => 1, 1095..=1097 => vector(1095, 2), 1098..=1113 => matrix(1098), 1114 | 1115 => 1, 1116..=1118 => vector(1116, 2), 1119..=1134 => matrix(1119), _ => return None, };
usize::try_from(n).ok()
}
fn plus(at: usize, delta: usize) -> Result<usize, Error> {
at.checked_add(delta)
.ok_or_else(|| error(at, "offset overflows the address space"))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Vertex {
pub instruction_count: u32,
pub instruction_slot: u32,
pub register_count: u32,
pub attribute_input_mask: u32,
pub attribute_output_mask: u32,
pub user_clip_mask: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Fragment {
pub instruction_count: u32,
pub attribute_input_mask: u32,
pub partial_tex_type: u32,
pub tex_coords_input_mask: u16,
pub tex_coords_2d: u16,
pub tex_coords_centroid: u16,
pub register_count: u8,
pub output_from_h0: bool,
pub depth_replace: bool,
pub pixel_kill: bool,
pub padding: [u8; 2],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Stage {
Vertex(Vertex),
Fragment(Fragment),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Parameter<'a> {
pub ty: u32,
pub resource: u32,
pub variability: u32,
pub resource_index: i32,
pub name: Option<&'a [u8]>,
pub semantic: Option<&'a [u8]>,
pub direction: u32,
pub parameter_number: i32,
pub referenced: bool,
pub shared: bool,
pub default_value: Option<Vec<u32>>,
pub embedded_constants: Vec<u32>,
}
impl Parameter<'_> {
pub fn texture_unit(&self) -> Option<u32> {
self.resource.checked_sub(2048).filter(|&n| n < 16)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Program<'a> {
pub profile: u32,
pub revision: u32,
pub stage: Stage,
pub parameters: Vec<Parameter<'a>>,
pub microcode: &'a [u8],
pub bytes: &'a [u8],
}
struct Reader<'a> {
bytes: &'a [u8],
fixed: Vec<Range<usize>>,
}
impl<'a> Reader<'a> {
fn bytes(&self, at: usize, n: usize) -> Result<&'a [u8], Error> {
View::new(self.bytes)
.slice(at, n)
.map_err(|e| error(at, e.to_string()))
}
fn word(&self, at: usize) -> Result<u32, Error> {
View::new(self.bytes)
.u32(at, Big)
.map_err(|e| error(at, e.to_string()))
}
fn offset(&self, at: usize) -> Result<usize, Error> {
usize::try_from(self.word(at)?).map_err(|_| error(at, "offset outside address space"))
}
fn boolean(&self, at: usize, byte: bool) -> Result<bool, Error> {
let n = if byte {
u32::from(
*self
.bytes(at, 1)?
.first()
.ok_or_else(|| error(at, "a boolean byte"))?,
)
} else {
self.word(at)?
};
match n {
0 => Ok(false),
1 => Ok(true),
_ => Err(error(at, "boolean is neither zero nor one")),
}
}
fn range(&self, at: usize, count: usize, stride: usize) -> Result<Range<usize>, Error> {
let n = count
.checked_mul(stride)
.ok_or_else(|| error(at, "array size overflow"))?;
self.bytes(at, n)?;
Ok(at..plus(at, n)?)
}
fn reserve(&mut self, r: Range<usize>) -> Result<(), Error> {
if !r.start.is_multiple_of(4) {
return Err(error(r.start, "unaligned structure"));
}
self.metadata(r.clone())?;
if !r.is_empty() {
self.fixed.push(r);
}
Ok(())
}
fn metadata(&self, r: Range<usize>) -> Result<(), Error> {
if !r.is_empty() && self.fixed.iter().any(|f| r.start < f.end && f.start < r.end) {
return Err(error(
r.start,
"overlapping structures or metadata inside a fixed structure",
));
}
Ok(())
}
fn string(&self, field: usize) -> Result<Option<&'a [u8]>, Error> {
let at = self.offset(field)?;
if at == 0 {
return Ok(None);
}
let tail = self
.bytes
.get(at..)
.ok_or_else(|| error(field, "string offset outside container"))?;
let len = tail
.iter()
.position(|&b| b == 0)
.ok_or_else(|| error(at, "unterminated string"))?;
let r = self.range(at, plus(len, 1)?, 1)?;
self.metadata(r)?;
Ok(Some(self.bytes(at, len)?))
}
fn floats(&self, field: usize, ty: u32) -> Result<Option<Vec<u32>>, Error> {
let at = self.offset(field)?;
if at == 0 {
return Ok(None);
}
let count = components(ty).ok_or_else(|| {
error(
field,
format!("a default value for CGtype {ty}, which is not a numeric type"),
)
})?;
if !at.is_multiple_of(4) {
return Err(error(field, "unaligned default value"));
}
let r = self.range(at, count, 4)?;
self.metadata(r.clone())?;
r.step_by(4)
.map(|p| self.word(p))
.collect::<Result<Vec<_>, _>>()
.map(Some)
}
fn embedded(&self, field: usize, ucode_size: usize) -> Result<Vec<u32>, Error> {
let at = self.offset(field)?;
if at == 0 {
return Ok(Vec::new());
}
if !at.is_multiple_of(4) {
return Err(error(field, "unaligned embedded constant table"));
}
let count = self.offset(at)?;
let r = self.range(
at,
count
.checked_add(1)
.ok_or_else(|| error(at, "constant count overflow"))?,
4,
)?;
self.metadata(r.clone())?;
let mut offsets = Vec::new();
for field in r.step_by(4).skip(1) {
let offset = self.word(field)?;
let n = usize::try_from(offset).map_err(|_| error(field, "constant outside address space"))?;
if !n.is_multiple_of(16) || n.checked_add(16).is_none_or(|end| end > ucode_size) {
return Err(error(
field,
"embedded constant is unaligned or outside microcode",
));
}
offsets.push(offset);
}
Ok(offsets)
}
}
pub fn parse(bytes: &[u8]) -> Result<Program<'_>, Error> {
let p = parse_prefix(bytes)?;
if p.bytes.len() != bytes.len() {
return Err(error(p.bytes.len(), "trailing bytes after container"));
}
Ok(p)
}
pub fn parse_prefix(bytes: &[u8]) -> Result<Program<'_>, Error> {
let header = Reader {
bytes,
fixed: Vec::new(),
};
header.bytes(0, 32)?;
let profile = header.word(0)?;
if !matches!(profile, 7003 | 7004) {
return Err(error(
0,
"unsupported profile (expected vertex 7003 or fragment 7004)",
));
}
let revision = header.word(4)?;
if revision != 6 {
return Err(error(4, "unsupported binary revision (expected 6)"));
}
let total = header.offset(8)?;
let mut r = Reader {
bytes: header.bytes(0, total)?,
fixed: Vec::new(),
};
r.bytes(0, 32)?;
r.fixed.push(0..32);
let count = r.offset(12)?;
let params = r.offset(16)?;
let stage_at = r.offset(20)?;
let ucode_size = r.offset(24)?;
let ucode_at = r.offset(28)?;
let param_range = r.range(params, count, 48)?;
let params_at = param_range.clone().step_by(48);
r.reserve(param_range)?;
let stage_range = r.range(stage_at, 1, 24)?;
r.reserve(stage_range)?;
let ucode_range = r.range(ucode_at, ucode_size, 1)?;
r.reserve(ucode_range)?;
if !ucode_at.is_multiple_of(16) || !ucode_size.is_multiple_of(16) {
return Err(error(28, "microcode offset or size is not 16-byte aligned"));
}
let stage_bytes = r.bytes(stage_at, 24)?;
let s = Reader {
bytes: stage_bytes,
fixed: Vec::new(),
};
let instruction_count = s.word(0)?;
if usize::try_from(instruction_count)
.ok()
.and_then(|slots| slots.checked_mul(16))
!= Some(ucode_size)
{
return Err(error(
stage_at,
"instruction slot count disagrees with microcode size",
));
}
let stage = if profile == 7003 {
Stage::Vertex(Vertex {
instruction_count,
instruction_slot: s.word(4)?,
register_count: s.word(8)?,
attribute_input_mask: s.word(12)?,
attribute_output_mask: s.word(16)?,
user_clip_mask: s.word(20)?,
})
} else {
let stage_view = View::new(stage_bytes);
let half = |at| {
stage_view
.u16(at, Big)
.map_err(|e| error(stage_at.saturating_add(at), e.to_string()))
};
let flag = |at| -> Result<bool, Error> { r.boolean(plus(stage_at, at)?, true) };
let byte = |at| {
stage_view
.u8(at)
.map_err(|e| error(stage_at.saturating_add(at), e.to_string()))
};
Stage::Fragment(Fragment {
instruction_count,
attribute_input_mask: s.word(4)?,
partial_tex_type: s.word(8)?,
tex_coords_input_mask: half(12)?,
tex_coords_2d: half(14)?,
tex_coords_centroid: half(16)?,
register_count: byte(18)?,
output_from_h0: flag(19)?,
depth_replace: flag(20)?,
pixel_kill: flag(21)?,
padding: [byte(22)?, byte(23)?],
})
};
let mut parameters = Vec::new();
for at in params_at {
let field = |delta| plus(at, delta);
let ty = r.word(at)?;
parameters.push(Parameter {
ty,
resource: r.word(field(4)?)?,
variability: r.word(field(8)?)?,
resource_index: r.word(field(12)?)? as i32,
name: r.string(field(16)?)?,
default_value: r.floats(field(20)?, ty)?,
embedded_constants: r.embedded(field(24)?, ucode_size)?,
semantic: r.string(field(28)?)?,
direction: r.word(field(32)?)?,
parameter_number: r.word(field(36)?)? as i32,
referenced: r.boolean(field(40)?, false)?,
shared: r.boolean(field(44)?, false)?,
});
}
Ok(Program {
profile,
revision,
stage,
parameters,
microcode: r.bytes(ucode_at, ucode_size)?,
bytes: r.bytes,
})
}