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}