Skip to main content

iridia_exec/
lib.rs

1//! A CPU reference executor for [`iridia::Program`]s: run a lifted shader on explicit inputs, whatever bytecode it came
2//! from, and read back exactly what it wrote. It is for checking a translation, not for rendering: it is bounded,
3//! deterministic and slow.
4//!
5//! [`Executable::new`] checks a program and compiles it once into a flat form (typed operations on four-lane word
6//! registers, pointers folded to word offsets, dead operations dropped). [`Executable::bind`] flattens the buffers a
7//! program reads once for many runs, and a [`Runner`] executes the program for one invocation, a 2 x 2 quad, or several
8//! invocations in lockstep (masked where they diverge, each with its own steps and budget), asking its [`Backend`] for
9//! everything outside the program: texture samples, loads and queries, and the effects of writes to unordered-access
10//! views. [`ProgramShader`] puts a vertex or pixel program behind [`PixelShader`], in the register vocabulary of the D3D
11//! bytecode it came from. The other stages have their own drivers: [`mesh`], [`geometry`], [`tessellation`] and
12//! [`tessellator`] (a model of the fixed-function tessellator), [`Pipeline`] (a ray tracing pipeline over a [`Scene`]),
13//! [`draw`] (a small software rasterizer over vertex and pixel programs) and [`probe`].
14//!
15//! What a run does follows the program's [`iridia::D3dModes`]: a program that says its body spells out D3D's integer
16//! division, conversions and `min`/`max` runs as written, and one that expects denormals flushed to zero has every
17//! 32-bit float operation flush its operands and result (sign kept). Anything the program does that the module cannot
18//! make valid (an index past the end of a local array, an integer division by zero in the module) is an error of the
19//! run, never a silent value. Every run has a step budget ([`STEP_LIMIT`]).
20//!
21//! Sampling belongs to the caller: texture samples are answered by a [`Sampler`] callback (a closure answers 2D
22//! samples), because a scalar execution cannot infer derivatives, filtering or addressing, and the caller states them.
23//! [`evaluate()`] runs a pixel shader over a grid of UVs and [`evaluate_periodic`] over a tiling block.
24//!
25//! # Example
26//!
27//! Lift a pixel shader and run it once. Its constant buffer holds `f = (0.25, 2, 4, 8)` and `u = (7, 2, 0, 0)`; the
28//! HLSL computes `min(f.x, f.y)`, `max(f.x, f.y)` into target 0 and `u.x / u.y`, `u.x % u.y` into target 1:
29//!
30//! ```
31//! use iridia_exec::{PixelShader, ProgramShader, Sample};
32//!
33//! let program = iridia_dxbc::lift(include_bytes!("../../iridia-dxbc/tests/fixtures/modes.dxbc"))?;
34//! let shader = ProgramShader::new(program)?;
35//!
36//! let bits = |v: [f32; 4]| v.map(f32::to_bits);
37//! let constants = [bits([0.25, 2.0, 4.0, 8.0]), [7, 2, 0, 0], [0; 4]];
38//! let inputs = vec![[0u32; 4]; shader.input_count()]; // SV_Position, which the program never reads
39//! let run = shader.execute(&inputs, &[(0, &constants)], &mut |_: Sample| Err("no textures".into()))?;
40//!
41//! assert_eq!(f32::from_bits(run.outputs[0][0]), 0.25); // min(f.x, f.y)
42//! assert_eq!(f32::from_bits(run.outputs[0][1]), 2.0); // max(f.x, f.y)
43//! assert_eq!(&run.outputs[1][..2], &[3, 1]); // 7 / 2, 7 % 2
44//! # Ok::<(), Box<dyn std::error::Error>>(())
45//! ```
46
47#![deny(missing_docs)]
48#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
49#![cfg_attr(
50    not(test),
51    deny(
52        clippy::indexing_slicing,
53        clippy::arithmetic_side_effects,
54        clippy::cast_possible_truncation
55    )
56)]
57
58// Run the examples in the README as doc tests.
59#[cfg(doctest)]
60#[doc = include_str!("../README.md")]
61struct ReadmeExamples;
62
63pub mod draw;
64pub mod evaluate;
65mod periodic;
66pub use evaluate::{
67    Evaluation, Field, Grid, Input, Invocation, Lane, Layout, Raw, evaluate, evaluate_raw, evaluate_raw_with,
68    evaluate_with,
69};
70pub use periodic::{evaluate_periodic, evaluate_periodic_with};
71pub mod texture;
72pub mod views;
73pub use texture::{Filtering, SampledTexture, SamplerUse, Unstated};
74pub use views::{Absent, Image, View, Views};
75
76use iridia::Interface;
77
78mod compile;
79mod flat;
80pub mod geometry;
81mod mask;
82pub mod mesh;
83mod pipeline;
84mod pixel;
85pub mod probe;
86mod rayquery;
87mod request;
88mod run;
89mod stage;
90pub mod tessellation;
91pub mod tessellator;
92
93pub use flat::Executable;
94pub use mask::{Mask, Wide};
95pub use pipeline::{DispatchStats, Pipeline, PipelineResources, ShaderTable};
96pub use pixel::ProgramShader;
97pub use rayquery::{Built, Geometry, Instance, InstanceFlags, Scene, flags as ray_flags};
98pub use request::{
99    Backend, CallShader, Dim, Element, ImageAtomic, ImageAtomicOp, ImageLoad, ImageLod, ImageQuery,
100    ImageSample, ImageStore, QueryKind, ReportHit, TraceRay,
101};
102pub use run::{Bound, Decision, Dispatched, Output, Resources, Runner, STEP_LIMIT, Scratch};
103pub use stage::Constants;
104
105use iridia::Program;
106
107/// `v` rounded to the nearest f32. Reference arithmetic evaluates in f64 and rounds once.
108#[allow(clippy::cast_possible_truncation)] // the rounding is the operation
109pub(crate) fn narrow(v: f64) -> f32 {
110    v as f32
111}
112
113/// The mask of the lowest `lanes` bits of a write (at most the 4 a register has).
114pub(crate) fn lane_mask(lanes: u8) -> u8 {
115    0xffu8
116        .checked_shr(8u32.saturating_sub(u32::from(lanes.min(8))))
117        .unwrap_or(0)
118}
119
120/// An error of checking or compiling a program, in the program's own terms.
121pub(crate) fn error(detail: impl Into<String>) -> Error {
122    Error {
123        bytecode: "program",
124        at: 0,
125        detail: detail.into(),
126    }
127}
128
129impl Executable {
130    /// Check `program` (its module and its interface) and compile it. A construct the executor does not run is
131    /// refused here, by name.
132    pub fn new(program: &Program) -> Result<Self, Error> {
133        if iridia::stage_functions(&program.interface)
134            .map_err(error)?
135            .is_some()
136        {
137            return Err(error(if program.stage.is_ray_tracing() {
138                format!(
139                    "a {:?} program does the ray tracing pipeline's work through operations only the pipeline has \
140                     ({}): run it with Executable::function inside a pipeline (Pipeline)",
141                    program.stage,
142                    iridia::rt::OPERATIONS.join(", ")
143                )
144            } else {
145                format!(
146                    "a {:?} program is functions, not an entry point: run one with Executable::function",
147                    program.stage
148                )
149            }));
150        }
151        let info = program.validate().map_err(|problems| {
152            error(
153                problems
154                    .iter()
155                    .map(ToString::to_string)
156                    .collect::<Vec<_>>()
157                    .join("; "),
158            )
159        })?;
160        compile::compile(program, &info, None).map_err(error)
161    }
162
163    /// Compile one function of a program made of functions (a hull program's `main` and `patch_constant`, a domain
164    /// program's or a position-less vertex program's `main`; see [`iridia::stage_functions`]) to run for one
165    /// invocation. The registers it takes are its arguments in order, an array argument (the control points) as one
166    /// register per element; the members of its result are the members of [`iridia::FunctionSig::result`].
167    pub fn function(program: &Program, name: &str) -> Result<Self, Error> {
168        let info = program.validate().map_err(|problems| {
169            error(
170                problems
171                    .iter()
172                    .map(ToString::to_string)
173                    .collect::<Vec<_>>()
174                    .join("; "),
175            )
176        })?;
177        compile::compile(program, &info, Some(name)).map_err(error)
178    }
179
180    /// Every function of a program made of functions ([`iridia::stage_functions`]: a hull, domain, geometry or ray tracing
181    /// program, a vertex program that feeds a tessellator), each compiled by [`Executable::function`] under its name;
182    /// `None` for a program that is an entry point, which [`Executable::new`] takes.
183    pub fn functions(program: &Program) -> Result<Option<Vec<(&'static str, Self)>>, Error> {
184        let Some(signatures) = iridia::stage_functions(&program.interface).map_err(error)? else {
185            return Ok(None);
186        };
187        signatures
188            .iter()
189            .map(|sig| Self::function(program, sig.name).map(|executable| (sig.name, executable)))
190            .collect::<Result<Vec<_>, _>>()
191            .map(Some)
192    }
193
194    /// The words of pointer argument `k` (a ray tracing program's payload, attributes or parameter).
195    pub(crate) fn pointer_words(&self, k: usize) -> Option<u32> {
196        self.pointer_args.get(k).map(|a| a.2)
197    }
198
199    /// The textures and the samplers the program's compiled code samples, loads from or asks the size of (an operation
200    /// nothing reads is not in it): the resources a caller must bind for a run that may reach any path.
201    pub fn sampled(
202        &self,
203    ) -> (
204        std::collections::BTreeSet<iridia::ResourceId>,
205        std::collections::BTreeSet<iridia::ResourceId>,
206    ) {
207        let (mut textures, mut samplers) = (
208            std::collections::BTreeSet::new(),
209            std::collections::BTreeSet::new(),
210        );
211        for (_, op) in &self.ops {
212            match op {
213                flat::Op::Sample { tex, smp, .. } => {
214                    textures.insert(tex.id);
215                    samplers.insert(smp.id);
216                }
217                flat::Op::Texel {
218                    tex, storage: false, ..
219                }
220                | flat::Op::Query { tex, .. } => {
221                    textures.insert(tex.id);
222                }
223                _ => {}
224            }
225        }
226        (textures, samplers)
227    }
228
229    /// Whether the program needs the other lanes of its 2 x 2 quad: it takes a screen derivative, or samples at an
230    /// implicit level of detail (`sample`, `sample_b`), whose footprint is the quad's. A program that does not runs
231    /// the same one invocation at a time as in a quad.
232    pub fn needs_quad(&self) -> bool {
233        self.ops.iter().any(|(_, op)| {
234            matches!(
235                op,
236                flat::Op::Deriv { .. }
237                    | flat::Op::Sample {
238                        level: flat::LevelOp::Auto | flat::LevelOp::Bias(_),
239                        ..
240                    }
241            )
242        })
243    }
244}
245
246/// How a sample request picks its level of detail.
247#[derive(Clone, Copy, Debug, PartialEq)]
248pub enum Lod {
249    /// The implicit level (`sample`), whose footprint a scalar evaluation leaves to the sampler.
250    Implicit,
251    /// The implicit level plus a bias the program states (`sample_b`, as given: the sampler clamps it to D3D's
252    /// -16..15.99).
253    Bias(f32),
254    /// A level the program states (`sample_l`).
255    Level(f32),
256    /// The UV's screen derivatives: those the program states (`sample_d`), or a quad's own for an implicit-LOD
257    /// sample (d/dx, then d/dy).
258    Gradients([[f32; 2]; 2]),
259    /// A quad's own UV derivatives plus the bias the program states (`sample_b` in a quad).
260    GradientsBias([[f32; 2]; 2], f32),
261}
262
263impl From<ImageLod> for Lod {
264    /// A 2D sample's level of detail as the image request states it (its gradients' u and v).
265    fn from(lod: ImageLod) -> Self {
266        match lod {
267            ImageLod::Implicit => Lod::Implicit,
268            ImageLod::Bias(bias) => Lod::Bias(bias),
269            ImageLod::Level(level) => Lod::Level(level),
270            ImageLod::Gradients(g) => Lod::Gradients([[g[0][0], g[0][1]], [g[1][0], g[1][1]]]),
271            ImageLod::GradientsBias(g, bias) => {
272                Lod::GradientsBias([[g[0][0], g[0][1]], [g[1][0], g[1][1]]], bias)
273            }
274        }
275    }
276}
277
278impl From<Lod> for ImageLod {
279    /// A 2D sample's level of detail as an image request states it (its gradients' third lane 0).
280    fn from(lod: Lod) -> Self {
281        let lift = |g: [[f32; 2]; 2]| [[g[0][0], g[0][1], 0.], [g[1][0], g[1][1], 0.]];
282        match lod {
283            Lod::Implicit => ImageLod::Implicit,
284            Lod::Bias(bias) => ImageLod::Bias(bias),
285            Lod::Level(level) => ImageLod::Level(level),
286            Lod::Gradients(g) => ImageLod::Gradients(lift(g)),
287            Lod::GradientsBias(g, bias) => ImageLod::GradientsBias(lift(g), bias),
288        }
289    }
290}
291
292/// A sample request of a 2D texture (or a 2D array's layer).
293#[derive(Clone, Copy, Debug)]
294pub struct Sample {
295    /// Texture bind slot (`t<n>`).
296    pub texture: u32,
297    /// Sampler bind slot (`s<n>`).
298    pub sampler: u32,
299    /// The texture coordinate (u, v).
300    pub uv: [f32; 2],
301    /// A 2D array's layer coordinate, as the program gives it (the sampler rounds it); `None` for a 2D texture.
302    pub layer: Option<f32>,
303    /// How the level of detail is picked.
304    pub lod: Lod,
305}
306
307/// A load request (`ld`): one texel by its integer address, without a sampler.
308#[derive(Clone, Copy, Debug)]
309pub struct Load {
310    /// Texture bind slot (`t<n>`).
311    pub texture: u32,
312    /// x, y, and a 2D array's layer or a 3D texture's z (0 for a 2D texture); a buffer's element at x (y and z 0).
313    pub texel: [i32; 3],
314    /// The mip level (0 for a buffer).
315    pub mip: i32,
316}
317
318/// A store request (`store_uav_typed`, `store_structured`).
319#[derive(Clone, Copy, Debug)]
320pub struct Store {
321    /// Unordered-access view bind slot (`u<n>`).
322    pub view: u32,
323    /// A typed view's x, y, and z or a 2D array's layer (0 where the view has fewer); a structured view's element and
324    /// byte offset (then 0).
325    pub address: [u32; 3],
326    /// The lanes as the program writes them (bits), only those `mask` marks.
327    pub value: [u32; 4],
328    /// The lanes written, bit 0 for x to bit 3 for w.
329    pub mask: u8,
330}
331
332/// An atomic add (`imm_atomic_iadd`) to the 32-bit word of an unordered-access view at `address`: a structured view's
333/// element and byte offset in its first two lanes, a typed view's x, y (and z) otherwise (0 where the view has fewer).
334#[derive(Clone, Copy, Debug)]
335pub struct AtomicAdd {
336    /// Unordered-access view bind slot (`u<n>`).
337    pub view: u32,
338    /// Where the add lands: a structured view's element and byte offset, or a typed view's x, y and z.
339    pub address: [u32; 3],
340    /// The value added to the 32-bit word.
341    pub value: u32,
342}
343
344/// Answers sample, load, store and structured-buffer requests; an error stops the evaluation. A closure answers samples
345/// only.
346pub trait Sampler {
347    /// Answer a 2D [`Sample`] with the texel's four lanes as floats, or an error that stops the evaluation.
348    fn sample(&mut self, request: Sample) -> std::result::Result<[f32; 4], String>;
349    /// The texel's four lanes as its view returns them: f32 bits for a float view, the integers for a uint or sint
350    /// one.
351    fn load(&mut self, request: Load) -> std::result::Result<[u32; 4], String> {
352        Err(format!("t{}: no loads given", request.texture))
353    }
354    /// A store to an unordered-access view ([`Store`]): the lanes `mask` marks of `value`, at the address the program
355    /// gives (a structured view's lanes the consecutive words from the byte offset). Its effect is the caller's: it
356    /// reaches none of this evaluation's outputs.
357    fn store(&mut self, request: Store) -> std::result::Result<(), String> {
358        Err(format!("u{}: no stores given", request.view))
359    }
360    /// An atomic add ([`AtomicAdd`]); its result the word as it was before. The add itself is the caller's effect.
361    fn atomic_add(&mut self, request: AtomicAdd) -> std::result::Result<u32, String> {
362        Err(format!("u{}: no atomics given", request.view))
363    }
364    /// The 32-bit word at byte `offset` of element `element` of the structured buffer at `t<slot>`.
365    fn structured(&mut self, slot: u32, element: u32, offset: u32) -> std::result::Result<u32, String> {
366        let _ = (element, offset);
367        Err(format!("t{slot}: no structured buffer given"))
368    }
369
370    /// How many elements the buffer view at bind slot `slot` has, where the sampler knows. A program that reads past the
371    /// end gets zeros (D3D's rule) when this is stated; when it is not, no read is taken for out of range.
372    fn buffer_elements(&mut self, slot: u32) -> Option<u32> {
373        let _ = slot;
374        None
375    }
376
377    /// A sample of any texture: 1D, 3D or cube, with a texel offset, a gather or a comparison. A plain 2D sample
378    /// (or a 2D array's layer) goes to [`Sampler::sample`]; anything else is refused unless the sampler answers it.
379    fn sample_image(&mut self, request: ImageSample<u32>) -> std::result::Result<[f32; 4], String> {
380        if request.dim != Dim::D2
381            || request.gather.is_some()
382            || request.depth_ref.is_some()
383            || request.depth
384            || request.offset != [0; 3]
385        {
386            return Err(format!(
387                "t{}: this sampler answers plain 2D samples only",
388                request.texture
389            ));
390        }
391        self.sample(Sample {
392            texture: request.texture,
393            sampler: request.sampler,
394            uv: [request.coord[0], request.coord[1]],
395            layer: request.layer,
396            lod: Lod::from(request.lod),
397        })
398    }
399    /// A texel load of any texture. A 2D or 3D one (or a 2D array's layer) goes to [`Sampler::load`].
400    fn load_image(&mut self, request: ImageLoad<u32>) -> std::result::Result<[u32; 4], String> {
401        if !matches!(request.dim, Dim::D2 | Dim::D3) || request.storage || request.sample.is_some() {
402            return Err(format!(
403                "t{}: this sampler answers 2D, array and 3D loads of shader resources only",
404                request.texture
405            ));
406        }
407        self.load(Load {
408            texture: request.texture,
409            texel: [
410                request.texel[0],
411                request.texel[1],
412                request.layer.unwrap_or(request.texel[2]),
413            ],
414            mip: request.mip.unwrap_or(0),
415        })
416    }
417    /// A texture's size, level count, layer count or sample count.
418    fn query_image(&mut self, request: ImageQuery<u32>) -> std::result::Result<[u32; 4], String> {
419        Err(format!("t{}: no queries given", request.texture))
420    }
421    /// An atomic operation on a texel of a single-channel 32-bit unordered-access texture; its result is the word as it
422    /// was before.
423    fn atomic_image(&mut self, request: ImageAtomic<u32>) -> std::result::Result<u32, String> {
424        Err(format!("u{}: no image atomics given", request.view))
425    }
426    /// A write to an unordered-access texture; the plain form is [`Sampler::store`].
427    fn store_image(&mut self, request: ImageStore<u32>) -> std::result::Result<(), String> {
428        self.store(Store {
429            view: request.view,
430            address: [
431                request.texel[0],
432                request.texel[1],
433                request.layer.unwrap_or(request.texel[2]),
434            ],
435            value: request.value,
436            mask: lane_mask(request.lanes),
437        })
438    }
439}
440impl<F: FnMut(Sample) -> std::result::Result<[f32; 4], String>> Sampler for F {
441    fn sample(&mut self, request: Sample) -> std::result::Result<[f32; 4], String> {
442        self(request)
443    }
444}
445
446/// What one evaluation wrote.
447#[derive(Clone, Debug, Default)]
448pub struct Execution {
449    /// One entry per output register.
450    pub outputs: Vec<[u32; 4]>,
451    /// Only lanes marked in this mask were written by the shader.
452    pub output_masks: Vec<u8>,
453    /// Instructions this evaluation executed.
454    pub instructions: usize,
455    /// Samples this evaluation requested.
456    pub samples: usize,
457    /// Whether the program discarded the pixel (`discard`): its outputs are then those written before.
458    pub discarded: bool,
459}
460
461/// An evaluation or binding error, located in the implementation's own terms (a DXBC word, a DXIL value).
462#[derive(Debug, thiserror::Error)]
463#[error("{bytecode} at {at}: {detail}")]
464pub struct Error {
465    /// What was being checked or run: `program`, `draw`, `evaluate` or `evaluate_periodic`.
466    pub bytecode: &'static str,
467    /// A position in the bytecode's own unit where the error has one (a grid row for `evaluate`); 0 otherwise.
468    pub at: usize,
469    /// What was refused or went wrong, in words.
470    pub detail: String,
471}
472
473/// A checked pixel shader that can be evaluated.
474pub trait PixelShader: std::fmt::Debug + Sync {
475    /// The program's varyings and resources, in the [`iridia::Interface`] vocabulary.
476    fn interface(&self) -> &Interface;
477    /// Input registers an evaluation needs.
478    fn input_count(&self) -> usize;
479    /// Constant buffers the code reads: bind slot and its size in rows of four lanes.
480    fn constant_buffers(&self) -> &[(u32, u32)];
481    /// Texture bind slots the code samples.
482    fn textures(&self) -> &[u32];
483    /// Sampler bind slots the code uses.
484    fn samplers(&self) -> &[u32];
485    /// Evaluate once. Missing inputs or buffers are errors; no partial output is returned (the sampler may already
486    /// have been called).
487    fn execute(
488        &self,
489        inputs: &[[u32; 4]],
490        buffers: &[(u32, &[[u32; 4]])],
491        sampler: &mut dyn Sampler,
492    ) -> std::result::Result<Execution, Error>;
493    /// Evaluate once with the inputs' screen derivatives: per input register, its d/dx then d/dy lanes (f32 bits),
494    /// for programs that read derivatives. A bytecode that reads none evaluates as [`PixelShader::execute`].
495    fn execute_with_derivatives(
496        &self,
497        inputs: &[[u32; 4]],
498        derivatives: &[[[u32; 4]; 2]],
499        buffers: &[(u32, &[[u32; 4]])],
500        sampler: &mut dyn Sampler,
501    ) -> std::result::Result<Execution, Error> {
502        let _ = derivatives;
503        self.execute(inputs, buffers, sampler)
504    }
505    /// Evaluate invocations that share their inputs' screen derivatives (`derivatives`, as
506    /// [`PixelShader::execute_with_derivatives`] takes them), each as that would alone: `each` is given every
507    /// invocation's index and execution (the same outputs, instructions and samples), in order. On an error, the first
508    /// failing invocation's index and its error, `each` given those before it. The sampler may be asked in another
509    /// order than one invocation after another. The default evaluates them one after another.
510    fn execute_lanes(
511        &self,
512        inputs: &[&[[u32; 4]]],
513        derivatives: &[[[u32; 4]; 2]],
514        buffers: &[(u32, &[[u32; 4]])],
515        sampler: &mut dyn Sampler,
516        each: &mut dyn FnMut(usize, &Execution),
517    ) -> std::result::Result<(), (usize, Error)> {
518        for (i, one) in inputs.iter().enumerate() {
519            let e = self
520                .execute_with_derivatives(one, derivatives, buffers, sampler)
521                .map_err(|e| (i, e))?;
522            each(i, &e);
523        }
524        Ok(())
525    }
526    /// Evaluate a 2 x 2 quad together (lanes 0, 1 the top row, 2, 3 the bottom), its derivatives and implicit
527    /// levels of detail the quad's own. A bytecode without quad evaluation refuses.
528    fn execute_quad(
529        &self,
530        inputs: [&[[u32; 4]]; 4],
531        buffers: &[(u32, &[[u32; 4]])],
532        sampler: &mut dyn Sampler,
533    ) -> std::result::Result<[Execution; 4], Error> {
534        let _ = (inputs, buffers, sampler);
535        Err(Error {
536            bytecode: "this bytecode",
537            at: 0,
538            detail: "no quad evaluation".to_owned(),
539        })
540    }
541}