#include "graphics/shader/recompiler/ir/passes/ConstantPropagation.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include #include #include #include namespace Libs::Graphics::ShaderRecompiler::IR { namespace { Value Arg(const Inst& inst, size_t index) { return inst.Arg(index).Resolve(); } bool IsImmediate(Value value, Type type) { return value.IsImmediate() && value.GetType() == type; } void Replace(Inst& inst, Value value) { inst.ReplaceUsesWith(value.Resolve()); } template bool FoldU32(Inst& inst, Function function) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (!IsImmediate(lhs, Type::U32) || !IsImmediate(rhs, Type::U32)) { return false; } Replace(inst, Value(static_cast(function(lhs.U32(), rhs.U32())))); return true; } template bool FoldU64(Inst& inst, Function function) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (!IsImmediate(lhs, Type::U64) || !IsImmediate(rhs, Type::U64)) { return false; } Replace(inst, Value(static_cast(function(lhs.U64(), rhs.U64())))); return true; } template bool FoldU64Shift(Inst& inst, Function function) { const auto value = Arg(inst, 0); const auto shift = Arg(inst, 1); if (!IsImmediate(value, Type::U64) || !IsImmediate(shift, Type::U32)) { return false; } Replace(inst, Value(static_cast(function(value.U64(), shift.U32())))); return true; } template bool FoldU32Compare(Inst& inst, Function function) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (!IsImmediate(lhs, Type::U32) || !IsImmediate(rhs, Type::U32)) { return false; } Replace(inst, Value(function(lhs.U32(), rhs.U32()))); return true; } template bool FoldU64Compare(Inst& inst, Function function) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (!IsImmediate(lhs, Type::U64) || !IsImmediate(rhs, Type::U64)) { return false; } Replace(inst, Value(function(lhs.U64(), rhs.U64()))); return true; } template bool FoldLogical(Inst& inst, Function function) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (!IsImmediate(lhs, Type::U1) || !IsImmediate(rhs, Type::U1)) { return false; } Replace(inst, Value(function(lhs.U1(), rhs.U1()))); return true; } bool ReplaceBinaryIdentity(Inst& inst, Type type, uint64_t identity) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (IsImmediate(lhs, type) && (type == Type::U32 ? lhs.U32() == identity : lhs.U64() == identity)) { Replace(inst, rhs); return true; } if (IsImmediate(rhs, type) && (type == Type::U32 ? rhs.U32() == identity : rhs.U64() == identity)) { Replace(inst, lhs); return true; } return false; } bool FoldSelect(Inst& inst) { const auto condition = Arg(inst, 0); const auto true_value = Arg(inst, 1); const auto false_value = Arg(inst, 2); if (IsImmediate(condition, Type::U1)) { Replace(inst, condition.U1() ? true_value : false_value); return true; } if (true_value == false_value) { Replace(inst, true_value); return true; } return false; } bool FoldPhi(Inst& inst) { Value same; for (size_t index = 0; index < inst.NumArgs(); index++) { const auto value = Arg(inst, index); if (value.TryInstruction() == &inst) { continue; } if (same.IsEmpty()) { same = value; } else if (same != value) { return false; } } if (same.IsEmpty()) { return false; } Replace(inst, same); return true; } bool FoldBitCast(Inst& inst, ValueOpcode reverse) { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::F32) && inst.GetOpcode() == ValueOpcode::BitCastU32F32) { Replace(inst, Value(std::bit_cast(value.F32Value()))); return true; } if (IsImmediate(value, Type::U32) && inst.GetOpcode() == ValueOpcode::BitCastF32U32) { Replace(inst, Value::F32(std::bit_cast(value.U32()))); return true; } if (IsImmediate(value, Type::F16) && inst.GetOpcode() == ValueOpcode::BitCastU16F16) { Replace(inst, Value(value.F16Bits())); return true; } if (IsImmediate(value, Type::U16) && inst.GetOpcode() == ValueOpcode::BitCastF16U16) { Replace(inst, Value::F16(value.U16())); return true; } if (auto* producer = value.TryInstruction(); producer != nullptr && producer->GetOpcode() == reverse) { Replace(inst, producer->Arg(0)); return true; } return false; } bool FoldCompositeExtract(Inst& inst, ValueOpcode construct, size_t components) { const auto composite = Arg(inst, 0); const auto index = Arg(inst, 1); if (!IsImmediate(index, Type::U32) || index.U32() >= components) { return false; } const auto component = index.U32(); const auto* producer = composite.TryInstruction(); if (IsImmediate(composite, Type::U64)) { Replace(inst, Value(static_cast(composite.U64() >> (component * 32u)))); return true; } if (producer != nullptr && producer->GetOpcode() == construct) { Replace(inst, producer->Arg(component)); return true; } if (component < 2u && producer != nullptr && producer->GetOpcode() == ValueOpcode::IAddCarry32) { const auto lhs = producer->Arg(0).Resolve(); const auto rhs = producer->Arg(1).Resolve(); if (IsImmediate(lhs, Type::U32) && IsImmediate(rhs, Type::U32)) { const auto sum = static_cast(lhs.U32()) + rhs.U32(); Replace(inst, Value(component == 0u ? static_cast(sum) : static_cast(sum >> 32u))); return true; } } return false; } void FoldInstruction(Block& block, Block::iterator instruction, std::unordered_set& lowered_ancillary) { auto& inst = *instruction; switch (inst.GetOpcode()) { case ValueOpcode::Phi: FoldPhi(inst); return; case ValueOpcode::SelectU1: case ValueOpcode::SelectU32: case ValueOpcode::SelectF32: FoldSelect(inst); return; case ValueOpcode::BitFieldInsert: { const auto base = Arg(inst, 0); const auto insert = Arg(inst, 1); const auto offset = Arg(inst, 2); const auto count = Arg(inst, 3); if (IsImmediate(base, Type::U32) && IsImmediate(insert, Type::U32) && IsImmediate(offset, Type::U32) && IsImmediate(count, Type::U32) && offset.U32() <= 32u && count.U32() <= 32u - offset.U32()) { if (count.U32() == 0u) { Replace(inst, base); return; } const auto mask = count.U32() == 32u ? UINT32_MAX : ((uint32_t {1} << count.U32()) - 1u) << offset.U32(); Replace(inst, Value((base.U32() & ~mask) | ((insert.U32() << offset.U32()) & mask))); } return; } case ValueOpcode::BitFieldUExtract: case ValueOpcode::BitFieldSExtract: { const auto value = Arg(inst, 0); const auto offset = Arg(inst, 1); const auto count = Arg(inst, 2); auto* source = value.TryInstruction(); if (source != nullptr && source->GetOpcode() == ValueOpcode::ShiftLeftLogical32 && IsImmediate(offset, Type::U32) && IsImmediate(count, Type::U32)) { const auto shift = Arg(*source, 1); if (IsImmediate(shift, Type::U32) && shift.U32() < 32u && offset.U32() <= shift.U32() && count.U32() <= shift.U32() - offset.U32()) { Replace(inst, Value(0u)); return; } } if (source != nullptr && source->GetOpcode() == ValueOpcode::GetBuiltin && source->Arg(0) == Value(static_cast(StageInputKind::PackedAncillary)) && IsImmediate(offset, Type::U32) && IsImmediate(count, Type::U32) && count.U32() != 0u) { constexpr struct { uint32_t start; uint32_t end; StageInputKind kind; } fields[] = {{8u, 12u, StageInputKind::SampleId}, {16u, 27u, StageInputKind::Layer}}; for (const auto& field: fields) { if (offset.U32() >= field.start && offset.U32() < field.end && count.U32() <= field.end - offset.U32()) { // Preserve extraction and sign extension while exposing only the used field. const auto input = block.PrependNewInst( instruction, ValueOpcode::GetBuiltin, {Value(static_cast(field.kind)), Value(0u)}); inst.SetArg(0, Value(&*input)); inst.SetArg(1, Value(offset.U32() - field.start)); lowered_ancillary.insert(source); return; } } } if (!IsImmediate(value, Type::U32) || !IsImmediate(offset, Type::U32) || !IsImmediate(count, Type::U32) || offset.U32() > 32u || count.U32() > 32u - offset.U32()) { return; } if (count.U32() == 0u) { Replace(inst, Value(0u)); } else if (inst.GetOpcode() == ValueOpcode::BitFieldUExtract) { const auto mask = count.U32() == 32u ? UINT32_MAX : (uint32_t {1} << count.U32()) - 1u; Replace(inst, Value((value.U32() >> offset.U32()) & mask)); } else { const auto left = 32u - offset.U32() - count.U32(); const auto bits = value.U32() << left; Replace(inst, Value(static_cast(std::bit_cast(bits) >> (left + offset.U32())))); } return; } case ValueOpcode::BitCastU16F16: FoldBitCast(inst, ValueOpcode::BitCastF16U16); return; case ValueOpcode::BitCastF16U16: FoldBitCast(inst, ValueOpcode::BitCastU16F16); return; case ValueOpcode::BitCastU32F32: FoldBitCast(inst, ValueOpcode::BitCastF32U32); return; case ValueOpcode::BitCastF32U32: FoldBitCast(inst, ValueOpcode::BitCastU32F32); return; case ValueOpcode::ConvertU16U32: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U32)) { Replace(inst, Value(static_cast(value.U32()))); } return; } case ValueOpcode::ConvertU32U16: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U16)) { Replace(inst, Value(static_cast(value.U16()))); } else if (auto* producer = value.TryInstruction(); producer != nullptr && producer->GetOpcode() == ValueOpcode::ConvertU16U32) { Replace(inst, producer->Arg(0)); } return; } case ValueOpcode::ConvertU8U32: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U32)) { Replace(inst, Value(static_cast(value.U32()))); } return; } case ValueOpcode::ConvertU32U8: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U8)) { Replace(inst, Value(static_cast(value.U8()))); } else if (auto* producer = value.TryInstruction(); producer != nullptr && producer->GetOpcode() == ValueOpcode::ConvertU8U32) { Replace(inst, producer->Arg(0)); } return; } case ValueOpcode::CompositeExtractU64: FoldCompositeExtract(inst, ValueOpcode::CompositeConstructU64, 2); return; case ValueOpcode::CompositeExtractU32x2: FoldCompositeExtract(inst, ValueOpcode::CompositeConstructU32x2, 2); return; case ValueOpcode::CompositeExtractU32x3: FoldCompositeExtract(inst, ValueOpcode::CompositeConstructU32x3, 3); return; case ValueOpcode::CompositeExtractU32x4: FoldCompositeExtract(inst, ValueOpcode::CompositeConstructU32x4, 4); return; case ValueOpcode::CompositeConstructU64: { const auto low = Arg(inst, 0); const auto high = Arg(inst, 1); if (IsImmediate(low, Type::U32) && IsImmediate(high, Type::U32)) { Replace(inst, Value(static_cast(low.U32()) | (static_cast(high.U32()) << 32u))); } return; } case ValueOpcode::IAdd32: if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a + b; })) { ReplaceBinaryIdentity(inst, Type::U32, 0u); } return; case ValueOpcode::IAdd64: if (!FoldU64(inst, [](uint64_t a, uint64_t b) { return a + b; })) { ReplaceBinaryIdentity(inst, Type::U64, 0u); } return; case ValueOpcode::ISub32: { if (FoldU32(inst, [](uint32_t a, uint32_t b) { return a - b; })) { return; } const auto rhs = Arg(inst, 1); if (IsImmediate(rhs, Type::U32) && rhs.U32() == 0u) { Replace(inst, Arg(inst, 0)); } return; } case ValueOpcode::ISub64: { if (FoldU64(inst, [](uint64_t a, uint64_t b) { return a - b; })) { return; } const auto rhs = Arg(inst, 1); if (IsImmediate(rhs, Type::U64) && rhs.U64() == 0u) { Replace(inst, Arg(inst, 0)); } return; } case ValueOpcode::IMul32: if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a * b; })) { ReplaceBinaryIdentity(inst, Type::U32, 1u); } return; case ValueOpcode::IMul64: if (!FoldU64(inst, [](uint64_t a, uint64_t b) { return a * b; })) { ReplaceBinaryIdentity(inst, Type::U64, 1u); } return; case ValueOpcode::WqmU64: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U64)) { const auto expand = [](uint32_t word) { auto quads = word | (word >> 1u); quads |= quads >> 2u; return (quads & 0x11111111u) * 0x0fu; }; const auto low = expand(static_cast(value.U64())); const auto high = expand(static_cast(value.U64() >> 32u)); Replace(inst, Value(static_cast(low) | (static_cast(high) << 32u))); } return; } case ValueOpcode::UDiv32: { const auto rhs = Arg(inst, 1); if (IsImmediate(rhs, Type::U32) && rhs.U32() == 1u) { Replace(inst, Arg(inst, 0)); } else if (IsImmediate(rhs, Type::U32) && rhs.U32() != 0u) { FoldU32(inst, [](uint32_t a, uint32_t b) { return a / b; }); } return; } case ValueOpcode::SMulHi: FoldU32(inst, [](uint32_t a, uint32_t b) { const auto product = static_cast(std::bit_cast(a)) * static_cast(std::bit_cast(b)); return static_cast(static_cast(product) >> 32u); }); return; case ValueOpcode::UMulHi: FoldU32(inst, [](uint32_t a, uint32_t b) { return static_cast((static_cast(a) * b) >> 32u); }); return; case ValueOpcode::IAbs32: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U32)) { Replace(inst, Value((value.U32() & 0x80000000u) != 0u ? 0u - value.U32() : value.U32())); } return; } case ValueOpcode::ShiftLeftLogical32: if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a << (b & 31u); })) { const auto shift = Arg(inst, 1); if (IsImmediate(shift, Type::U32) && (shift.U32() & 31u) == 0u) { Replace(inst, Arg(inst, 0)); } } return; case ValueOpcode::ShiftRightLogical32: if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a >> (b & 31u); })) { const auto shift = Arg(inst, 1); if (IsImmediate(shift, Type::U32) && (shift.U32() & 31u) == 0u) { Replace(inst, Arg(inst, 0)); } } return; case ValueOpcode::ShiftRightArithmetic32: FoldU32(inst, [](uint32_t a, uint32_t b) { return static_cast(std::bit_cast(a) >> (b & 31u)); }); return; case ValueOpcode::ShiftLeftLogical64: if (!FoldU64Shift(inst, [](uint64_t a, uint32_t b) { return a << (b & 63u); })) { const auto shift = Arg(inst, 1); if (IsImmediate(shift, Type::U32) && (shift.U32() & 63u) == 0u) { Replace(inst, Arg(inst, 0)); } } return; case ValueOpcode::ShiftRightLogical64: if (!FoldU64Shift(inst, [](uint64_t a, uint32_t b) { return a >> (b & 63u); })) { const auto shift = Arg(inst, 1); if (IsImmediate(shift, Type::U32) && (shift.U32() & 63u) == 0u) { Replace(inst, Arg(inst, 0)); } } return; case ValueOpcode::ShiftRightArithmetic64: FoldU64Shift(inst, [](uint64_t a, uint32_t b) { return static_cast(std::bit_cast(a) >> (b & 63u)); }); return; case ValueOpcode::BitwiseAnd32: if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; })) { ReplaceBinaryIdentity(inst, Type::U32, 0xffffffffu); } return; case ValueOpcode::BitwiseAnd64: if (!FoldU64(inst, [](uint64_t a, uint64_t b) { return a & b; })) { ReplaceBinaryIdentity(inst, Type::U64, UINT64_MAX); } return; case ValueOpcode::BitwiseOr32: case ValueOpcode::BitwiseXor32: if (!FoldU32(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) { return opcode == ValueOpcode::BitwiseOr32 ? a | b : a ^ b; })) { ReplaceBinaryIdentity(inst, Type::U32, 0u); } return; case ValueOpcode::BitwiseNot32: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U32)) { Replace(inst, Value(~value.U32())); } else if (auto* producer = value.TryInstruction(); producer != nullptr && producer->GetOpcode() == ValueOpcode::BitwiseNot32) { Replace(inst, producer->Arg(0)); } return; } case ValueOpcode::BitCount32: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U32)) { Replace(inst, Value(static_cast(std::popcount(value.U32())))); } return; } case ValueOpcode::BitCount64: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U64)) { Replace(inst, Value(static_cast(std::popcount(value.U64())))); } return; } case ValueOpcode::SMin32: case ValueOpcode::SMax32: FoldU32(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) { const auto lhs = std::bit_cast(a); const auto rhs = std::bit_cast(b); return opcode == ValueOpcode::SMin32 ? (lhs < rhs ? a : b) : (lhs > rhs ? a : b); }); return; case ValueOpcode::UMin32: FoldU32(inst, [](uint32_t a, uint32_t b) { return std::min(a, b); }); return; case ValueOpcode::UMax32: FoldU32(inst, [](uint32_t a, uint32_t b) { return std::max(a, b); }); return; case ValueOpcode::IEqual32: FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; }); return; case ValueOpcode::INotEqual32: FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; }); return; case ValueOpcode::ULessThan32: FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a < b; }); return; case ValueOpcode::ULessThanEqual32: FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a <= b; }); return; case ValueOpcode::UGreaterThan32: FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a > b; }); return; case ValueOpcode::UGreaterThanEqual32: FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a >= b; }); return; case ValueOpcode::SLessThan32: case ValueOpcode::SLessThanEqual32: case ValueOpcode::SGreaterThan32: case ValueOpcode::SGreaterThanEqual32: FoldU32Compare(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) { const auto lhs = std::bit_cast(a); const auto rhs = std::bit_cast(b); switch (opcode) { case ValueOpcode::SLessThan32: return lhs < rhs; case ValueOpcode::SLessThanEqual32: return lhs <= rhs; case ValueOpcode::SGreaterThan32: return lhs > rhs; default: return lhs >= rhs; } }); return; case ValueOpcode::IEqual64: FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a == b; }); return; case ValueOpcode::INotEqual64: FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a != b; }); return; case ValueOpcode::ULessThan64: FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a < b; }); return; case ValueOpcode::UGreaterThan64: FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return a > b; }); return; case ValueOpcode::SLessThan64: FoldU64Compare(inst, [](uint64_t a, uint64_t b) { return std::bit_cast(a) < std::bit_cast(b); }); return; case ValueOpcode::LogicalAnd: if (!FoldLogical(inst, [](bool a, bool b) { return a && b; })) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (IsImmediate(lhs, Type::U1)) { Replace(inst, lhs.U1() ? rhs : lhs); } else if (IsImmediate(rhs, Type::U1)) { Replace(inst, rhs.U1() ? lhs : rhs); } } return; case ValueOpcode::LogicalOr: if (!FoldLogical(inst, [](bool a, bool b) { return a || b; })) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (IsImmediate(lhs, Type::U1)) { Replace(inst, lhs.U1() ? lhs : rhs); } else if (IsImmediate(rhs, Type::U1)) { Replace(inst, rhs.U1() ? rhs : lhs); } } return; case ValueOpcode::LogicalXor: if (!FoldLogical(inst, [](bool a, bool b) { return a != b; })) { const auto lhs = Arg(inst, 0); const auto rhs = Arg(inst, 1); if (IsImmediate(lhs, Type::U1) && !lhs.U1()) { Replace(inst, rhs); } else if (IsImmediate(rhs, Type::U1) && !rhs.U1()) { Replace(inst, lhs); } } return; case ValueOpcode::LogicalNot: { const auto value = Arg(inst, 0); if (IsImmediate(value, Type::U1)) { Replace(inst, Value(!value.U1())); } else if (auto* producer = value.TryInstruction(); producer != nullptr && producer->GetOpcode() == ValueOpcode::LogicalNot) { Replace(inst, producer->Arg(0)); } return; } default: return; } } } // namespace void ConstantPropagationPass(const BlockList& blocks) { std::unordered_set lowered_ancillary; for (auto* block: blocks) { for (auto inst = block->begin(); inst != block->end(); ++inst) { FoldInstruction(*block, inst, lowered_ancillary); } } // Normalize retained PHI/select values only after every supported field read has // been lowered; direct raw consumers remain unsupported. for (auto* source: lowered_ancillary) { const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) { const auto& user = *use.user; if (!user.HasUses() && !user.MayHaveSideEffects()) { return true; } return user.GetOpcode() == ValueOpcode::Phi || (user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u); }); if (retained_only) { Replace(*source, Value(0u)); } } } } // namespace Libs::Graphics::ShaderRecompiler::IR