iridia-exec 0.5.0

CPU reference executor for lifted Direct3D shaders: run a program on explicit inputs to check a translation
Documentation

iridia-exec

A bounded CPU reference executor for iridia::Program: it runs a lifted shader on explicit inputs, whatever bytecode it was lifted from, and returns exactly what it wrote. It is for checking a translation (against values worked out by hand, or against Microsoft's WARP rasterizer), not for rendering: it is deterministic and slow.

Part of iridia, which turns compiled Direct3D shaders into naga modules. This crate depends on iridia; the readers (iridia-dxbc, iridia-dxil, iridia-d3d9, iridia-mojo) do not depend on it.

What it runs

  • Entry-point programs (pixel, vertex, compute, mesh). Executable::new checks a program and compiles it once into a flat form (typed operations on four-lane word registers, pointers folded to word offsets, dead operations dropped). A Runner executes one invocation, a 2 x 2 quad, or several invocations in lockstep (masked where they diverge, each with its own steps and budget) against a Backend, which answers what is outside the program: texture samples, loads, queries, unordered-access effects. ProgramShader puts a vertex or pixel program behind the PixelShader trait, in the register vocabulary of the D3D bytecode it came from: inputs and outputs by signature register, constant buffers and samplers by bind slot.
  • Stages WGSL has no entry point for. Their programs lift as functions, which Executable::functions compiles: geometry (the input assembler's topologies and a geometry program's strips), tessellation and tessellator (hull and domain programs over a port of Microsoft's reference fixed-function tessellator), mesh (a thread group's meshlet), Pipeline with Scene (a ray tracing pipeline: DispatchRays, TraceRay, hit groups, miss and callable programs, over acceleration structures the caller builds; inline ray queries too).
  • draw: a small software rasterizer for an indexed triangle list (vertex program, clipping, viewport, rasterization, pixel program, depth test, render targets), for re-running a recorded draw on its programs and setting its pixels against the game's.
  • evaluate: a pixel shader over a grid of UVs, to bake a procedural material to textures. The caller states everything the program reads (the constant layout and values, a texture per sampled slot, a value per used input) and which outputs it wants; the evaluator has no policy of its own and refuses what it is not given. evaluate_periodic bakes to a texture that tiles.
  • probe: runs one vertex of a vertex program with chosen constants to see what it does with them (what a matrix or a table is).
  • value: one node of a program's value graph (iridia::flow::Values) evaluated from the words of the leaves it reads, with the executor's own operations: what a run computes for that lane where it is given the same leaves, bit for bit. The caller answers each leaf (an input lane, a constant word, a texel) or leaves it unknown; a lane that needs an unknown leaf is unknown and names it. A loop's value, a merge whose arms differ, a run-time index, a derivative and a subgroup operation are refused by name.
  • texture and views: a 2D texture as the executor samples it (levels, colour transfer, the sampler state's addressing and bias, the filtering the caller chooses) and resource views bound by slot.

Example

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 HLSL computes min(f.x, f.y) and max(f.x, f.y) into target 0 and u.x / u.y and u.x % u.y into target 1:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    use iridia_exec::{PixelShader, ProgramShader, Sample};

    let program = iridia_dxbc::lift(&std::fs::read("../iridia-dxbc/tests/fixtures/modes.dxbc")?)?;
    let shader = ProgramShader::new(program)?;

    let bits = |v: [f32; 4]| v.map(f32::to_bits);
    let constants = [bits([0.25, 2.0, 4.0, 8.0]), [7, 2, 0, 0], [0; 4]];
    let inputs = vec![[0u32; 4]; shader.input_count()]; // SV_Position, which the program never reads
    let run = shader.execute(&inputs, &[(0, &constants)], &mut |_: Sample| Err("no textures".into()))?;

    assert_eq!(f32::from_bits(run.outputs[0][0]), 0.25); // min(f.x, f.y)
    assert_eq!(f32::from_bits(run.outputs[0][1]), 2.0); // max(f.x, f.y)
    assert_eq!(&run.outputs[1][..2], &[3, 1]); // 7 / 2, 7 % 2
    Ok(())
}

Guarantees and limits

  • Resources::constant_elements supplies constant-buffer arrays by (resource ID, descriptor index), with separate rows for every descriptor. bind_resource_arrays borrows explicit element maps for buffer and constant-buffer arrays, including unbounded arrays; bind_resources uses the maps in Resources. Missing descriptors error when accessed. These bindings and compute writes store only supplied elements, so an unbounded declaration does not allocate by its sentinel count. The older bind_elements callback needs a bounded declaration; use an explicit map for an unbounded one.
  • What a run does follows the program's D3dModes: a program that says its body spells out D3D's integer division, conversions and min/max runs as written, and one that expects denormals flushed to zero has every 32-bit float operation flush its operands and result (sign kept). Anything the program does that its module cannot make valid (an index past the end of a local array, an integer division by zero in the module) is an error of the run, never a silent value. Errors name their bytecode and location.
  • Every run has a step budget (STEP_LIMIT).
  • Sampling belongs to the caller: a scalar execution cannot infer derivatives, filtering or addressing, so a Sampler callback answers texture samples and the caller states the footprint, addressing, filtering and transfer. A quad run supplies the derivatives of its four invocations.
  • A construct the executor does not run is refused by Executable::new with its name. A ray tracing program is not an entry point; it runs inside a Pipeline.
  • The tessellator is a port of the reference tessellator Microsoft published with the Direct3D 11.3 functional specification (MIT licence; the notice is kept in src/tessellator.rs). Its output is held against that reference's recorded output in tests/fixtures/tessellator_reference.txt.

Tests

cargo test -p iridia-exec needs no game files: it runs fixtures through both iridia-dxbc and iridia-dxil (development dependencies, a cycle cargo allows), holding the interface read from either to the values worked out from the HLSL (tests/interface.rs), the evaluator to outputs worked out by hand (tests/evaluate.rs), lockstep lanes against one pixel after another with diverging control flow (tests/lanes.rs), the value evaluator against a run of the same program on the same leaves, bit for bit (tests/value.rs), and the stage drivers to hand-worked meshes, strips, patches and scenes. The comparisons against WARP live in iridia-dxbc and iridia-dxil (tests/exec_warp*.rs).

Check the examples

From the repository root, cargo test -p iridia-exec --doc checks the crate documentation, and cargo build -p iridia-exec --examples builds its example programs. Examples that read paths need the caller's files; the repository ships no game assets.

Licence

GPL-3.0-or-later, apart from the tessellator port noted above.

Texture sampling carries the stated border colour and mip range (SamplerUse, ViewSampler): outside a border axis reads that colour, and the mip range clamps the biased level. A border with no colour remains a refusal. Vector constant expressions compile as initial values, including constants introduced by material specialisation.