iridia-exec 0.1.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).
  • 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

  • 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), 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).

Licence

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