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