iridia 0.5.0

Compiled Direct3D shaders as naga modules: the program type the iridia readers produce, with validation, a WGSL writer and analyses
Documentation
//! How a program holds a matrix constant ([`iridia::matrix::lines`]), on the lifters' fixtures: a domain program
//! that applies a `column_major` matrix as `mul(M, v)` (its lines are the columns of the matrix it applies) as fxc and dxc
//! compile it, optimized and not, and a vertex program that applies one as `mul(v, M)` (the lines are the rows of the
//! matrix it applies to column vectors, the transpose) as dxc compiles it for two shader models and unoptimized. The
//! two lifters write a dot product as the same node, so the row form is not fxc-specific. The domain program is not walked
//! by the value graph (it has no entry point a WebGPU module can hold), so the column form is held on fxc's vertex
//! programs of FromSoftware's fixtures, which also hold two palettes chosen by an index, one blended across joints, and
//! a cloth program's blend chosen by a flag, as dxc and fxc compile it.
use iridia::flow::Values;
use iridia::matrix::{Unshown, Words, lines};
use iridia::{Majorness, Program, ResourceKind};

/// The arrangement `lines` reads for the constant `name`, by its place in the program's own layout, or, for a palette,
/// by the indexed reads of its buffer.
fn lines_of(program: &Program, name: &str) -> Result<Majorness, Unshown> {
    read(program, name, false)
}

fn palette_lines_of(program: &Program, name: &str) -> Result<Majorness, Unshown> {
    read(program, name, true)
}

fn read(program: &Program, name: &str, indexed: bool) -> Result<Majorness, Unshown> {
    let values = Values::of_program(program).expect("the value graph");
    for r in &program.interface.resources {
        let ResourceKind::ConstantBuffer { layout } = &r.kind else {
            continue;
        };
        if let Some(c) = layout.constants.iter().find(|c| c.name == name) {
            let size = c.ty.size().expect("a sized constant");
            let words = if indexed {
                Words::Indexed
            } else {
                Words::At(c.offset / 4..(c.offset + size).div_ceil(4))
            };
            return lines(&values, r.id, &words);
        }
    }
    panic!("no constant {name}")
}

fn dxbc(bytes: &[u8]) -> Program {
    iridia_dxbc::lift(bytes).expect("fxc's program lifts")
}

fn dxil(bytes: &[u8]) -> Program {
    iridia_dxil::lift(bytes).expect("dxc's program lifts")
}

const SINGLE_UV_VS: &[u8] = include_bytes!("fixtures/single_uv_vs.dxbc");
const SKIN_VS: &[u8] = include_bytes!("fixtures/layered_skin_vs.dxbc");

/// `mul(M, v)` of a `column_major` matrix scales each line by a lane of the vector: its lines are columns.
#[test]
fn a_matrix_applied_to_a_column_vector_is_read_by_columns() {
    let single = dxbc(SINGLE_UV_VS);
    assert_eq!(lines_of(&single, "VC_MatrixViewProj"), Ok(Majorness::Column));
    let skin = dxbc(SKIN_VS);
    for name in ["VC_MatrixViewProj", "FC_MatrixView"] {
        assert_eq!(lines_of(&skin, name), Ok(Majorness::Column), "{name}");
    }
}

/// A palette chosen by an index is read by the line of its element the index adds: `mul(v, M[i])` of a `column_major`
/// `float4x3` dots the vector with each of the element's lines, and a blend of the joints' matrices line by line
/// (`M[a] * wa + M[b] * wb`) that the program then dots the position with is read by rows too, the blend not being a
/// sum across lines.
#[test]
fn a_palette_is_read_by_the_lines_of_its_elements() {
    let skin = dxbc(SKIN_VS);
    for name in ["VC_aClothCancelObjMatrix", "VC_aObjMatrix"] {
        assert_eq!(palette_lines_of(&skin, name), Ok(Majorness::Row), "{name}");
    }
}

