Skip to content

Commit 77b4e7a

Browse files
committed
Fold OpLoad from Private variables with constant initializers
Disclosure: I had AI write this and I only superficially understand the code. Replace OpLoad from Private/Function storage class variables with their constant initializers. This eliminates pointer-to-pointer patterns like `&&123` which would otherwise generate invalid SPIR-V in Logical addressing mode (pointer-to-pointer is only allowed for StorageBuffer/Workgroup, not Private). The optimization runs after inlining (which may expose such patterns) and before DCE (so the now-unused variables can be removed). Also extend DCE to treat Private storage class variables as pure, allowing them to be removed when unused. Newer versions of spirv-val fire catch this.
1 parent 5e1fe97 commit 77b4e7a

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

crates/rustc_codegen_spirv/src/linker/dce.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,13 @@ fn instruction_is_pure(inst: &Instruction) -> bool {
282282
| PtrEqual
283283
| PtrNotEqual
284284
| PtrDiff => true,
285-
Variable => inst.operands.first() == Some(&Operand::StorageClass(StorageClass::Function)),
285+
// Variables with Function or Private storage class are pure and can be DCE'd if unused.
286+
// Other storage classes (Input, Output, Uniform, etc.) are part of the shader interface
287+
// and must be kept.
288+
Variable => matches!(
289+
inst.operands.first(),
290+
Some(&Operand::StorageClass(StorageClass::Function | StorageClass::Private))
291+
),
286292
_ => false,
287293
}
288294
}

crates/rustc_codegen_spirv/src/linker/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,16 @@ pub fn link(
415415
inline::inline(sess, &mut output)?;
416416
}
417417

418+
// Fold OpLoad from Private/Function variables with constant initializers.
419+
// This must run after inlining (which may expose such patterns) and before DCE
420+
// (so that the now-unused variables can be removed).
421+
// This is critical for pointer-to-pointer patterns like `&&123` which would
422+
// otherwise generate invalid SPIR-V in Logical addressing mode.
423+
{
424+
let _timer = sess.timer("link_fold_load_from_constant_variable");
425+
peephole_opts::fold_load_from_constant_variable(&mut output);
426+
}
427+
418428
{
419429
let _timer = sess.timer("link_dce-after-inlining");
420430
dce::dce(&mut output);

crates/rustc_codegen_spirv/src/linker/peephole_opts.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,3 +606,76 @@ pub fn bool_fusion(
606606
}
607607
super::apply_rewrite_rules(&rewrite_rules, &mut function.blocks);
608608
}
609+
610+
/// Fold `OpLoad` from Private/Function storage class variables that have constant initializers.
611+
///
612+
/// This optimization handles patterns like:
613+
/// ```text
614+
/// %ptr = OpVariable %_ptr_Private_T Private %initializer
615+
/// %val = OpLoad %T %ptr
616+
/// ```
617+
/// After this optimization, uses of `%val` are replaced with `%initializer`.
618+
///
619+
/// This is particularly important for pointer-to-pointer constants (e.g., `&&123`)
620+
/// where the outer pointer variable has a known initializer (the inner pointer).
621+
/// Without this optimization, such patterns generate invalid SPIR-V in Logical
622+
/// addressing mode (pointer-to-pointer with Private storage class is not allowed).
623+
pub fn fold_load_from_constant_variable(
624+
module: &mut Module,
625+
) {
626+
use rspirv::spirv::StorageClass;
627+
628+
// Build a map of variable ID -> initializer ID for Private/Function variables
629+
// that have constant initializers.
630+
let var_initializers: FxHashMap<Word, Word> = module
631+
.types_global_values
632+
.iter()
633+
.filter_map(|inst| {
634+
if inst.class.opcode != Op::Variable {
635+
return None;
636+
}
637+
// Check storage class - only Private and Function can be folded
638+
let storage_class = inst.operands.first()?.unwrap_storage_class();
639+
if !matches!(storage_class, StorageClass::Private | StorageClass::Function) {
640+
return None;
641+
}
642+
// Check for initializer (second operand after storage class)
643+
let initializer = inst.operands.get(1)?.id_ref_any()?;
644+
Some((inst.result_id?, initializer))
645+
})
646+
.collect();
647+
648+
if var_initializers.is_empty() {
649+
return;
650+
}
651+
652+
// Rewrite OpLoad instructions that load from variables with known initializers
653+
let mut rewrite_rules: FxHashMap<Word, Word> = FxHashMap::default();
654+
655+
for func in &mut module.functions {
656+
for block in &mut func.blocks {
657+
for inst in &mut block.instructions {
658+
if inst.class.opcode != Op::Load {
659+
continue;
660+
}
661+
// OpLoad has pointer as first operand
662+
let ptr_id = inst.operands[0].unwrap_id_ref();
663+
if let Some(&initializer) = var_initializers.get(&ptr_id) {
664+
// This load can be replaced with the initializer value
665+
if let Some(result_id) = inst.result_id {
666+
rewrite_rules.insert(result_id, initializer);
667+
// Turn this instruction into a Nop
668+
*inst = Instruction::new(Op::Nop, None, None, Vec::new());
669+
}
670+
}
671+
}
672+
}
673+
}
674+
675+
if !rewrite_rules.is_empty() {
676+
// Apply rewrite rules to all functions
677+
for func in &mut module.functions {
678+
super::apply_rewrite_rules(&rewrite_rules, &mut func.blocks);
679+
}
680+
}
681+
}

0 commit comments

Comments
 (0)