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.
This commit is contained in:
Karan Vora 2026-10-12 02:40:09 -04:00
parent 2ccbd376f8
commit 69d0dc2288
2 changed files with 67 additions and 0 deletions

View File

@ -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<IrValue*> 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;

View File

@ -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;