use super::exit_check::dominator_sets;
use super::graph::spirv_block_successors_by_label;
use crate::spirv_module::Operand;
use crate::spirv_module::{Function, Instruction, Module};
use spirv::{Op, StorageClass, Word};
use std::collections::{HashMap, HashSet};
pub(in crate::native) fn demote_nondominating_values(module: &mut Module) -> bool {
let type_kind: HashMap<Word, Op> = module
.types_global_values
.iter()
.filter_map(|i| i.result_id.map(|id| (id, i.class.opcode)))
.collect();
let spillable = |ty: Word| -> bool {
matches!(
type_kind.get(&ty),
Some(
Op::TypeInt
| Op::TypeFloat
| Op::TypeBool
| Op::TypeVector
| Op::TypeMatrix
| Op::TypeArray
| Op::TypeStruct
)
)
};
let mut fn_ptr_type: HashMap<Word, Word> = HashMap::new();
for i in &module.types_global_values {
if i.class.opcode == Op::TypePointer {
if let (
Some(rid),
Some(Operand::StorageClass(StorageClass::Function)),
Some(Operand::IdRef(pointee)),
) = (i.result_id, i.operands.first(), i.operands.get(1))
{
fn_ptr_type.entry(*pointee).or_insert(rid);
}
}
}
let mut next_id = module.header.as_ref().map(|h| h.bound).unwrap_or(0);
let mut fresh = || {
let id = next_id;
next_id += 1;
id
};
let mut new_ptr_types: Vec<Instruction> = Vec::new();
let mut any = false;
for function in &mut module.functions {
any |= demote_in_function(
function,
&spillable,
&mut fn_ptr_type,
&mut new_ptr_types,
&mut fresh,
);
}
if any {
module.types_global_values.extend(new_ptr_types);
if let Some(header) = module.header.as_mut() {
header.bound = next_id;
}
}
any
}
struct Target {
value: Word,
ty: Word,
def_block: usize,
load_blocks: Vec<usize>,
}
fn demote_in_function(
function: &mut Function,
spillable: &impl Fn(Word) -> bool,
fn_ptr_type: &mut HashMap<Word, Word>,
new_ptr_types: &mut Vec<Instruction>,
fresh: &mut impl FnMut() -> Word,
) -> bool {
if function.blocks.len() < 2 {
return false;
}
let Some(entry) = function
.blocks
.first()
.and_then(|b| b.label.as_ref()?.result_id)
else {
return false;
};
let idx_to_label: Vec<Option<Word>> = function
.blocks
.iter()
.map(|b| b.label.as_ref().and_then(|l| l.result_id))
.collect();
let mut label_to_idx: HashMap<Word, usize> = HashMap::new();
let mut labels: Vec<Word> = Vec::new();
for (bi, l) in idx_to_label.iter().enumerate() {
if let Some(rid) = l {
label_to_idx.insert(*rid, bi);
labels.push(*rid);
}
}
let successors = spirv_block_successors_by_label(&function.blocks);
let doms = dominator_sets(entry, &labels, &successors);
let dominates = |a_label: Word, b_label: Word| -> bool {
doms.get(&b_label).is_some_and(|s| s.contains(&a_label))
};
let mut def_block: HashMap<Word, usize> = HashMap::new();
let mut value_type: HashMap<Word, Word> = HashMap::new();
for (bi, block) in function.blocks.iter().enumerate() {
for inst in &block.instructions {
if let Some(rid) = inst.result_id {
def_block.insert(rid, bi);
if let Some(rty) = inst.result_type {
value_type.insert(rid, rty);
}
}
}
}
let mut targets: HashMap<Word, HashSet<usize>> = HashMap::new();
for (bi, block) in function.blocks.iter().enumerate() {
let this_label = idx_to_label[bi];
for inst in &block.instructions {
if inst.class.opcode == Op::Phi {
let mut pos = 0;
while pos + 1 < inst.operands.len() {
if let (Operand::IdRef(v), Operand::IdRef(pred)) =
(&inst.operands[pos], &inst.operands[pos + 1])
{
if let Some(&db) = def_block.get(v) {
if let (Some(db_label), Some(&pred_idx)) =
(idx_to_label[db], label_to_idx.get(pred))
{
if !dominates(db_label, *pred) {
targets.entry(*v).or_default().insert(pred_idx);
}
}
}
}
pos += 2;
}
} else {
let Some(this) = this_label else { continue };
for op in &inst.operands {
if let Operand::IdRef(v) = op {
if let Some(&db) = def_block.get(v) {
if let Some(db_label) = idx_to_label[db] {
if !dominates(db_label, this) {
targets.entry(*v).or_default().insert(bi);
}
}
}
}
}
}
}
}
if targets.is_empty() {
return false;
}
let mut plans: Vec<Target> = Vec::new();
for (value, load_blocks) in targets {
let Some(&ty) = value_type.get(&value) else {
continue;
};
if !spillable(ty) {
continue;
}
let Some(&def_b) = def_block.get(&value) else {
continue;
};
let mut load_blocks: Vec<usize> = load_blocks.into_iter().collect();
load_blocks.sort_unstable();
plans.push(Target {
value,
ty,
def_block: def_b,
load_blocks,
});
}
if plans.is_empty() {
return false;
}
plans.sort_by_key(|t| t.value);
for plan in &plans {
let ptr_ty = *fn_ptr_type.entry(plan.ty).or_insert_with(|| {
let id = fresh();
new_ptr_types.push(Instruction::new(
Op::TypePointer,
None,
Some(id),
vec![
Operand::StorageClass(StorageClass::Function),
Operand::IdRef(plan.ty),
],
));
id
});
let var_id = fresh();
{
let entry_block = &mut function.blocks[0];
let after_vars = entry_block
.instructions
.iter()
.position(|i| i.class.opcode != Op::Variable)
.unwrap_or(0);
entry_block.instructions.insert(
after_vars,
Instruction::new(
Op::Variable,
Some(ptr_ty),
Some(var_id),
vec![Operand::StorageClass(StorageClass::Function)],
),
);
}
{
let block = &mut function.blocks[plan.def_block];
let def_idx = block
.instructions
.iter()
.position(|i| i.result_id == Some(plan.value))
.expect("def instruction present");
let first_non_phi = block
.instructions
.iter()
.position(|i| i.class.opcode != Op::Phi)
.unwrap_or(block.instructions.len());
let mut at = (def_idx + 1).max(first_non_phi);
let n = block.instructions.len();
let safe_max = if n >= 2
&& matches!(
block.instructions[n - 2].class.opcode,
Op::SelectionMerge | Op::LoopMerge
) {
n - 2
} else {
n - 1
};
at = at.min(safe_max);
block.instructions.insert(
at,
Instruction::new(
Op::Store,
None,
None,
vec![Operand::IdRef(var_id), Operand::IdRef(plan.value)],
),
);
}
for &lb in &plan.load_blocks {
let load_id = fresh();
let lb_label = idx_to_label[lb];
{
let block = &mut function.blocks[lb];
let after_phis = block
.instructions
.iter()
.position(|i| i.class.opcode != Op::Phi && i.class.opcode != Op::Variable)
.unwrap_or(0);
block.instructions.insert(
after_phis,
Instruction::new(
Op::Load,
Some(plan.ty),
Some(load_id),
vec![Operand::IdRef(var_id)],
),
);
for inst in block.instructions.iter_mut() {
if inst.class.opcode == Op::Phi
|| inst.class.opcode == Op::Variable
|| inst.result_id == Some(load_id)
{
continue;
}
if inst.class.opcode == Op::Store
&& inst.operands.first() == Some(&Operand::IdRef(var_id))
{
continue;
}
for op in inst.operands.iter_mut() {
if *op == Operand::IdRef(plan.value) {
*op = Operand::IdRef(load_id);
}
}
}
}
if let Some(lb_label) = lb_label {
for other in function.blocks.iter_mut() {
for inst in other.instructions.iter_mut() {
if inst.class.opcode != Op::Phi {
continue;
}
let mut pos = 0;
while pos + 1 < inst.operands.len() {
if inst.operands[pos] == Operand::IdRef(plan.value)
&& inst.operands[pos + 1] == Operand::IdRef(lb_label)
{
inst.operands[pos] = Operand::IdRef(load_id);
}
pos += 2;
}
}
}
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spirv_module::{Block, ModuleHeader};
fn inst(op: Op, ty: Option<Word>, res: Option<Word>, ops: Vec<Operand>) -> Instruction {
Instruction::new(op, ty, res, ops)
}
fn block(label: Word, instrs: Vec<Instruction>) -> Block {
Block {
label: Some(inst(Op::Label, None, Some(label), vec![])),
instructions: instrs,
}
}
#[test]
fn demotes_loop_body_value_used_after_early_exit_loop() {
let mut m = Module::new();
m.header = Some(ModuleHeader::new(60));
m.types_global_values = vec![
inst(Op::TypeBool, None, Some(1), vec![]),
inst(
Op::TypeFloat,
None,
Some(2),
vec![Operand::LiteralBit32(32)],
),
];
let entry = block(
10,
vec![inst(Op::Branch, None, None, vec![Operand::IdRef(11)])],
);
let header = block(
11,
vec![
inst(Op::Undef, Some(1), Some(20), vec![]),
inst(
Op::LoopMerge,
None,
None,
vec![
Operand::IdRef(14),
Operand::IdRef(13),
Operand::LoopControl(spirv::LoopControl::NONE),
],
),
inst(
Op::BranchConditional,
None,
None,
vec![Operand::IdRef(20), Operand::IdRef(12), Operand::IdRef(14)],
),
],
);
let body = block(
12,
vec![
inst(
Op::FAdd,
Some(2),
Some(30),
vec![Operand::IdRef(2), Operand::IdRef(2)],
),
inst(Op::Branch, None, None, vec![Operand::IdRef(13)]),
],
);
let latch = block(
13,
vec![inst(Op::Branch, None, None, vec![Operand::IdRef(11)])],
);
let merge = block(
14,
vec![
inst(
Op::FSub,
Some(2),
Some(40),
vec![Operand::IdRef(30), Operand::IdRef(30)],
),
inst(Op::Return, None, None, vec![]),
],
);
let mut func = Function::new();
func.blocks = vec![entry, header, body, latch, merge];
m.functions = vec![func];
assert!(demote_nondominating_values(&mut m));
let ptr_ty = m
.types_global_values
.iter()
.find(|i| {
i.class.opcode == Op::TypePointer
&& i.operands.first() == Some(&Operand::StorageClass(StorageClass::Function))
&& i.operands.get(1) == Some(&Operand::IdRef(2))
})
.and_then(|i| i.result_id)
.expect("Function float pointer type synthesized");
let f = &m.functions[0];
let var = f.blocks[0]
.instructions
.iter()
.find(|i| i.class.opcode == Op::Variable)
.expect("OpVariable in entry");
assert_eq!(var.result_type, Some(ptr_ty));
let var_id = var.result_id.unwrap();
let body = &f.blocks[2];
let store_pos = body
.instructions
.iter()
.position(|i| {
i.class.opcode == Op::Store
&& i.operands == vec![Operand::IdRef(var_id), Operand::IdRef(30)]
})
.expect("store of the loop-body value after its def");
let def_pos = body
.instructions
.iter()
.position(|i| i.result_id == Some(30))
.unwrap();
assert!(store_pos > def_pos);
assert_eq!(body.instructions.last().unwrap().class.opcode, Op::Branch);
let merge = &f.blocks[4];
let load = merge
.instructions
.iter()
.find(|i| i.class.opcode == Op::Load && i.operands == vec![Operand::IdRef(var_id)])
.expect("load at the merge");
let load_id = load.result_id.unwrap();
let fsub = merge
.instructions
.iter()
.find(|i| i.class.opcode == Op::FSub)
.unwrap();
assert_eq!(
fsub.operands,
vec![Operand::IdRef(load_id), Operand::IdRef(load_id)]
);
}
#[test]
fn valid_module_is_untouched() {
let mut m = Module::new();
m.header = Some(ModuleHeader::new(30));
m.types_global_values = vec![inst(
Op::TypeFloat,
None,
Some(2),
vec![Operand::LiteralBit32(32)],
)];
let entry = block(
1,
vec![
inst(
Op::FAdd,
Some(2),
Some(10),
vec![Operand::IdRef(2), Operand::IdRef(2)],
),
inst(Op::Branch, None, None, vec![Operand::IdRef(2)]),
],
);
let tail = block(
2,
vec![
inst(
Op::FSub,
Some(2),
Some(11),
vec![Operand::IdRef(10), Operand::IdRef(10)],
),
inst(Op::Return, None, None, vec![]),
],
);
let mut func = Function::new();
func.blocks = vec![entry, tail];
m.functions = vec![func];
let before = m.clone();
assert!(!demote_nondominating_values(&mut m));
assert_eq!(
m.functions[0].blocks.len(),
before.functions[0].blocks.len()
);
assert_eq!(
m.functions[0].blocks[1].instructions[0].operands,
vec![Operand::IdRef(10), Operand::IdRef(10)]
);
}
}