use std::collections::{BTreeSet, HashMap};
use iridia_interface::MatrixLines;
pub use iridia_interface::Unshown;
use naga::{BinaryOperator, MathFunction, UnaryOperator};
use crate::flow::{At, Literal, Node, NodeId, Op, Values};
use crate::{ConstantType, Interface, Majorness, ProgramGraph, ResourceId, ResourceKind};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Words {
At(std::ops::Range<u32>),
Indexed,
}
type Word = (i64, u32);
#[allow(clippy::cast_possible_truncation)] fn whole_number(v: f64) -> Option<i64> {
((-9.223_372_036_854_776e18..9.223_372_036_854_776e18).contains(&v) && v.fract() == 0.0)
.then_some(v as i64)
}
fn line_of(values: &Values, index: &[NodeId]) -> i64 {
let literal = |id: NodeId| match values.node(id) {
Node::Literal(Literal(v)) if v.fract() == 0.0 => whole_number(*v),
_ => None,
};
match *index {
[id] => match values.node(id) {
Node::Op {
op: Op::Binary(BinaryOperator::Add),
operands,
} => match *operands.as_slice() {
[a, b] => literal(b).or_else(|| literal(a)).unwrap_or(0),
_ => 0,
},
_ => 0,
},
_ => 0,
}
}
pub fn lines(values: &Values, resource: ResourceId, words: &Words) -> Result<Majorness, Unshown> {
let read = |id: NodeId| -> Option<Word> {
let Node::Global {
resource: Some(r),
at,
} = values.node(id)
else {
return None;
};
if *r != resource {
return None;
}
match (words, at) {
(Words::At(range), At::Component(c)) if range.contains(c) => Some((i64::from(c / 4), c % 4)),
(Words::Indexed, At::Indexed { lane, index }) => Some((
line_of(values, index).saturating_add(i64::from(lane / 4)),
lane % 4,
)),
_ => None,
}
};
let order = post_order(values);
let mut words: HashMap<NodeId, (NodeId, Word)> = HashMap::new();
let mut made_of: HashMap<NodeId, BTreeSet<NodeId>> = HashMap::new();
let mut terms: HashMap<NodeId, BTreeSet<Word>> = HashMap::new();
let (mut along, mut across, mut seen) = (0usize, 0usize, false);
for &id in &order {
seen |= read(id).is_some();
let ways: &[NodeId] = match values.node(id) {
Node::Op {
op: Op::Select,
operands,
} => operands.get(1..).unwrap_or(&[]),
Node::Merge { arms, .. } => arms,
_ => &[],
};
let chosen = || -> Option<(NodeId, Word)> {
let mut matrix = ways.iter().filter_map(|w| words.get(w));
let first = *matrix.next()?;
let one = matrix.all(|(_, w)| *w == first.1);
let others_clear = ways
.iter()
.filter(|w| !words.contains_key(w))
.all(|w| made_of.get(w).is_none_or(BTreeSet::is_empty));
(one && others_clear).then_some(first)
};
if let Some(found) = read(id).map(|w| (id, w)).or_else(chosen) {
words.insert(id, found);
}
let word = |o: NodeId| words.get(&o).copied();
let mut reads: BTreeSet<NodeId> = values
.operands(id)
.iter()
.filter_map(|o| made_of.get(o))
.flatten()
.copied()
.collect();
reads.extend(word(id).map(|(at, _)| at));
made_of.insert(id, reads);
let of = |o: NodeId| terms.get(&o).cloned().unwrap_or_default();
let reads_of = |o: NodeId| made_of.get(&o).cloned().unwrap_or_default();
let product = |a: NodeId, b: NodeId| -> BTreeSet<Word> {
match (word(a), word(b)) {
(Some((at, w)), _) if !reads_of(b).contains(&at) => BTreeSet::from([w]),
(_, Some((at, w))) if !reads_of(a).contains(&at) => BTreeSet::from([w]),
_ if reads_of(a).is_disjoint(&reads_of(b)) && !of(a).is_empty() => of(a),
_ if reads_of(a).is_disjoint(&reads_of(b)) => of(b),
_ => BTreeSet::new(),
}
};
let chosen_terms = || -> BTreeSet<Word> {
let mut matrix = ways.iter().filter(|&&w| !reads_of(w).is_empty()).map(|&w| of(w));
match matrix.next() {
Some(first) if matrix.all(|t| t == first) => first,
_ => BTreeSet::new(),
}
};
let sum: BTreeSet<Word> = match values.node(id) {
Node::Op { op: Op::Select, .. } | Node::Merge { .. } => chosen_terms(),
Node::Op { op, operands } => match (op, operands.as_slice()) {
(Op::Binary(BinaryOperator::Multiply), &[a, b]) => product(a, b),
(Op::Binary(BinaryOperator::Add | BinaryOperator::Subtract), &[a, b]) => {
of(a).into_iter().chain(of(b)).collect()
}
(Op::Unary(UnaryOperator::Negate), &[a]) => of(a),
(Op::Math(MathFunction::Fma), &[a, b, c]) => product(a, b).into_iter().chain(of(c)).collect(),
(Op::Math(MathFunction::Dot), all) if all.len() % 2 == 0 => {
let (x, y) = all.split_at(all.len() / 2);
x.iter().zip(y).flat_map(|(&a, &b)| product(a, b)).collect()
}
_ => BTreeSet::new(),
},
_ => BTreeSet::new(),
};
if sum.len() >= 2 {
let mut lines = sum.iter().map(|(line, _)| line);
let mut lanes = sum.iter().map(|(_, lane)| lane);
let first_line = lines.next();
let first_lane = lanes.next();
if lines.all(|l| Some(l) == first_line) {
along = along.saturating_add(1);
} else if lanes.all(|l| Some(l) == first_lane) {
across = across.saturating_add(1);
}
}
terms.insert(id, sum);
}
match (along, across) {
(0, 0) if !seen => Err(Unshown::NotRead),
(0, 0) => Err(Unshown::NotApplied),
(_, 0) => Ok(Majorness::Row),
(0, _) => Ok(Majorness::Column),
(along, across) => Err(Unshown::BothWays { along, across }),
}
}
pub fn lines_at(
graph: Result<&ProgramGraph, &str>,
interface: &Interface,
resource: ResourceId,
offset: u32,
size: u32,
) -> MatrixLines {
let unexamined = |why: String| Unshown::Unexamined { why };
let found = graph
.map_err(|why| unexamined(format!("the value graph: {why}")))
.and_then(|graph| {
let d3d = interface
.resource(resource)
.and_then(|r| r.provenance.d3d)
.ok_or_else(|| {
unexamined("its buffer has no D3D binding to find it in the program by".into())
})?;
let (buffer, layout) = graph
.interface
.resources
.iter()
.find_map(|r| match &r.kind {
ResourceKind::ConstantBuffer { layout }
if r.provenance
.d3d
.is_some_and(|b| (b.space, b.register) == (d3d.space, d3d.register)) =>
{
Some((r.id, layout))
}
_ => None,
})
.ok_or_else(|| unexamined("the program has no constant buffer at its D3D binding".into()))?;
let end = offset.saturating_add(size);
let at = Words::At(offset / 4..end.div_ceil(4));
match lines(&graph.values, buffer, &at) {
Err(Unshown::NotRead) => {
let mut arrays = layout
.constants
.iter()
.filter(|k| matches!(k.ty, ConstantType::Array { .. }));
let alone = arrays.clone().next().is_some()
&& arrays.all(|k| {
k.offset >= offset
&& k.ty.size().is_some_and(|n| k.offset.saturating_add(n) <= end)
});
if !alone {
return Err(Unshown::NotRead);
}
lines(&graph.values, buffer, &Words::Indexed)
}
other => other,
}
});
match found {
Ok(Majorness::Row) => MatrixLines::Rows,
Ok(Majorness::Column) => MatrixLines::Columns,
Err(why) => MatrixLines::Unshown(why),
}
}
fn post_order(values: &Values) -> Vec<NodeId> {
let mut order = Vec::new();
let mut done = BTreeSet::new();
let mut open = BTreeSet::new();
for root in values.roots() {
let mut stack = vec![(root, false)];
while let Some((id, expanded)) = stack.pop() {
if expanded {
open.remove(&id);
if done.insert(id) {
order.push(id);
}
continue;
}
if done.contains(&id) || !open.insert(id) {
continue;
}
stack.push((id, true));
for o in values.operands(id) {
if !done.contains(&o) && !open.contains(&o) {
stack.push((o, false));
}
}
}
}
order
}