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