#![deny(missing_docs)]
#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
#![deny(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::cast_possible_truncation
)]
use primase_foundations::bytes::View;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("DXBC container 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(),
}
}
#[derive(Clone, Copy, Debug)]
pub struct Chunk<'a> {
pub kind: [u8; 4],
pub data: &'a [u8],
}
pub fn chunks(bytes: &[u8]) -> Result<Vec<Chunk<'_>>, Error> {
let view = View::new(bytes);
let word = |at: usize| -> Result<usize, Error> {
let word = view.array::<4>(at).map_err(|e| error(at, e.to_string()))?;
usize::try_from(u32::from_le_bytes(word)).map_err(|_| error(at, "a word past the address space"))
};
if bytes.get(..4) != Some(b"DXBC") {
return Err(error(0, "the magic is not DXBC"));
}
if word(20)? != 1 {
return Err(error(20, "the container's version is not 1"));
}
if word(24)? != bytes.len() {
return Err(error(
24,
format!(
"the container states a size of {}, it has {} bytes",
word(24)?,
bytes.len()
),
));
}
let count = word(28)?;
let directory_end = count
.checked_mul(4)
.and_then(|n| n.checked_add(32))
.filter(|end| *end <= bytes.len())
.ok_or_else(|| {
error(
28,
format!("a directory of {count} parts does not fit the container"),
)
})?;
let mut parts: Vec<Chunk<'_>> = Vec::new();
let mut next = directory_end;
for i in 0..count {
let entry = i
.checked_mul(4)
.and_then(|n| n.checked_add(32))
.ok_or_else(|| error(28, "a directory past the address space"))?;
let start = word(entry)?;
if start != next {
let shown = view.array::<4>(start).map_or_else(
|_| "past the end".to_owned(),
|kind| String::from_utf8_lossy(&kind).into_owned(),
);
return Err(error(
entry,
if start < next {
format!(
"overlapping parts: part {i} ({shown}) starts at {start}, before the previous part ends at {next}"
)
} else {
format!(
"{} bytes between parts: part {i} ({shown}) starts at {start}, where the previous part ends at {next}",
start.saturating_sub(next)
)
},
));
}
let kind = view.array::<4>(start).map_err(|e| error(start, e.to_string()))?;
let shown = String::from_utf8_lossy(&kind).into_owned();
let length = word(
start
.checked_add(4)
.ok_or_else(|| error(start, "a part past the address space"))?,
)?;
let data_at = start
.checked_add(8)
.ok_or_else(|| error(start, "a part past the address space"))?;
let data = view
.slice(data_at, length)
.map_err(|e| error(start, format!("part {i} ({shown}): {e}")))?;
if parts.iter().any(|p| p.kind == kind) {
return Err(error(start, format!("a part kind twice: {shown}")));
}
next = data_at
.checked_add(length)
.ok_or_else(|| error(start, "a part past the address space"))?;
parts.push(Chunk { kind, data });
}
if next != bytes.len() {
return Err(error(
next,
format!("{} bytes after the last part", bytes.len().saturating_sub(next)),
));
}
Ok(parts)
}
pub fn checksum(container: &[u8]) -> Option<[u8; 16]> {
let data = container.get(20..)?;
let bits = u32::try_from(data.len()).ok()?.wrapping_mul(8);
let (blocks, rest) = data.as_chunks::<64>();
let mut state = primase_foundations::hash::MD5_INIT;
for block in blocks {
primase_foundations::hash::md5_block(&mut state, block);
}
let mut last = [0u8; 64];
if rest.len() < 56 {
last.get_mut(4..)?.get_mut(..rest.len())?.copy_from_slice(rest);
*last.get_mut(rest.len().checked_add(4)?)? = 0x80;
} else {
let mut first = [0u8; 64];
first.get_mut(..rest.len())?.copy_from_slice(rest);
*first.get_mut(rest.len())? = 0x80;
primase_foundations::hash::md5_block(&mut state, &first);
}
last.get_mut(..4)?.copy_from_slice(&bits.to_le_bytes());
last.get_mut(60..)?
.copy_from_slice(&((bits >> 2) | 1).to_le_bytes());
primase_foundations::hash::md5_block(&mut state, &last);
Some(primase_foundations::hash::md5_bytes(state))
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("cannot write a DXBC container: {detail}")]
pub struct WriteError {
pub detail: String,
}
fn write_error(detail: impl Into<String>) -> WriteError {
WriteError {
detail: detail.into(),
}
}
pub fn serialize(parts: &[Chunk<'_>]) -> Result<Vec<u8>, WriteError> {
let too_big = || write_error("the container is larger than the header's 32-bit size can state");
let mut offset = parts
.len()
.checked_mul(4)
.and_then(|n| n.checked_add(32))
.ok_or_else(too_big)?;
let mut directory = Vec::with_capacity(parts.len());
for (i, part) in parts.iter().enumerate() {
if let Some(first) = parts.iter().take(i).position(|p| p.kind == part.kind) {
return Err(write_error(format!(
"parts {first} and {i} are both {}",
String::from_utf8_lossy(&part.kind)
)));
}
directory.push(u32::try_from(offset).map_err(|_| too_big())?);
offset = offset
.checked_add(8)
.and_then(|n| n.checked_add(part.data.len()))
.ok_or_else(too_big)?;
}
let size = u32::try_from(offset).map_err(|_| too_big())?;
let count = u32::try_from(parts.len()).map_err(|_| too_big())?;
let mut out = Vec::with_capacity(offset);
out.extend_from_slice(b"DXBC");
out.extend_from_slice(&[0; 16]);
for word in [1, size, count] {
out.extend_from_slice(&u32::to_le_bytes(word));
}
for at in directory {
out.extend_from_slice(&at.to_le_bytes());
}
for part in parts {
out.extend_from_slice(&part.kind);
out.extend_from_slice(
&u32::try_from(part.data.len())
.map_err(|_| too_big())?
.to_le_bytes(),
);
out.extend_from_slice(part.data);
}
repair_digest(&mut out)?;
Ok(out)
}
pub fn repair_digest(container: &mut [u8]) -> Result<(), WriteError> {
if container.get(..4) != Some(b"DXBC") || container.len() < 20 {
return Err(write_error(
"the bytes are not a DXBC container (no DXBC magic and 16-byte digest)",
));
}
let digest = checksum(container)
.ok_or_else(|| write_error("the container is too long for the digest's bit count"))?;
container
.get_mut(4..20)
.ok_or_else(|| write_error("the digest field is out of range"))?
.copy_from_slice(&digest);
Ok(())
}
pub fn replace_part(container: &[u8], kind: [u8; 4], data: &[u8]) -> Result<Vec<u8>, ReplaceError> {
let mut parts = chunks(container).map_err(ReplaceError::Read)?;
match parts.iter_mut().find(|p| p.kind == kind) {
Some(part) => part.data = data,
None => parts.push(Chunk { kind, data }),
}
serialize(&parts).map_err(ReplaceError::Write)
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ReplaceError {
#[error(transparent)]
Read(Error),
#[error(transparent)]
Write(WriteError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Bytecode {
Dxbc,
Dxil,
}
impl Bytecode {
pub fn of(parts: &[Chunk<'_>]) -> Option<Self> {
let has = |kinds: &[&[u8; 4]]| parts.iter().any(|p| kinds.contains(&&p.kind));
match (has(&[b"SHDR", b"SHEX"]), has(&[b"DXIL"])) {
(true, false) => Some(Self::Dxbc),
(false, true) => Some(Self::Dxil),
_ => None,
}
}
}
pub fn feature_name(bytecode: Bytecode, bit: u32) -> Option<&'static str> {
let shared = match bit {
0 => Some("Double-precision floating point"),
1 => Some("Raw and Structured buffers"),
2 => Some("UAVs at every shader stage"),
3 => Some("64 UAV slots"),
4 => Some("Minimum-precision data types"),
5 => Some("Double-precision extensions for 11.1"),
6 => Some("Shader extensions for 11.1"),
7 => Some("Comparison filtering for feature level 9"),
8 => Some("Tiled resources"),
9 => Some("PS Output Stencil Ref"),
10 => Some("PS Inner Coverage"),
11 => Some("Typed UAV Load Additional Formats"),
12 => Some("Raster Ordered UAVs"),
13 => Some("SV_RenderTargetArrayIndex or SV_ViewportArrayIndex from any shader feeding rasterizer"),
_ => None,
};
if shared.is_some() || bytecode == Bytecode::Dxbc {
return shared;
}
Some(match bit {
14 => "Wave level operations",
15 => "64-Bit integer",
16 => "View Instancing",
17 => "Barycentrics",
18 => "Use native low precision",
19 => "Shading Rate",
20 => "Raytracing tier 1.1 features",
21 => "Sampler feedback",
22 => "64-bit Atomics on Typed Resources",
23 => "64-bit Atomics on Group Shared",
24 => "Derivatives in mesh and amplification shaders",
25 => "Resource descriptor heap indexing",
26 => "Sampler descriptor heap indexing",
27 => "Wave Matrix",
28 => "64-bit Atomics on Heap Resources",
29 => "Advanced Texture Ops",
30 => "Writeable MSAA Textures",
31 => "SampleCmp with gradient or bias",
32 => "Extended command info",
33 => "Linear Algebra",
40 => "Function uses derivatives",
41 => "Function requires visible group",
_ => return None,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FeatureFlags {
pub bytecode: Bytecode,
pub bits: u64,
}
impl FeatureFlags {
pub fn has(self, bit: u32) -> bool {
self.bits.checked_shr(bit).is_some_and(|rest| rest & 1 == 1)
}
pub fn names(self) -> Vec<&'static str> {
(0..u64::BITS)
.filter(|&b| self.has(b))
.filter_map(|b| feature_name(self.bytecode, b))
.collect()
}
pub fn unnamed(self) -> u64 {
(0..u64::BITS)
.filter(|&b| self.has(b) && feature_name(self.bytecode, b).is_none())
.fold(0, |mask, b| mask | 1u64.checked_shl(b).unwrap_or(0))
}
}
pub fn feature_flags(container: &[u8]) -> Result<Option<FeatureFlags>, Error> {
let parts = chunks(container)?;
let Some(part) = parts.iter().find(|p| &p.kind == b"SFI0") else {
return Ok(None);
};
let at = part
.data
.as_ptr()
.addr()
.saturating_sub(container.as_ptr().addr());
let bits = <[u8; 8]>::try_from(part.data)
.map_err(|_| error(at, format!("the SFI0 part has {} bytes, not 8", part.data.len())))?;
let bytecode = Bytecode::of(&parts).ok_or_else(|| {
error(
at,
"an SFI0 part in a container with no single kind of program part",
)
})?;
Ok(Some(FeatureFlags {
bytecode,
bits: u64::from_le_bytes(bits),
}))
}