use crate::spirv_module::Module;
use crate::spirv_module::Operand;
use crate::spirv_module::{Block, Function, Instruction};
use spirv::{Op, StorageClass, Word};
use std::collections::{HashMap, HashSet};
pub(super) fn rewrite_to_relooper(module: &mut Module, max_blocks: usize) -> bool {
let mut next_id = module.header.as_ref().map(|h| h.bound).unwrap_or(1);
let mut tc = TypeCtx::new(module, &mut next_id);
let mut any = false;
let mut functions = std::mem::take(&mut module.functions);
if crate::env_vars::reloop_why() {
eprintln!("RELOOP-ENTER functions={}", functions.len());
}
for function in &mut functions {
if rewrite_function(function, &mut tc, max_blocks) {
any = true;
}
}
module.functions = functions;
tc.flush(module);
if let Some(h) = module.header.as_mut() {
h.bound = next_id;
}
any
}
struct TypeCtx<'a> {
next_id: &'a mut Word,
int_types: HashMap<(u32, u32), Word>, bool_type: Option<Word>,
ptr_func: HashMap<Word, Word>, int_consts: HashMap<(Word, u64), Word>, type_op: HashMap<Word, Op>,
pending: Vec<Instruction>,
}
impl<'a> TypeCtx<'a> {
fn new(module: &Module, next_id: &'a mut Word) -> Self {
let mut int_types = HashMap::new();
let mut bool_type = None;
let mut ptr_func = HashMap::new();
let mut int_consts = HashMap::new();
let mut type_op = HashMap::new();
for inst in &module.types_global_values {
let Some(rid) = inst.result_id else { continue };
type_op.insert(rid, inst.class.opcode);
match inst.class.opcode {
Op::TypeInt => {
if let (Some(Operand::LiteralBit32(w)), Some(Operand::LiteralBit32(s))) =
(inst.operands.first(), inst.operands.get(1))
{
int_types.insert((*w, *s), rid);
}
}
Op::TypeBool => bool_type = Some(rid),
Op::TypePointer => {
if let (
Some(Operand::StorageClass(StorageClass::Function)),
Some(Operand::IdRef(p)),
) = (inst.operands.first(), inst.operands.get(1))
{
ptr_func.insert(*p, rid);
}
}
Op::Constant => {
if let (Some(ty), Some(Operand::LiteralBit32(v))) =
(inst.result_type, inst.operands.first())
{
int_consts.insert((ty, *v as u64), rid);
}
}
_ => {}
}
}
TypeCtx {
next_id,
int_types,
bool_type,
ptr_func,
int_consts,
type_op,
pending: Vec::new(),
}
}
fn fresh(&mut self) -> Word {
let id = *self.next_id;
*self.next_id += 1;
id
}
fn type_opcode(&self, ty: Word) -> Option<Op> {
self.type_op.get(&ty).copied()
}
fn int_ty(&mut self, width: u32, signed: u32) -> Word {
if let Some(&id) = self.int_types.get(&(width, signed)) {
return id;
}
let id = self.fresh();
self.pending.push(Instruction::new(
Op::TypeInt,
None,
Some(id),
vec![Operand::LiteralBit32(width), Operand::LiteralBit32(signed)],
));
self.type_op.insert(id, Op::TypeInt);
self.int_types.insert((width, signed), id);
id
}
fn i32_ty(&mut self) -> Word {
self.int_ty(32, 0)
}
fn bool_ty(&mut self) -> Word {
if let Some(id) = self.bool_type {
return id;
}
let id = self.fresh();
self.pending
.push(Instruction::new(Op::TypeBool, None, Some(id), vec![]));
self.type_op.insert(id, Op::TypeBool);
self.bool_type = Some(id);
id
}
fn ptr_function(&mut self, pointee: Word) -> Word {
if let Some(&id) = self.ptr_func.get(&pointee) {
return id;
}
let id = self.fresh();
self.pending.push(Instruction::new(
Op::TypePointer,
None,
Some(id),
vec![
Operand::StorageClass(StorageClass::Function),
Operand::IdRef(pointee),
],
));
self.type_op.insert(id, Op::TypePointer);
self.ptr_func.insert(pointee, id);
id
}
fn int_const(&mut self, ty: Word, value: u64) -> Word {
if let Some(&id) = self.int_consts.get(&(ty, value)) {
return id;
}
let id = self.fresh();
self.pending.push(Instruction::new(
Op::Constant,
Some(ty),
Some(id),
vec![Operand::LiteralBit32(value as u32)],
));
self.int_consts.insert((ty, value), id);
id
}
fn flush(self, module: &mut Module) {
module.types_global_values.extend(self.pending);
}
}
enum Term {
Branch(Word),
BranchCond(Word, Word, Word), Switch(Word, Word, Vec<(u32, Word)>), Return,
ReturnValue(Word),
Unreachable,
Kill(Instruction), }
fn decode_term(inst: &Instruction) -> Option<Term> {
match inst.class.opcode {
Op::Branch => match inst.operands.first()? {
Operand::IdRef(t) => Some(Term::Branch(*t)),
_ => None,
},
Op::BranchConditional => {
let (Operand::IdRef(c), Operand::IdRef(t), Operand::IdRef(f)) = (
inst.operands.first()?,
inst.operands.get(1)?,
inst.operands.get(2)?,
) else {
return None;
};
Some(Term::BranchCond(*c, *t, *f))
}
Op::Switch => {
let Operand::IdRef(sel) = inst.operands.first()? else {
return None;
};
let Operand::IdRef(def) = inst.operands.get(1)? else {
return None;
};
let mut cases = Vec::new();
let mut i = 2;
while i + 1 < inst.operands.len() {
let lit = match &inst.operands[i] {
Operand::LiteralBit32(v) => *v,
_ => return None,
};
let Operand::IdRef(lbl) = &inst.operands[i + 1] else {
return None;
};
cases.push((lit, *lbl));
i += 2;
}
Some(Term::Switch(*sel, *def, cases))
}
Op::Return => Some(Term::Return),
Op::ReturnValue => match inst.operands.first()? {
Operand::IdRef(v) => Some(Term::ReturnValue(*v)),
_ => None,
},
Op::Unreachable => Some(Term::Unreachable),
Op::Kill | Op::TerminateInvocation => Some(Term::Kill(inst.clone())),
_ => None,
}
}
fn block_label(block: &Block) -> Option<Word> {
block.label.as_ref().and_then(|l| l.result_id)
}
const MAX_RELOOPER_BLOCKS: usize = 1024;
pub(super) fn default_max_relooper_blocks() -> usize {
crate::env_vars::relooper_max_blocks(MAX_RELOOPER_BLOCKS)
}
fn rewrite_function(function: &mut Function, tc: &mut TypeCtx, max_blocks: usize) -> bool {
let bail = |_r: &str| -> bool {
if crate::env_vars::reloop_why() {
eprintln!("RELOOP-BAIL {_r} (blocks={})", function.blocks.len());
}
false
};
if crate::env_vars::reloop_why() {
eprintln!(
"RELOOP-FN blocks={} max={}",
function.blocks.len(),
max_blocks
);
}
if function.blocks.len() < 2 {
return bail("too-few-blocks");
}
if function.blocks.len() > max_blocks {
return bail("too-many-blocks");
}
let mut terms: Vec<Term> = Vec::with_capacity(function.blocks.len());
for block in &function.blocks {
let Some(last) = block.instructions.last() else {
return bail("empty-block");
};
match decode_term(last) {
Some(t) => terms.push(t),
None => return bail("unhandled-terminator"),
}
}
let labels: Vec<Word> = match function
.blocks
.iter()
.map(block_label)
.collect::<Option<_>>()
{
Some(v) => v,
None => return false,
};
let label_index: HashMap<Word, usize> =
labels.iter().enumerate().map(|(i, l)| (*l, i)).collect();
let entry_term = &terms[0];
let entry_has_successor = matches!(
entry_term,
Term::Branch(_) | Term::BranchCond(..) | Term::Switch(..)
);
let mut all_targets: HashSet<Word> = HashSet::new();
for t in &terms {
match t {
Term::Branch(x) => {
all_targets.insert(*x);
}
Term::BranchCond(_, a, b) => {
all_targets.insert(*a);
all_targets.insert(*b);
}
Term::Switch(_, d, cs) => {
all_targets.insert(*d);
for (_, l) in cs {
all_targets.insert(*l);
}
}
_ => {}
}
}
if !entry_has_successor {
return bail("entry-no-successor");
}
if !all_targets
.iter()
.all(|target| label_index.contains_key(target))
{
return bail("missing-target");
}
if all_targets.contains(&labels[0]) {
return bail("entry-is-branch-target");
}
if function.blocks[0]
.instructions
.iter()
.any(|i| i.class.opcode == Op::Phi)
{
return bail("entry-has-phi");
}
let mut def_block: HashMap<Word, usize> = HashMap::new();
let mut phis: HashMap<Word, (Word, Vec<(Word, Word)>)> = HashMap::new();
let mut value_type: HashMap<Word, Word> = HashMap::new();
let mut variables: Vec<Instruction> = Vec::new();
let mut variable_ids: HashSet<Word> = HashSet::new();
let mut ptr_def: HashMap<Word, Instruction> = HashMap::new();
for (bi, block) in function.blocks.iter().enumerate() {
for inst in &block.instructions {
if let Some(rid) = inst.result_id {
if let Some(rty) = inst.result_type {
value_type.insert(rid, rty);
}
def_block.insert(rid, bi);
if matches!(
inst.class.opcode,
Op::AccessChain
| Op::InBoundsAccessChain
| Op::PtrAccessChain
| Op::Select
| Op::CopyObject
) {
ptr_def.insert(rid, inst.clone());
}
}
match inst.class.opcode {
Op::Phi => {
let rid = inst.result_id.unwrap();
let rty = match inst.result_type {
Some(t) => t,
None => return false,
};
let mut incoming = Vec::new();
let mut k = 0;
while k + 1 < inst.operands.len() {
let (Operand::IdRef(v), Operand::IdRef(p)) =
(&inst.operands[k], &inst.operands[k + 1])
else {
return false;
};
incoming.push((*v, *p));
k += 2;
}
phis.insert(rid, (rty, incoming));
}
Op::Variable => {
variables.push(inst.clone());
variable_ids.insert(inst.result_id.unwrap_or(0));
}
_ => {}
}
}
}
let block_phis: Vec<Vec<Word>> = function
.blocks
.iter()
.map(|b| {
b.instructions
.iter()
.filter(|i| i.class.opcode == Op::Phi)
.filter_map(|i| i.result_id)
.collect()
})
.collect();
let param_ids: HashSet<Word> = function
.parameters
.iter()
.filter_map(|p| p.result_id)
.collect();
let mut demote: HashSet<Word> = phis.keys().copied().collect();
for (bi, block) in function.blocks.iter().enumerate() {
for inst in &block.instructions {
for op in &inst.operands {
if let Operand::IdRef(id) = op {
if param_ids.contains(id) || variable_ids.contains(id) {
continue;
}
if let Some(&db) = def_block.get(id) {
if db != bi && db != 0 {
demote.insert(*id);
}
}
}
}
}
}
for (_, incoming) in phis.values() {
for (val, pred) in incoming {
if param_ids.contains(val) || variable_ids.contains(val) {
continue;
}
let Some(&pred_bi) = label_index.get(pred) else {
continue;
};
if let Some(&db) = def_block.get(val) {
if db != pred_bi && db != 0 {
demote.insert(*val);
}
}
}
}
let dominates_all_cases = |id: &Word| -> bool {
!def_block.contains_key(id)
|| def_block.get(id) == Some(&0)
|| param_ids.contains(id)
|| variable_ids.contains(id)
};
let mut remat: HashMap<Word, Instruction> = HashMap::new();
let mut remat_scalar_demote: HashSet<Word> = HashSet::new();
loop {
let mut newly: Vec<(Word, Instruction, Vec<Word>)> = Vec::new();
for (&v, inst) in &ptr_def {
if remat.contains_key(&v) {
continue;
}
let Some(&ty) = value_type.get(&v) else {
continue;
};
if tc.type_opcode(ty) != Some(Op::TypePointer) {
continue;
}
let ids: Option<Vec<Word>> = inst
.operands
.iter()
.map(|o| match o {
Operand::IdRef(i) => Some(*i),
_ => None,
})
.collect();
let Some(ids) = ids else { continue };
let (ptr_ops, scalar_ops): (&[Word], &[Word]) = match inst.class.opcode {
Op::AccessChain | Op::InBoundsAccessChain | Op::PtrAccessChain
if ids.len() >= 2 =>
{
(&ids[..1], &ids[1..]) }
Op::Select if ids.len() == 3 => (&ids[1..3], &ids[..1]), Op::CopyObject if ids.len() == 1 => (&ids[..1], &ids[..0]), _ => continue,
};
if !ptr_ops
.iter()
.all(|p| dominates_all_cases(p) || remat.contains_key(p))
{
continue;
}
let mut pending: Vec<Word> = Vec::new();
let mut ok = true;
for s in scalar_ops {
if dominates_all_cases(s) {
continue;
}
match value_type.get(s).and_then(|t| tc.type_opcode(*t)) {
Some(Op::TypeInt | Op::TypeFloat | Op::TypeVector | Op::TypeBool) => {
pending.push(*s)
}
_ => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
newly.push((v, inst.clone(), pending));
}
if newly.is_empty() {
break;
}
for (v, inst, pending) in newly {
remat.insert(v, inst);
remat_scalar_demote.extend(pending);
}
}
for v in remat.keys() {
demote.remove(v);
}
demote.extend(remat_scalar_demote);
let mut remat_phi: HashMap<Word, (Word, Vec<(Word, Word)>)> = HashMap::new();
loop {
let mut added = false;
for (&pid, (pty, incoming)) in &phis {
if remat_phi.contains_key(&pid)
|| !demote.contains(&pid)
|| tc.type_opcode(*pty) != Some(Op::TypePointer)
{
continue;
}
if incoming.iter().all(|(v, _)| {
*v != pid
&& (dominates_all_cases(v)
|| remat.contains_key(v)
|| remat_phi.contains_key(v))
}) {
remat_phi.insert(pid, (*pty, incoming.clone()));
added = true;
}
}
if !added {
break;
}
}
for pid in remat_phi.keys() {
demote.remove(pid);
}
let mut remat_phi_invariant: HashSet<Word> = HashSet::new();
for (&pid, (_, incoming)) in &remat_phi {
let key_of = |v: Word| -> (u32, Vec<Operand>) {
match remat.get(&v) {
Some(inst) => (inst.class.opcode as u32, inst.operands.clone()),
None => (u32::MAX, vec![Operand::IdRef(v)]),
}
};
let mut keys = incoming.iter().map(|(v, _)| key_of(*v));
if let Some(first) = keys.next() {
if keys.all(|k| k == first) {
remat_phi_invariant.insert(pid);
}
}
}
for v in &demote {
let Some(&ty) = value_type.get(v) else {
return bail("non-spillable-demote");
};
match tc.type_opcode(ty) {
Some(Op::TypePointer)
| Some(Op::TypeImage)
| Some(Op::TypeSampler)
| Some(Op::TypeSampledImage)
| Some(Op::TypeAccelerationStructureKHR)
| Some(Op::TypeRuntimeArray)
| None => {
return bail("non-spillable-demote");
}
_ => {}
}
}
let i32_ty = tc.i32_ty();
let ptr_i32 = tc.ptr_function(i32_ty);
let bool_ty = tc.bool_ty();
let mut demote_order: Vec<Word> = demote.iter().copied().collect();
demote_order.sort_unstable();
let mut spill: HashMap<Word, (Word, Word)> = HashMap::new(); let mut spill_vars: Vec<Instruction> = Vec::new();
for &v in &demote_order {
let ty = value_type[&v];
let ptr_ty = tc.ptr_function(ty);
let var = tc.fresh();
spill_vars.push(Instruction::new(
Op::Variable,
Some(ptr_ty),
Some(var),
vec![Operand::StorageClass(StorageClass::Function)],
));
spill.insert(v, (var, ty));
}
let mut remat_phi_order: Vec<Word> = remat_phi.keys().copied().collect();
remat_phi_order.sort_unstable();
let mut phi_tag: HashMap<Word, Word> = HashMap::new();
for &pid in &remat_phi_order {
let var = tc.fresh();
spill_vars.push(Instruction::new(
Op::Variable,
Some(ptr_i32),
Some(var),
vec![Operand::StorageClass(StorageClass::Function)],
));
phi_tag.insert(pid, var);
}
let state_var = tc.fresh();
spill_vars.push(Instruction::new(
Op::Variable,
Some(ptr_i32),
Some(state_var),
vec![Operand::StorageClass(StorageClass::Function)],
));
let case_const: Vec<Word> = (0..function.blocks.len())
.map(|i| tc.int_const(i32_ty, i as u64))
.collect();
let new_entry = tc.fresh();
let loop_header = tc.fresh();
let dispatch = tc.fresh();
let switch_default_break = tc.fresh();
let sel_merge = tc.fresh();
let loop_continue = tc.fresh();
let loop_merge = tc.fresh();
let phi_ids: HashSet<Word> = phis
.keys()
.copied()
.filter(|p| !remat_phi.contains_key(p))
.collect();
let demo = Demo {
spill: &spill,
demote: &demote,
def_block: &def_block,
phi: &phi_ids,
remat: &remat,
remat_phi: &remat_phi,
phi_tag: &phi_tag,
remat_phi_invariant: &remat_phi_invariant,
};
let mut case_blocks: Vec<Block> = Vec::with_capacity(function.blocks.len());
let mut entry_processed: Vec<Instruction> = Vec::new();
for (bi, block) in function.blocks.iter().enumerate() {
let this_label = labels[bi];
let exit_target = if bi == 0 { loop_header } else { sel_merge };
let mut prelude: Vec<Instruction> = Vec::new();
let mut local_load: HashMap<Word, Word> = HashMap::new();
let mut local_remat: HashMap<Word, Word> = HashMap::new();
let mut body: Vec<Instruction> = Vec::new();
let n = block.instructions.len();
for (ii, inst) in block.instructions.iter().enumerate() {
if matches!(
inst.class.opcode,
Op::Phi | Op::Variable | Op::LoopMerge | Op::SelectionMerge
) || ii == n - 1
{
continue;
}
let mut inst = inst.clone();
for op in &mut inst.operands {
if let Operand::IdRef(id) = op {
if remat_phi.contains_key(id)
|| (remat.contains_key(id) && def_block.get(id) != Some(&bi))
{
let r = rematerialize(
tc,
&mut prelude,
&mut local_remat,
&mut local_load,
&demo,
bi,
*id,
);
*op = Operand::IdRef(r);
} else if demote.contains(id)
&& (phi_ids.contains(id) || def_block.get(id) != Some(&bi))
{
let l = load_demoted(tc, &mut prelude, &mut local_load, &spill, *id);
*op = Operand::IdRef(l);
}
}
}
let spill_after = inst.result_id.filter(|r| demote.contains(r));
body.push(inst);
if let Some(r) = spill_after {
let (var, _) = spill[&r];
body.push(Instruction::new(
Op::Store,
None,
None,
vec![Operand::IdRef(var), Operand::IdRef(r)],
));
}
}
let mut tail: Vec<Instruction> = Vec::new();
match &terms[bi] {
Term::Branch(t) => {
store_phi_edges(
tc,
&mut prelude,
&mut tail,
&mut local_load,
&demo,
bi,
this_label,
*t,
&label_index,
&block_phis,
&phis,
);
let next = case_const[label_index[t]];
tail.push(store_state(state_var, next));
tail.push(branch(exit_target));
}
Term::BranchCond(c, t, f) => {
store_phi_edges(
tc,
&mut prelude,
&mut tail,
&mut local_load,
&demo,
bi,
this_label,
*t,
&label_index,
&block_phis,
&phis,
);
store_phi_edges(
tc,
&mut prelude,
&mut tail,
&mut local_load,
&demo,
bi,
this_label,
*f,
&label_index,
&block_phis,
&phis,
);
if t == f {
let next = case_const[label_index[t]];
tail.push(store_state(state_var, next));
} else {
let cond = resolve(tc, &mut prelude, &mut local_load, &demo, bi, *c);
let tc_const = case_const[label_index[t]];
let fc_const = case_const[label_index[f]];
let sel = tc.fresh();
tail.push(Instruction::new(
Op::Select,
Some(i32_ty),
Some(sel),
vec![
Operand::IdRef(cond),
Operand::IdRef(tc_const),
Operand::IdRef(fc_const),
],
));
tail.push(store_state(state_var, sel));
}
tail.push(branch(exit_target));
}
Term::Switch(selv, def, cases) => {
store_phi_edges(
tc,
&mut prelude,
&mut tail,
&mut local_load,
&demo,
bi,
this_label,
*def,
&label_index,
&block_phis,
&phis,
);
for (_, lbl) in cases {
store_phi_edges(
tc,
&mut prelude,
&mut tail,
&mut local_load,
&demo,
bi,
this_label,
*lbl,
&label_index,
&block_phis,
&phis,
);
}
let selector = resolve(tc, &mut prelude, &mut local_load, &demo, bi, *selv);
let sel_ty = value_type.get(selv).copied().unwrap_or(i32_ty);
let mut cur = case_const[label_index[def]];
for (lit, lbl) in cases {
let lit_const = tc.int_const(sel_ty, *lit as u64);
let eq = tc.fresh();
tail.push(Instruction::new(
Op::IEqual,
Some(bool_ty),
Some(eq),
vec![Operand::IdRef(selector), Operand::IdRef(lit_const)],
));
let picked = case_const[label_index[lbl]];
let nxt = tc.fresh();
tail.push(Instruction::new(
Op::Select,
Some(i32_ty),
Some(nxt),
vec![
Operand::IdRef(eq),
Operand::IdRef(picked),
Operand::IdRef(cur),
],
));
cur = nxt;
}
tail.push(store_state(state_var, cur));
tail.push(branch(exit_target));
}
Term::Return => tail.push(Instruction::new(Op::Return, None, None, vec![])),
Term::ReturnValue(v) => {
let rv = resolve(tc, &mut prelude, &mut local_load, &demo, bi, *v);
tail.push(Instruction::new(
Op::ReturnValue,
None,
None,
vec![Operand::IdRef(rv)],
));
}
Term::Unreachable => tail.push(Instruction::new(Op::Unreachable, None, None, vec![])),
Term::Kill(inst) => tail.push(inst.clone()),
}
let mut instructions = Vec::new();
instructions.extend(prelude);
instructions.extend(body);
instructions.extend(tail);
if bi == 0 {
entry_processed = instructions;
} else {
case_blocks.push(Block {
label: Some(Instruction::new(Op::Label, None, Some(this_label), vec![])),
instructions,
});
}
}
let mut entry_insts: Vec<Instruction> = Vec::new();
entry_insts.extend(variables.iter().cloned());
entry_insts.extend(spill_vars);
entry_insts.extend(entry_processed);
let entry_block = Block {
label: Some(Instruction::new(Op::Label, None, Some(new_entry), vec![])),
instructions: entry_insts,
};
let header_block = Block {
label: Some(Instruction::new(Op::Label, None, Some(loop_header), vec![])),
instructions: vec![
Instruction::new(
Op::LoopMerge,
None,
None,
vec![
Operand::IdRef(loop_merge),
Operand::IdRef(loop_continue),
Operand::LoopControl(spirv::LoopControl::NONE),
],
),
Instruction::new(Op::Branch, None, None, vec![Operand::IdRef(dispatch)]),
],
};
let state_load = tc.fresh();
let mut switch_ops = vec![
Operand::IdRef(state_load),
Operand::IdRef(switch_default_break),
];
for (i, lbl) in labels.iter().enumerate().skip(1) {
switch_ops.push(Operand::LiteralBit32(i as u32));
switch_ops.push(Operand::IdRef(*lbl));
}
let dispatch_block = Block {
label: Some(Instruction::new(Op::Label, None, Some(dispatch), vec![])),
instructions: vec![
Instruction::new(
Op::Load,
Some(i32_ty),
Some(state_load),
vec![Operand::IdRef(state_var)],
),
Instruction::new(
Op::SelectionMerge,
None,
None,
vec![
Operand::IdRef(sel_merge),
Operand::SelectionControl(spirv::SelectionControl::NONE),
],
),
Instruction::new(Op::Switch, None, None, switch_ops),
],
};
let switch_default_block = Block {
label: Some(Instruction::new(
Op::Label,
None,
Some(switch_default_break),
vec![],
)),
instructions: vec![Instruction::new(
Op::Branch,
None,
None,
vec![Operand::IdRef(loop_merge)],
)],
};
let sel_merge_block = Block {
label: Some(Instruction::new(Op::Label, None, Some(sel_merge), vec![])),
instructions: vec![Instruction::new(
Op::Branch,
None,
None,
vec![Operand::IdRef(loop_continue)],
)],
};
let continue_block = Block {
label: Some(Instruction::new(
Op::Label,
None,
Some(loop_continue),
vec![],
)),
instructions: vec![Instruction::new(
Op::Branch,
None,
None,
vec![Operand::IdRef(loop_header)],
)],
};
let merge_terminal = if function
.def
.as_ref()
.and_then(|def| def.result_type)
.is_some_and(|ty| tc.type_opcode(ty) == Some(Op::TypeVoid))
{
Instruction::new(Op::Return, None, None, vec![])
} else {
Instruction::new(Op::Unreachable, None, None, vec![])
};
let merge_block = Block {
label: Some(Instruction::new(Op::Label, None, Some(loop_merge), vec![])),
instructions: vec![merge_terminal],
};
let mut new_blocks = Vec::with_capacity(case_blocks.len() + 7);
new_blocks.push(entry_block);
new_blocks.push(header_block);
new_blocks.push(dispatch_block);
new_blocks.push(switch_default_block);
new_blocks.extend(case_blocks);
new_blocks.push(sel_merge_block);
new_blocks.push(continue_block);
new_blocks.push(merge_block);
function.blocks = new_blocks;
true
}
struct Demo<'a> {
spill: &'a HashMap<Word, (Word, Word)>,
demote: &'a HashSet<Word>,
def_block: &'a HashMap<Word, usize>,
phi: &'a HashSet<Word>,
remat: &'a HashMap<Word, Instruction>,
remat_phi: &'a HashMap<Word, (Word, Vec<(Word, Word)>)>,
phi_tag: &'a HashMap<Word, Word>,
remat_phi_invariant: &'a HashSet<Word>,
}
fn store_state(state_var: Word, value: Word) -> Instruction {
Instruction::new(
Op::Store,
None,
None,
vec![Operand::IdRef(state_var), Operand::IdRef(value)],
)
}
fn branch(target: Word) -> Instruction {
Instruction::new(Op::Branch, None, None, vec![Operand::IdRef(target)])
}
fn load_demoted(
tc: &mut TypeCtx,
prelude: &mut Vec<Instruction>,
local_load: &mut HashMap<Word, Word>,
spill: &HashMap<Word, (Word, Word)>,
v: Word,
) -> Word {
if let Some(&l) = local_load.get(&v) {
return l;
}
let (var, ty) = spill[&v];
let l = tc.fresh();
prelude.push(Instruction::new(
Op::Load,
Some(ty),
Some(l),
vec![Operand::IdRef(var)],
));
local_load.insert(v, l);
l
}
fn resolve(
tc: &mut TypeCtx,
prelude: &mut Vec<Instruction>,
local_load: &mut HashMap<Word, Word>,
demo: &Demo,
bi: usize,
v: Word,
) -> Word {
if demo.demote.contains(&v) && (demo.phi.contains(&v) || demo.def_block.get(&v) != Some(&bi)) {
load_demoted(tc, prelude, local_load, demo.spill, v)
} else {
v
}
}
fn rematerialize(
tc: &mut TypeCtx,
prelude: &mut Vec<Instruction>,
local_remat: &mut HashMap<Word, Word>,
local_load: &mut HashMap<Word, Word>,
demo: &Demo,
bi: usize,
v: Word,
) -> Word {
if let Some(&r) = local_remat.get(&v) {
return r;
}
if let Some((pty, incoming)) = demo.remat_phi.get(&v) {
if demo.remat_phi_invariant.contains(&v) {
let first = incoming.first().expect("phi has >=1 incoming").0;
let r = if demo.remat.contains_key(&first) || demo.remat_phi.contains_key(&first) {
rematerialize(tc, prelude, local_remat, local_load, demo, bi, first)
} else {
resolve(tc, prelude, local_load, demo, bi, first)
};
local_remat.insert(v, r);
return r;
}
let i32_ty = tc.i32_ty();
let bool_ty = tc.bool_ty();
let tag_var = demo.phi_tag[&v];
let tag = tc.fresh();
prelude.push(Instruction::new(
Op::Load,
Some(i32_ty),
Some(tag),
vec![Operand::IdRef(tag_var)],
));
let arm = |tc: &mut TypeCtx,
prelude: &mut Vec<Instruction>,
local_remat: &mut HashMap<Word, Word>,
local_load: &mut HashMap<Word, Word>,
id: Word|
-> Word {
if demo.remat.contains_key(&id) || demo.remat_phi.contains_key(&id) {
rematerialize(tc, prelude, local_remat, local_load, demo, bi, id)
} else {
resolve(tc, prelude, local_load, demo, bi, id)
}
};
let last = incoming.last().expect("phi has at least one incoming").0;
let mut acc = arm(tc, prelude, local_remat, local_load, last);
for (i, (val, _)) in incoming.iter().enumerate().rev().skip(1) {
let armv = arm(tc, prelude, local_remat, local_load, *val);
let tagc = tc.int_const(i32_ty, i as u64);
let cmp = tc.fresh();
prelude.push(Instruction::new(
Op::IEqual,
Some(bool_ty),
Some(cmp),
vec![Operand::IdRef(tag), Operand::IdRef(tagc)],
));
let sel = tc.fresh();
prelude.push(Instruction::new(
Op::Select,
Some(*pty),
Some(sel),
vec![
Operand::IdRef(cmp),
Operand::IdRef(armv),
Operand::IdRef(acc),
],
));
acc = sel;
}
local_remat.insert(v, acc);
return acc;
}
let inst = demo.remat[&v].clone();
let mut new_ops = Vec::with_capacity(inst.operands.len());
for op in inst.operands.iter() {
match op {
Operand::IdRef(id) => {
let r = if demo.remat.contains_key(id) {
rematerialize(tc, prelude, local_remat, local_load, demo, bi, *id)
} else {
resolve(tc, prelude, local_load, demo, bi, *id)
};
new_ops.push(Operand::IdRef(r));
}
_ => new_ops.push(op.clone()),
}
}
let result = tc.fresh();
prelude.push(Instruction::new(
inst.class.opcode,
inst.result_type,
Some(result),
new_ops,
));
local_remat.insert(v, result);
result
}
#[allow(clippy::too_many_arguments)]
fn store_phi_edges(
tc: &mut TypeCtx,
prelude: &mut Vec<Instruction>,
tail: &mut Vec<Instruction>,
local_load: &mut HashMap<Word, Word>,
demo: &Demo,
bi: usize,
this_label: Word,
target: Word,
label_index: &HashMap<Word, usize>,
block_phis: &[Vec<Word>],
phis: &HashMap<Word, (Word, Vec<(Word, Word)>)>,
) {
let Some(&ti) = label_index.get(&target) else {
return;
};
for &rid in &block_phis[ti] {
let (_, incoming) = &phis[&rid];
if let Some(&tag_var) = demo.phi_tag.get(&rid) {
for (idx, (_, pred)) in incoming.iter().enumerate() {
if *pred == this_label {
let i32_ty = tc.i32_ty();
let tagc = tc.int_const(i32_ty, idx as u64);
tail.push(Instruction::new(
Op::Store,
None,
None,
vec![Operand::IdRef(tag_var), Operand::IdRef(tagc)],
));
}
}
continue;
}
for (val, pred) in incoming {
if *pred == this_label {
let (var, _) = demo.spill[&rid];
let rv = resolve(tc, prelude, local_load, demo, bi, *val);
tail.push(Instruction::new(
Op::Store,
None,
None,
vec![Operand::IdRef(var), Operand::IdRef(rv)],
));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn scratch() -> std::path::PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"metal2vulkan_relooper_{}_{}",
std::process::id(),
n
));
let _ = std::fs::create_dir_all(&dir);
dir
}
fn assemble(spvasm: &str) -> Option<Vec<u8>> {
if std::process::Command::new("spirv-as")
.arg("--version")
.output()
.is_err()
{
return None;
}
let dir = scratch();
let src = dir.join("in.spvasm");
let out = dir.join("in.spv");
std::fs::write(&src, spvasm).unwrap();
let st = std::process::Command::new("spirv-as")
.args(["--target-env", "vulkan1.3"])
.arg(&src)
.arg("-o")
.arg(&out)
.output()
.unwrap();
assert!(
st.status.success(),
"spirv-as: {}",
String::from_utf8_lossy(&st.stderr)
);
Some(std::fs::read(&out).unwrap())
}
fn validates(spv: &[u8]) -> bool {
let dir = scratch();
let p = dir.join("m.spv");
std::fs::write(&p, spv).unwrap();
let st = std::process::Command::new("spirv-val")
.args(["--target-env", "vulkan1.3"])
.arg(&p)
.output()
.unwrap();
if !st.status.success() {
eprintln!("spirv-val: {}", String::from_utf8_lossy(&st.stderr));
}
st.status.success()
}
fn relooper_bytes(spv: &[u8]) -> Vec<u8> {
let mut module = crate::spirv_module::load_bytes(spv).expect("load");
assert!(
rewrite_to_relooper(&mut module, MAX_RELOOPER_BLOCKS),
"expected a rewrite"
);
module
.assemble()
.iter()
.flat_map(|w| w.to_le_bytes())
.collect()
}
fn block_id(block: &Block) -> Word {
block
.label
.as_ref()
.and_then(|label| label.result_id)
.expect("block label id")
}
fn id_ref(operand: &Operand) -> Word {
let Operand::IdRef(id) = operand else {
panic!("expected IdRef operand, got {operand:?}");
};
*id
}
fn single_loop_dispatch_default(spv: &[u8]) -> (Word, Word, Word, Op) {
let module = crate::spirv_module::load_bytes(spv).expect("load relooped module");
let function = module
.functions
.iter()
.find(|function| {
function.blocks.iter().any(|block| {
block
.instructions
.iter()
.any(|instruction| instruction.class.opcode == Op::LoopMerge)
})
})
.expect("function with a relooped dispatcher");
let loop_header = function
.blocks
.iter()
.find(|block| {
block
.instructions
.iter()
.any(|instruction| instruction.class.opcode == Op::LoopMerge)
})
.expect("loop header");
let loop_merge = loop_header
.instructions
.iter()
.find(|instruction| instruction.class.opcode == Op::LoopMerge)
.and_then(|instruction| instruction.operands.first())
.map(id_ref)
.expect("loop merge target");
let dispatch = loop_header
.instructions
.iter()
.find(|instruction| instruction.class.opcode == Op::Branch)
.and_then(|instruction| instruction.operands.first())
.map(id_ref)
.expect("dispatch branch");
let dispatch_block = function
.blocks
.iter()
.find(|block| block_id(block) == dispatch)
.expect("dispatch block");
let switch_default = dispatch_block
.instructions
.iter()
.find(|instruction| instruction.class.opcode == Op::Switch)
.and_then(|instruction| instruction.operands.get(1))
.map(id_ref)
.expect("switch default target");
let default_block = function
.blocks
.iter()
.find(|block| block_id(block) == switch_default)
.expect("switch default block");
let default_branch = default_block
.instructions
.iter()
.find(|instruction| instruction.class.opcode == Op::Branch)
.and_then(|instruction| instruction.operands.first())
.map(id_ref)
.expect("default block branch");
let merge_terminal = function
.blocks
.iter()
.find(|block| block_id(block) == loop_merge)
.and_then(|block| block.instructions.last())
.map(|instruction| instruction.class.opcode)
.expect("loop merge terminal");
(loop_merge, switch_default, default_branch, merge_terminal)
}
#[test]
fn relooper_bails_instead_of_panicking_on_missing_target() {
let mut module = Module::new();
let mut function = Function::new();
function.blocks = vec![
Block {
label: Some(Instruction::new(Op::Label, None, Some(1), vec![])),
instructions: vec![Instruction::new(
Op::Branch,
None,
None,
vec![Operand::IdRef(2)],
)],
},
Block {
label: Some(Instruction::new(Op::Label, None, Some(2), vec![])),
instructions: vec![Instruction::new(
Op::Branch,
None,
None,
vec![Operand::IdRef(99)],
)],
},
];
module.functions.push(function);
assert!(
!rewrite_to_relooper(&mut module, MAX_RELOOPER_BLOCKS),
"missing branch target should decline, not panic"
);
}
#[test]
fn relooper_dispatch_default_breaks_to_loop_merge() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%bool = OpTypeBool
%true = OpConstantTrue %bool
%main = OpFunction %void None %fn
%entry = OpLabel
OpBranch %head
%head = OpLabel
OpLoopMerge %merge %cont None
OpBranchConditional %true %body %merge
%body = OpLabel
OpBranch %cont
%cont = OpLabel
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(validates(&out), "relooper output must validate");
let (loop_merge, switch_default, default_branch, merge_terminal) =
single_loop_dispatch_default(&out);
assert_ne!(
switch_default, loop_merge,
"SPIR-V requires a dominated switch default block, not a direct merge target"
);
assert_eq!(
default_branch, loop_merge,
"relooper switch default block must statically break to the loop merge"
);
assert_eq!(
merge_terminal,
Op::Return,
"void-function dispatch merge represents normal function exit"
);
}
#[test]
fn relooper_loop_with_phi_validates() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u10 = OpConstant %uint 10
%ptr_u = OpTypePointer Function %uint
%main = OpFunction %void None %fn
%entry = OpLabel
%acc = OpVariable %ptr_u Function
OpStore %acc %u0
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %body
%cmp = OpULessThan %bool %i %u10
OpLoopMerge %merge %body None
OpBranchConditional %cmp %body %merge
%body = OpLabel
%inext = OpIAdd %uint %i %u1
OpStore %acc %inext
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(validates(&out), "relooper output must validate");
}
#[test]
fn relooper_switch_in_loop_validates() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u2 = OpConstant %uint 2
%u5 = OpConstant %uint 5
%ptr_u = OpTypePointer Function %uint
%main = OpFunction %void None %fn
%entry = OpLabel
%acc = OpVariable %ptr_u Function
OpStore %acc %u0
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %cont
%cmp = OpULessThan %bool %i %u5
OpLoopMerge %merge %cont None
OpBranchConditional %cmp %sw %merge
%sw = OpLabel
%dbl = OpIAdd %uint %i %i
OpSelectionMerge %swm None
OpSwitch %i %def 0 %c0 1 %c1
%c0 = OpLabel
OpStore %acc %dbl
OpBranch %swm
%c1 = OpLabel
OpStore %acc %i
OpBranch %swm
%def = OpLabel
OpStore %acc %u2
OpBranch %swm
%swm = OpLabel
OpBranch %cont
%cont = OpLabel
%inext = OpIAdd %uint %i %u1
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(validates(&out), "relooper output must validate");
}
#[test]
fn relooper_entry_pointer_used_in_loop_validates() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u10 = OpConstant %uint 10
%ptr_u = OpTypePointer Function %uint
%main = OpFunction %void None %fn
%entry = OpLabel
%acc = OpVariable %ptr_u Function
%p = OpAccessChain %ptr_u %acc
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %body
%cmp = OpULessThan %bool %i %u10
OpLoopMerge %merge %body None
OpBranchConditional %cmp %body %merge
%body = OpLabel
OpStore %p %i
%inext = OpIAdd %uint %i %u1
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(validates(&out), "relooper output must validate");
}
#[test]
fn relooper_rematerializes_nonentry_pointer_access_chain() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u10 = OpConstant %uint 10
%u4 = OpConstant %uint 4
%ptr_u = OpTypePointer Function %uint
%arr_t = OpTypeArray %uint %u4
%ptr_arr = OpTypePointer Function %arr_t
%main = OpFunction %void None %fn
%entry = OpLabel
%arr = OpVariable %ptr_arr Function
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %use
%cmp = OpULessThan %bool %i %u10
OpLoopMerge %merge %use None
OpBranchConditional %cmp %body %merge
%body = OpLabel
%p = OpAccessChain %ptr_u %arr %u0
OpBranch %use
%use = OpLabel
OpStore %p %i
%inext = OpIAdd %uint %i %u1
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(
validates(&out),
"relooper output must validate (rematerialized pointer)"
);
}
#[test]
fn relooper_rematerializes_pointer_select_and_phi() {
let spvasm = r#"
OpCapability Shader
OpCapability VariablePointersStorageBuffer
OpExtension "SPV_KHR_variable_pointers"
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main" %buf
OpExecutionMode %main LocalSize 1 1 1
OpDecorate %arr_t ArrayStride 4
OpMemberDecorate %buf_t 0 Offset 0
OpDecorate %buf_t Block
OpDecorate %buf DescriptorSet 0
OpDecorate %buf Binding 0
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u2 = OpConstant %uint 2
%u4 = OpConstant %uint 4
%u10 = OpConstant %uint 10
%arr_t = OpTypeArray %uint %u4
%buf_t = OpTypeStruct %arr_t
%ptr_buf = OpTypePointer StorageBuffer %buf_t
%buf = OpVariable %ptr_buf StorageBuffer
%ptr_u = OpTypePointer StorageBuffer %uint
%main = OpFunction %void None %fn
%entry = OpLabel
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %cont
%cmp = OpULessThan %bool %i %u10
OpLoopMerge %merge %cont None
OpBranchConditional %cmp %body %merge
%body = OpLabel
%c = OpULessThan %bool %i %u2
%pa = OpAccessChain %ptr_u %buf %u0 %u0
%pb = OpAccessChain %ptr_u %buf %u0 %u1
%psel = OpSelect %ptr_u %c %pa %pb
OpSelectionMerge %join None
OpBranchConditional %c %t %f
%t = OpLabel
%pt = OpAccessChain %ptr_u %buf %u0 %u2
OpBranch %join
%f = OpLabel
OpBranch %join
%join = OpLabel
%p = OpPhi %ptr_u %pt %t %psel %f
OpStore %p %i
OpBranch %cont
%cont = OpLabel
%inext = OpIAdd %uint %i %u1
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(
validates(&out),
"relooper output must validate (rematerialized pointer select + phi)"
);
}
#[test]
fn relooper_rematerializes_pointer_copy_object() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u10 = OpConstant %uint 10
%u4 = OpConstant %uint 4
%ptr_u = OpTypePointer Function %uint
%arr_t = OpTypeArray %uint %u4
%ptr_arr = OpTypePointer Function %arr_t
%main = OpFunction %void None %fn
%entry = OpLabel
%arr = OpVariable %ptr_arr Function
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %use
%cmp = OpULessThan %bool %i %u10
OpLoopMerge %merge %use None
OpBranchConditional %cmp %body %merge
%body = OpLabel
%p = OpAccessChain %ptr_u %arr %u0
%pc = OpCopyObject %ptr_u %p
OpBranch %use
%use = OpLabel
OpStore %pc %i
%inext = OpIAdd %uint %i %u1
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
assert!(validates(&spv), "input must validate");
let out = relooper_bytes(&spv);
assert!(
validates(&out),
"relooper output must validate (rematerialized pointer CopyObject)"
);
}
#[test]
fn relooper_rematerializes_invariant_function_pointer_phi() {
let spvasm = r#"
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %main "main"
OpExecutionMode %main LocalSize 1 1 1
%void = OpTypeVoid
%fn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%bool = OpTypeBool
%u0 = OpConstant %uint 0
%u1 = OpConstant %uint 1
%u2 = OpConstant %uint 2
%u4 = OpConstant %uint 4
%u10 = OpConstant %uint 10
%arr_t = OpTypeArray %uint %u4
%ptr_arr = OpTypePointer Function %arr_t
%ptr_u = OpTypePointer Function %uint
%main = OpFunction %void None %fn
%entry = OpLabel
%arr = OpVariable %ptr_arr Function
OpBranch %head
%head = OpLabel
%i = OpPhi %uint %u0 %entry %inext %cont
%cmp = OpULessThan %bool %i %u10
OpLoopMerge %merge %cont None
OpBranchConditional %cmp %body %merge
%body = OpLabel
%c = OpULessThan %bool %i %u2
OpSelectionMerge %join None
OpBranchConditional %c %t %f
%t = OpLabel
%pt = OpAccessChain %ptr_u %arr %u2
OpBranch %join
%f = OpLabel
%pf = OpAccessChain %ptr_u %arr %u2
OpBranch %join
%join = OpLabel
%p = OpPhi %ptr_u %pt %t %pf %f
OpStore %p %i
OpBranch %cont
%cont = OpLabel
%inext = OpIAdd %uint %i %u1
OpBranch %head
%merge = OpLabel
OpReturn
OpFunctionEnd
"#;
let Some(spv) = assemble(spvasm) else { return };
let out = relooper_bytes(&spv);
assert!(
validates(&out),
"relooper output must validate (invariant Function pointer phi rematerialized without a select)"
);
}
}