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