Skip to main content

dxil_reader/
lib.rs

1//! A reader of Shader Model 6 programs (DXIL) as Direct3D stores them in a `DXBC` container: the program part's header
2//! ([`program`]), its LLVM bitcode through a bounded bitstream reader ([`bitstream`]) into the module it holds
3//! ([`mod@module`]), the entry point's metadata, resource records and signature parts ([`reflection`]), the validator's
4//! pipeline state record ([`psv`]), and a library's runtime data ([`rdat`]), each as the records the file stores with
5//! DXIL's and D3D's own numbers.
6//!
7//! It reads and checks bytes and never interprets them for an API: a resource is its record's class, range and shape
8//! numbers, a signature element its stored name, index, register and masks. Mapping those into a graphics API's
9//! vocabulary is the caller's choice. Everything it can't read (a truncated record, an abbreviation it does not know, a
10//! reference outside its table) is an [`Error`] naming the bit where it happened; it never panics on untrusted input.
11//!
12//! ```no_run
13//! # fn main() -> Result<(), dxil_reader::Error> {
14//! let container = std::fs::read("shader.dxil").expect("a compiled shader");
15//! let parts = dxil_reader::parts(&container)?;
16//! let module = dxil_reader::module(&parts, b"DXIL")?.expect("a DXIL part");
17//! println!("{} functions", module.functions.len());
18//! # Ok(()) }
19//! ```
20#![deny(
21    clippy::arithmetic_side_effects,
22    clippy::indexing_slicing,
23    clippy::cast_possible_truncation
24)]
25#![deny(missing_docs)]
26#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
27pub mod bitstream;
28pub mod module;
29pub mod psv;
30pub mod rdat;
31pub mod reflection;
32
33pub use module::Module;
34
35/// Something the reader can't read: the bit where it is (in the bitcode, or eight times a byte of the container or
36/// part being read), and what.
37#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
38#[error("DXIL at bit {bit}: {detail}")]
39pub struct Error {
40    /// The bit offset, counted from the start of the bitcode, part or container being read.
41    pub bit: usize,
42    /// What was expected there, or what was found instead.
43    pub detail: String,
44}
45
46/// The reader's result: an [`Error`] naming the bit where reading failed.
47pub type Result<T> = std::result::Result<T, Error>;
48
49pub(crate) fn error(bit: usize, detail: impl Into<String>) -> Error {
50    Error {
51        bit,
52        detail: detail.into(),
53    }
54}
55
56/// The `DXIL` part of a DXBC container: the program header and its LLVM bitcode.
57#[derive(Clone, Copy, Debug)]
58pub struct Part<'a> {
59    /// Shader kind in the high 16 bits (0 pixel), model major and minor in the low byte (`0x60` is 6.0).
60    pub program_version: u32,
61    /// DXIL version (`0x100` is 1.0).
62    pub dxil_version: u32,
63    /// The program's LLVM bitcode (starting at its `BC` magic), for [`bitstream::read`].
64    pub bitcode: &'a [u8],
65}
66
67/// The parts of a DXBC container (its directory), an error of the container at the bit of its byte.
68pub fn parts(container: &[u8]) -> Result<Vec<dxcontainer::Chunk<'_>>> {
69    dxcontainer::chunks(container).map_err(|e| error(e.offset.saturating_mul(8), e.detail))
70}
71
72/// A program part's header (`DXIL` and the `STAT` reflection part share it) and its bitcode.
73pub fn program(data: &[u8]) -> Result<Part<'_>> {
74    let view = primase_foundations::bytes::View::new(data);
75    let word = |at: usize| -> Result<u32> {
76        view.array::<4>(at)
77            .map(u32::from_le_bytes)
78            .map_err(|e| error(at.saturating_mul(8), e.to_string()))
79    };
80    let program_version = word(0)?;
81    let dwords = word(4)? as usize;
82    if dwords.checked_mul(4) != Some(data.len()) {
83        return Err(error(32, "program size disagrees with the part"));
84    }
85    if view.slice(8, 4).map_err(|e| error(64, e.to_string()))? != b"DXIL" {
86        return Err(error(64, "program header magic"));
87    }
88    let dxil_version = word(12)?;
89    // The bitcode offset counts from the magic.
90    let offset = word(16)? as usize;
91    let size = word(20)? as usize;
92    let bitcode = offset
93        .checked_add(8)
94        .and_then(|start| view.slice(start, size).ok())
95        .ok_or_else(|| error(128, "bitcode outside the part"))?;
96    Ok(Part {
97        program_version,
98        dxil_version,
99        bitcode,
100    })
101}
102
103/// Find and check a container's `DXIL` part.
104pub fn part(container: &[u8]) -> Result<Part<'_>> {
105    let chunks = parts(container)?;
106    let data = chunks
107        .iter()
108        .find(|c| &c.kind == b"DXIL")
109        .ok_or_else(|| error(0, "no DXIL part"))?
110        .data;
111    program(data)
112}
113
114/// The module of a container's program part of this kind (`DXIL`, or `STAT` for the reflection module), if it has one.
115pub fn module(chunks: &[dxcontainer::Chunk<'_>], kind: &[u8; 4]) -> Result<Option<Module>> {
116    let Some(c) = chunks.iter().find(|c| &c.kind == kind) else {
117        return Ok(None);
118    };
119    read_module(&program(c.data)?).map(Some)
120}
121
122fn read_module(part: &Part<'_>) -> Result<Module> {
123    let blocks = bitstream::read(part.bitcode, *b"BC\xc0\xde")?;
124    Module::read(&blocks)
125}
126
127/// The shader kind (0 pixel, 1 vertex, 2 geometry, 3 hull, 4 domain, 5 compute) of a container and the `dx.op`
128/// operations called across all the module's function bodies (a hull shader's patch-constant function is a second
129/// one), by name: for surveying which operations a set of shaders uses.
130pub fn operations(container: &[u8]) -> Result<(u32, std::collections::BTreeMap<String, usize>)> {
131    use module::Op;
132    let chunks = parts(container)?;
133    let part = program(
134        chunks
135            .iter()
136            .find(|c| &c.kind == b"DXIL")
137            .ok_or_else(|| error(0, "no DXIL part"))?
138            .data,
139    )?;
140    let module = read_module(&part)?;
141    let mut out = std::collections::BTreeMap::new();
142    for f in &module.functions {
143        for inst in &f.instructions {
144            let Op::Call { function, args, .. } = &inst.op else {
145                continue;
146            };
147            let Some(name) = module.name(*function).filter(|n| n.starts_with("dx.op.")) else {
148                continue;
149            };
150            let code = args.first().and_then(|&a| {
151                let value = match a.checked_sub(f.first) {
152                    None => module.values.get(a),
153                    Some(local) => f.locals.get(local),
154                };
155                match value {
156                    Some(module::Value::Constant(module::Constant::Integer(i))) => Some(*i),
157                    _ => None,
158                }
159            });
160            let key = match code {
161                Some(code) => format!("{name} (op {code})"),
162                None => name.to_owned(),
163            };
164            out.entry(key)
165                .and_modify(|n: &mut usize| *n = n.saturating_add(1))
166                .or_insert(1);
167        }
168    }
169    Ok((part.program_version >> 16, out))
170}