/// A cloth program that blends its joints' matrices word by word and then chooses each blended word by a flag against
/// another buffer's matrix (Elden Ring's, as dxc and fxc compile its shape) is read through the choice: the position is
/// dotted with each blended line, a row.
#[test]
fn a_palette_blended_then_chosen_by_a_flag_is_read_by_rows() {
    for (fixture, program) in [
        (
            "dxc",
            dxil(include_bytes!("fixtures/layered_cloth_blend_vs.dxil")),
        ),
        (
            "fxc",
            dxbc(include_bytes!("fixtures/layered_cloth_blend_vs.dxbc")),
        ),
    ] {
        assert_eq!(
            palette_lines_of(&program, "VC_aObjMatrix"),
            Ok(Majorness::Row),
            "{fixture}"
        );
    }
}

/// A program that reads a palette's translations alone (Elden Ring's LOD billboards add them to an offset) applies none
/// of its matrices: it shows no arrangement.
#[test]
fn a_palette_read_only_for_its_translations_shows_no_arrangement() {
    let billboard = dxbc(include_bytes!("fixtures/layered_billboard_vs.dxbc"));
    assert_eq!(
        palette_lines_of(&billboard, "VC_aObjMatrix"),
        Err(Unshown::NotApplied)
    );
}

/// `mul(v, M)` of a `column_major` matrix dots the vector with each line: they are the rows of the matrix applied to
/// column vectors (`M` transposed).
#[test]
fn a_matrix_applied_to_a_row_vector_is_read_by_rows() {
    for (fixture, program) in [
        ("vertex.dxil", dxil(include_bytes!("fixtures/vertex.dxil"))),
        ("vertex.66.dxil", dxil(include_bytes!("fixtures/vertex.66.dxil"))),
        ("vertex.od.dxil", dxil(include_bytes!("fixtures/vertex.od.dxil"))),
    ] {
        assert_eq!(
            lines_of(&program, "world_view_projection"),
            Ok(Majorness::Row),
            "{fixture}"
        );
    }
}

/// A constant the program reads but never multiplies by another value shows no arrangement, and is refused with that.
#[test]
fn a_constant_never_applied_shows_no_arrangement() {
    let program = dxil(include_bytes!("fixtures/vertex.dxil"));
    assert_eq!(lines_of(&program, "tint"), Err(Unshown::NotApplied));
}

/// `lines_at` reads a constant's lines from the program, finding its buffer by the D3D binding: the clip matrix by
/// columns, the joints' palette (its buffer's one array) through its indexed reads by rows, and a matrix the program
/// reads but never applies unshown, with the reason; a graph that could not be built leaves it unexamined, with why.
#[test]
fn a_constant_s_lines_are_read_at_its_place() {
    use iridia::matrix::lines_at;
    use iridia_interface::MatrixLines;
    let skin = dxbc(SKIN_VS);
    let graph = skin.graph().expect("the value graph");
    let at = |graph: Result<&iridia::ProgramGraph, &str>, name: &str| {
        let (resource, c) = skin
            .interface
            .resources
            .iter()
            .find_map(|r| match &r.kind {
                ResourceKind::ConstantBuffer { layout } => layout
                    .constants
                    .iter()
                    .find(|c| c.name == name)
                    .map(|c| (r.id, c)),
                _ => None,
            })
            .expect("the constant");
        let size = c.ty.size().expect("a sized constant");
        lines_at(graph, &skin.interface, resource, c.offset, size)
    };
    assert_eq!(at(Ok(&graph), "VC_MatrixViewProj"), MatrixLines::Columns);
    assert_eq!(at(Ok(&graph), "VC_aObjMatrix"), MatrixLines::Rows);
    // read, but only added and scaled lane by lane: not applied as a matrix
    assert_eq!(
        at(Ok(&graph), "SC_CameraPos"),
        MatrixLines::Unshown(Unshown::NotApplied)
    );
    assert_eq!(
        at(Err("a call"), "VC_MatrixViewProj"),
        MatrixLines::Unshown(Unshown::Unexamined {
            why: "the value graph: a call".into()
        })
    );
}