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