Skip to main content

dxbc_reader/
lib.rs

1//! A reader of Shader Model 4 and 5 programs as Direct3D stores them in a `DXBC` container: the token stream of any
2//! stage ([`mod@decode`]), the reflection and signature chunks as the records the file holds ([`reflection`]), and the
3//! compiler's own counters ([`mod@statistics`]).
4//!
5//! It reads and checks bytes and never interprets them for an API: a register binding is the RDEF record's own
6//! numbers, a signature element its stored name, index and masks. Mapping those into a graphics API's vocabulary is
7//! the caller's choice. Everything it can't read (a truncated instruction, an out-of-range table, a cyclic type) is an
8//! [`Error`] naming the 32-bit word where it happened; it never panics on untrusted input.
9//!
10//! ```no_run
11//! # fn main() -> Result<(), dxbc_reader::Error> {
12//! let container = std::fs::read("shader.dxbc").expect("a compiled shader");
13//! let code = dxbc_reader::code(&container)?;
14//! let program = dxbc_reader::decode(&code)?;
15//! println!("{} with {} instructions", program.target(), program.instructions.len());
16//! # Ok(()) }
17//! ```
18#![deny(
19    clippy::arithmetic_side_effects,
20    clippy::indexing_slicing,
21    clippy::cast_possible_truncation
22)]
23#![deny(missing_docs)]
24#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
25
26mod controls;
27pub mod decode;
28mod disasm;
29mod format;
30pub mod interfaces;
31pub mod level9;
32mod opcodes;
33pub mod reflection;
34mod sampling;
35mod spdb;
36pub mod statistics;
37
38pub use controls::{GlobalFlags, InfoReturn, SyncFlags};
39pub use decode::{
40    Declaration, Declared, Decoded, Index, Instruction, Operand, ProgramType, Register, RegisterRange,
41    Selection, decode, opcode_name,
42};
43pub use disasm::{Options, disassemble, disassemble_with};
44pub use format::{OperandClass, OperandFormat, operand_format};
45pub use interfaces::{ClassInstance, ClassType, InterfaceSlots, Interfaces, interfaces};
46pub use level9::{ConstantMapping, Level9, RuntimeConstant, SamplerMapping, ScalarMapping, level9};
47pub use reflection::{
48    Rdef, RdefBinding, RdefBuffer, RdefMember, RdefType, RdefVariable, SignatureElement,
49    patch_signature_elements, program_type, rdef, rdef_target, signature, signature_elements,
50};
51pub use sampling::{TextureRead, texture_reads};
52pub use statistics::{Statistics, statistics};
53
54/// Something the reader can't read: the 32-bit word of the container's chunk or token stream where it is, and what.
55#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
56#[error("DXBC at word {word}: {detail}")]
57pub struct Error {
58    /// The word offset, counted from the start of the chunk or token stream being read.
59    pub word: usize,
60    /// What was expected there, or what was found instead.
61    pub detail: String,
62}
63
64/// The reader's result: an [`Error`] naming the word where reading failed.
65pub type Result<T> = std::result::Result<T, Error>;
66
67pub(crate) fn error(word: usize, detail: impl Into<String>) -> Error {
68    Error {
69        word,
70        detail: detail.into(),
71    }
72}
73
74/// The container's parts (its `DXBC` directory).
75pub fn parts(bytes: &[u8]) -> Result<Vec<dxcontainer::Chunk<'_>>> {
76    dxcontainer::chunks(bytes).map_err(|e| error(e.offset / 4, e.detail))
77}
78
79/// The container's one part of kind `tag`: an error where it has none, or more than one.
80pub fn chunk<'a>(directory: &[dxcontainer::Chunk<'a>], tag: &[u8; 4]) -> Result<&'a [u8]> {
81    let shown = String::from_utf8_lossy(tag);
82    let mut matching = directory.iter().filter(|c| &c.kind == tag);
83    let data = matching
84        .next()
85        .ok_or_else(|| error(0, format!("the container has no {shown} part")))?
86        .data;
87    if matching.next().is_some() {
88        return Err(error(
89            0,
90            format!("the container has the {shown} part more than once"),
91        ));
92    }
93    Ok(data)
94}
95
96/// The container's code part (`SHEX` or `SHDR`). Refuses a `DXIL` container, whose program is a `DXIL` part of LLVM
97/// bitcode, and a container with neither, naming the parts.
98pub(crate) fn code_part<'a>(directory: &'a [dxcontainer::Chunk<'a>]) -> Result<&'a dxcontainer::Chunk<'a>> {
99    if directory.iter().any(|c| &c.kind == b"DXIL") {
100        return Err(error(
101            0,
102            "a DXIL container (Shader Model 6): its program is the DXIL part's LLVM bitcode, not a Shader Model 4 or 5 \
103             token stream",
104        ));
105    }
106    directory
107        .iter()
108        .find(|c| matches!(&c.kind, b"SHDR" | b"SHEX"))
109        .ok_or_else(|| error(0, "the container has no code part (SHEX, SHDR or DXIL)"))
110}
111
112/// The program's token stream (its `SHEX` or `SHDR` part) as 32-bit little-endian words, for [`decode()`]. Refuses
113/// what [`parts`] refuses and a container without that part (a `DXIL` one among them).
114pub fn code(bytes: &[u8]) -> Result<Vec<u32>> {
115    let directory = parts(bytes)?;
116    let code = code_part(&directory)?;
117    if code.data.len() % 4 != 0 {
118        return Err(error(
119            code.data.len() / 4,
120            format!(
121                "the code part is {} bytes, expected a multiple of 4 (32-bit tokens)",
122                code.data.len()
123            ),
124        ));
125    }
126    Ok(code
127        .data
128        .as_chunks::<4>()
129        .0
130        .iter()
131        .map(|b| u32::from_le_bytes(*b))
132        .collect())
133}