mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 03:16:54 +02:00
shader: eliminate impossible relative writes with closed loop bounds
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
This commit is contained in:
@@ -352,6 +352,7 @@ public:
|
||||
if (m_program.resource_tracking_complete) {
|
||||
Fail(0, "resources already tracked");
|
||||
}
|
||||
FoldBoundedLoopSelectors();
|
||||
PlanScalarReads();
|
||||
EliminateDeadCode(m_program.blocks);
|
||||
PlanIndirectDescriptors();
|
||||
@@ -1409,12 +1410,13 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
const Inst* BoundedLoop(Value key, const Block* use) const {
|
||||
const Inst* BoundedLoop(Value key, const Block* use, const auto& accepts_bound) const {
|
||||
const auto* phi = key.Resolve().TryInstruction();
|
||||
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi ||
|
||||
phi->GetType() != Type::U32 || phi->NumArgs() != 2u ||
|
||||
phi->GetType() != Type::U32 || phi->NumArgs() != 2u || phi->NumPhiBlocks() != 2u ||
|
||||
m_program.blocks.size() != m_program.block_info.size()) return {};
|
||||
const Block* increment_block = nullptr;
|
||||
uint32_t initial_arm = 0;
|
||||
for (uint32_t initial = 0; initial < 2u; ++initial) {
|
||||
const auto zero = phi->Arg(initial).Resolve();
|
||||
const auto* step = phi->Arg(initial ^ 1u).Resolve().TryInstruction();
|
||||
@@ -1427,6 +1429,7 @@ private:
|
||||
(step->Arg(1).Resolve() == key &&
|
||||
ImmediateU32(step->Arg(0), increment) && increment == 1u)) {
|
||||
increment_block = step->Parent();
|
||||
initial_arm = initial;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1437,8 +1440,8 @@ private:
|
||||
const auto* compare = use_of_key.user;
|
||||
if ((compare->GetOpcode() != ValueOpcode::SLessThan32 &&
|
||||
compare->GetOpcode() != ValueOpcode::ULessThan32) || use_of_key.operand != 0u ||
|
||||
!ValidateRuntimeValue(m_program, compare->Arg(1), RuntimeValueType::Integer)) continue;
|
||||
if (!GuardedOnEntry(use, stops, [&](const EdgePredicate& edge) {
|
||||
!accepts_bound(*compare, initial_arm)) continue;
|
||||
if (!GuardedOnEntry(use != nullptr ? use : increment_block, stops, [&](const EdgePredicate& edge) {
|
||||
return edge.positive && Implies(edge.condition, Value(use_of_key.user));
|
||||
})) continue;
|
||||
LoopBoundProof proof(m_program, *phi, *compare);
|
||||
@@ -1451,6 +1454,54 @@ private:
|
||||
return {};
|
||||
}
|
||||
|
||||
const Inst* BoundedLoop(Value key, const Block* use) const {
|
||||
return BoundedLoop(key, use, [&](const Inst& compare, uint32_t) {
|
||||
return ValidateRuntimeValue(m_program, compare.Arg(1), RuntimeValueType::Integer);
|
||||
});
|
||||
}
|
||||
|
||||
void FoldBoundedLoopSelectors() {
|
||||
for (auto* header: m_program.blocks) {
|
||||
for (auto& induction: *header) {
|
||||
const Inst* carried = nullptr;
|
||||
uint32_t maximum = 0;
|
||||
const auto* compare = BoundedLoop(Value(&induction), nullptr,
|
||||
[&](const Inst& test, uint32_t initial) {
|
||||
if (test.GetOpcode() != ValueOpcode::ULessThan32) return false;
|
||||
carried = test.Arg(1).Resolve().TryInstruction();
|
||||
if (carried == nullptr || carried->GetOpcode() != ValueOpcode::Phi ||
|
||||
carried->GetType() != Type::U32 || carried->Parent() != header ||
|
||||
carried->NumArgs() != 2u || carried->NumPhiBlocks() != 2u ||
|
||||
carried->PhiBlock(0) != induction.PhiBlock(0) ||
|
||||
carried->PhiBlock(1) != induction.PhiBlock(1)) return false;
|
||||
maximum = SelectorMaximum(carried->Arg(initial));
|
||||
if (maximum == 0u || maximum == UINT32_MAX) return false;
|
||||
const std::array assumptions {std::pair {static_cast<const Inst*>(&induction), maximum - 1u},
|
||||
std::pair {carried, maximum}};
|
||||
// Close the carried-bound invariant before using either assumption.
|
||||
return SelectorMaximum(carried->Arg(initial ^ 1u), assumptions) <= maximum;
|
||||
});
|
||||
if (compare == nullptr) continue;
|
||||
const std::array assumptions {std::pair {static_cast<const Inst*>(&induction), maximum - 1u},
|
||||
std::pair {carried, maximum}};
|
||||
// Native scalar branches advance the zero/+1 induction uniformly, so
|
||||
// even an any-lane bound test limits it by the largest carried bound.
|
||||
// Per-lane edges instead require the comparison on every executing lane.
|
||||
for (auto* block: m_program.blocks) {
|
||||
if (!GuardedOnEntry(block, [&](const Block* at) { return at == header; },
|
||||
[&](const EdgePredicate& edge) {
|
||||
return edge.positive && Implies(edge.condition, Value(const_cast<Inst*>(compare)));
|
||||
})) continue;
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::SelectU32 &&
|
||||
SelectorPredicateFalse(inst.Arg(0), assumptions))
|
||||
inst.ReplaceUsesWith(inst.Arg(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BoundedSelectedIndex(Value index, Value active,
|
||||
DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
const auto* selected = index.Resolve().TryInstruction();
|
||||
@@ -1728,20 +1779,56 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t SelectorMaximum(Value value) const {
|
||||
using SelectorBounds = std::span<const std::pair<const Inst*, uint32_t>>;
|
||||
|
||||
bool SelectorPredicateFalse(Value value, SelectorBounds bounds, uint32_t depth = 0) const {
|
||||
if (depth > 32u) return false;
|
||||
value = value.Resolve();
|
||||
if (value.IsImmediate()) return value.GetType() == Type::U1 && !value.U1();
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return false;
|
||||
if (inst->GetOpcode() == ValueOpcode::LogicalAnd)
|
||||
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u) ||
|
||||
SelectorPredicateFalse(inst->Arg(1), bounds, depth + 1u);
|
||||
if (inst->GetOpcode() == ValueOpcode::IEqual32) {
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
uint32_t target;
|
||||
if (ImmediateU32(inst->Arg(arg), target) &&
|
||||
SelectorMaximum(inst->Arg(arg ^ 1u), bounds, depth + 1u) < target) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0) const {
|
||||
if (depth > 32u) return UINT32_MAX;
|
||||
value = value.Resolve();
|
||||
if (value.IsImmediate() && value.GetType() == Type::U32) return value.U32();
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return UINT32_MAX;
|
||||
if (inst->GetOpcode() == ValueOpcode::FindILsb32)
|
||||
return NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
|
||||
if (inst->GetOpcode() == ValueOpcode::UMin32)
|
||||
return std::min(SelectorMaximum(inst->Arg(0)), SelectorMaximum(inst->Arg(1)));
|
||||
uint32_t shift;
|
||||
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), shift) && shift < 32u) {
|
||||
const auto maximum = uint64_t {SelectorMaximum(inst->Arg(0))} << shift;
|
||||
if (maximum <= UINT32_MAX) return static_cast<uint32_t>(maximum);
|
||||
for (const auto& [phi, maximum]: bounds)
|
||||
if (inst == phi) return maximum;
|
||||
const auto maximum = [&](uint32_t arg) {
|
||||
return SelectorMaximum(inst->Arg(arg), bounds, depth + 1u);
|
||||
};
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::FindILsb32:
|
||||
return NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
|
||||
case ValueOpcode::UMin32:
|
||||
case ValueOpcode::BitwiseAnd32: return std::min(maximum(0), maximum(1));
|
||||
case ValueOpcode::SelectU32:
|
||||
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u)
|
||||
? maximum(2) : std::max(maximum(1), maximum(2));
|
||||
case ValueOpcode::ShiftRightLogical32:
|
||||
case ValueOpcode::ShiftLeftLogical32: {
|
||||
uint32_t shift;
|
||||
if (!ImmediateU32(inst->Arg(1), shift) || shift >= 32u) break;
|
||||
if (inst->GetOpcode() == ValueOpcode::ShiftRightLogical32) return maximum(0) >> shift;
|
||||
const auto result = uint64_t {maximum(0)} << shift;
|
||||
if (result <= UINT32_MAX) return static_cast<uint32_t>(result);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
@@ -2728,6 +2728,92 @@ void TestInvariantLoopPhi() {
|
||||
"loop-invariant descriptor phi was not evaluated through typed SSA");
|
||||
}
|
||||
|
||||
void TestBoundedRelativeRegisterWrites() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
enum class Variant { Bounded, BoundClobber, DescriptorClobber, EntryBypass };
|
||||
for (const auto variant : {Variant::Bounded, Variant::BoundClobber,
|
||||
Variant::DescriptorClobber, Variant::EntryBypass}) {
|
||||
Fixture fixture;
|
||||
auto *entry = fixture.block;
|
||||
auto *header = fixture.AddBlock();
|
||||
auto *body = fixture.AddBlock();
|
||||
auto *exit = fixture.AddBlock();
|
||||
entry->AddBranch(header);
|
||||
if (variant == Variant::EntryBypass) entry->AddBranch(body);
|
||||
header->AddBranch(body);
|
||||
header->AddBranch(exit);
|
||||
body->AddBranch(header);
|
||||
fixture.program.block_info[0].terminator = {
|
||||
.kind = variant == Variant::EntryBypass ? CFG::TerminatorKind::ConditionalBranch
|
||||
: CFG::TerminatorKind::Branch,
|
||||
.true_block = 1u, .false_block = 2u};
|
||||
fixture.program.block_info[1].terminator = {
|
||||
.kind = CFG::TerminatorKind::ConditionalBranch,
|
||||
.true_block = 2u, .false_block = 3u};
|
||||
fixture.program.block_info[2].terminator = {
|
||||
.kind = CFG::TerminatorKind::Branch, .true_block = 1u};
|
||||
fixture.program.block_info[3].terminator.kind = CFG::TerminatorKind::Return;
|
||||
|
||||
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
const auto enabled = fixture.Emit(ValueOpcode::ULessThan32, {lane, Value(32u)});
|
||||
fixture.program.block_info[0].condition = enabled;
|
||||
const auto chunk = fixture.Emit(ValueOpcode::SelectU32,
|
||||
{enabled, Value(64u), Value(512u)});
|
||||
const auto initial_bound = fixture.Emit(ValueOpcode::ShiftRightLogical32,
|
||||
{chunk, Value(6u)});
|
||||
const auto initial_records = fixture.UserData(2u);
|
||||
const auto base = fixture.UserData(0u);
|
||||
auto &counter = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
auto &bound = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
auto &records = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
|
||||
const auto in_range = fixture.Emit(ValueOpcode::ULessThan32,
|
||||
{Value(&counter), Value(&bound)}, 0, header);
|
||||
fixture.program.block_info[1].condition = fixture.Emit(
|
||||
ValueOpcode::ConditionRef, {in_range}, CFG::BranchCondition::VccNonZero, header);
|
||||
const auto m0 = fixture.Emit(ValueOpcode::BitwiseAnd32,
|
||||
{fixture.Emit(ValueOpcode::ShiftLeftLogical32,
|
||||
{Value(&counter), Value(1u)}, 0, body), Value(255u)}, 0, body);
|
||||
const auto relative_write = [&](Value old_value, uint32_t offset) {
|
||||
const auto selected = fixture.Emit(ValueOpcode::IEqual32,
|
||||
{m0, Value(offset)}, 0, body);
|
||||
const auto active = fixture.Emit(ValueOpcode::LogicalAnd,
|
||||
{enabled, selected}, 0, body);
|
||||
return fixture.Emit(ValueOpcode::SelectU32, {active, lane, old_value}, 0, body);
|
||||
};
|
||||
bound.AddPhiOperand(entry, initial_bound);
|
||||
bound.AddPhiOperand(body, relative_write(Value(&bound),
|
||||
variant == Variant::BoundClobber ? 14u : 22u));
|
||||
records.AddPhiOperand(entry, initial_records);
|
||||
records.AddPhiOperand(body, relative_write(Value(&records),
|
||||
variant == Variant::DescriptorClobber ? 14u : 21u));
|
||||
counter.AddPhiOperand(entry, Value(0u));
|
||||
counter.AddPhiOperand(body, fixture.Emit(ValueOpcode::IAdd32,
|
||||
{Value(&counter), Value(1u)}, 0, body));
|
||||
const auto handle = fixture.Emit(ValueOpcode::GetBufferResource,
|
||||
{base, Value(4u << 16u), Value(&records), Value(0x16204u)},
|
||||
MemoryFlags{0, 0x3e8}, exit);
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Buffer;
|
||||
fixture.Emit(ValueOpcode::StoreBufferU32,
|
||||
{handle, Value(0u), Value(0u), Value(0u), Value(1u), Value(true)},
|
||||
fixture.AddMemory(memory, 0x3e8), exit);
|
||||
if (variant != Variant::Bounded) {
|
||||
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
|
||||
"relative register proof discarded a possible loop clobber");
|
||||
continue;
|
||||
}
|
||||
fixture.PlanAndTrack();
|
||||
const std::array<uint32_t, 3> user_data{0x1000u, 0u, 72u};
|
||||
SrtRuntime runtime{.user_data = user_data};
|
||||
DescriptorValue descriptor;
|
||||
Check(SrtWalker(fixture.program, runtime).EvaluateDescriptor(
|
||||
fixture.program.info.buffers[0].source, descriptor) &&
|
||||
descriptor.dwords[2] == 72u,
|
||||
"impossible relative writes left a false descriptor dependency");
|
||||
}
|
||||
}
|
||||
|
||||
void TestDmaAddressMaterialization() {
|
||||
Fixture fixture;
|
||||
const auto based =
|
||||
@@ -3553,6 +3639,7 @@ int main() {
|
||||
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
|
||||
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
|
||||
Run("invariant loop phi", TestInvariantLoopPhi);
|
||||
Run("bounded relative register writes", TestBoundedRelativeRegisterWrites);
|
||||
Run("DMA address materialization", TestDmaAddressMaterialization);
|
||||
Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma);
|
||||
Run("buffer swizzle specialization", TestBufferSwizzleSpecialization);
|
||||
|
||||
Reference in New Issue
Block a user