From 69d0dc228809a6786c99a38bb69171e830f61461 Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:40:09 -0400 Subject: [PATCH] perf(shader): treat f32 min, max, med3, floor and ceil results as denormal-free In the float modes that flush f32 denormals, writeOperand flushes the result of v_min/max/min3/max3/med3_f32 and v_floor/ceil_f32. DenormalFlushEliminator drops a flush only when it can show the value isn't a denormal, and it had no rule for these opcodes, so their result flush stayed even when every operand had already been flushed. FPMin32, FPMax32, FPMinTri32, FPMaxTri32 and FPMedTri32 return one of their operands' bits, or the and or or of two zeros, so the result is denormal-free when every operand is. FPFloor32 and FPCeil32 return an integer, a zero, an infinity or a NaN whatever their operand. In IEEE mode the min and max results and the floor and ceil results go through selects with a quieted operand, which the eliminator can't follow, so their flush stays there. agc_denormal_flush_elimination covers the five min and max forms with flushed and with unknown operands, floor and ceil of an unknown value, and a max carried around a loop. --- .../tests/DenormalFlushElimination.cpp | 60 +++++++++++++++++++ .../src/DenormalFlushEliminator.cpp | 7 +++ 2 files changed, 67 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index 47020acb0..3db6a8ce0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -150,6 +150,66 @@ int main() { return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses(); }); + passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] { + Builder b; + const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); }; + const auto flushedResult = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)})); + }; + auto& x = value(b.flush(b.unknown())); + auto& y = value(b.flush(b.unknown())); + auto& z = value(b.flush(b.unknown())); + auto& raw = value(b.unknown()); + IrValue* removed[] = { + &flushedResult(IrOpcode::FPMin32, {&x, &y}), + &flushedResult(IrOpcode::FPMax32, {&y, &x}), + &flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}), + &flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}), + &flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}), + }; + auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw}); + auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw}); + for (auto* flushed : removed) b.keep(*flushed); + b.keep(mixedPair); + b.keep(mixedMedian); + const bool counted = b.eliminate() == 5u; + bool kept = false; + for (auto* flushed : removed) kept |= flushed->HasUses(); + return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses(); + }); + + passed &= run("a floor or ceil needs no flush, even of an unknown value", [] { + Builder b; + auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})})); + auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})})); + b.keep(floored); + b.keep(ceiled); + return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses(); + }); + + passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + head.AddBranch(&body); + body.AddBranch(&body); + body.AddBranch(&exit); + auto& initial = b.flush(b.unknown()); + b.block = &body; + auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + body.AppendInstruction(&carried); + auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())}); + auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})}); + carried.AddPhiOperand(&head, &initial); + carried.AddPhiOperand(&body, &maximum); + auto& flushed = b.flush(maximum); + b.block = &exit; + b.keep(flushed); + return b.eliminate() == 0u && flushed.HasUses(); + }); + passed &= run("a phi of flushed values needs no flush", [] { Builder b; IrBlock& head = *b.block; diff --git a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp index 7de56772e..d448da5e6 100644 --- a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp @@ -51,6 +51,8 @@ private: case IrOpcode::ConvertF32S32: case IrOpcode::ConvertF32U32: case IrOpcode::ConvertF32F16: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: return true; case IrOpcode::BitCastU32F32: case IrOpcode::BitCastF32U32: @@ -67,6 +69,11 @@ private: case IrOpcode::SelectU32: case IrOpcode::SelectF32: return Contains(value.Argument(1)) && Contains(value.Argument(2)); + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPMinTri32: + case IrOpcode::FPMaxTri32: + case IrOpcode::FPMedTri32: case IrOpcode::Phi: for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { if (!Contains(value.Argument(index))) return false;