shader: emit F32 min/max as exact compare-selects, MED3 with one max and saturate as NClamp

This commit is contained in:
nmzik committed 2026-10-20 23:36:26 +02:00
1 parent 8cc5f5364a
commit 3c4124dafa
4 files changed
+45 -51

No files matched your search

@@ -115,7 +115,7 @@ uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
} else { } else {
state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc); state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc);
} }
const auto nan = EmitClassifyF32(state, src).nan; const auto nan = Unary(state, spv::OpIsNan, TypeBool(state), src);
if (signed_value) { if (signed_value) {
const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src, const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src,
ConstantF32(state, 0xcf000000u)); ConstantF32(state, 0xcf000000u));
@@ -242,11 +242,9 @@ uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1)
} }
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) { uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined. // The clamp modifier runs in DX10 clamp mode: NaN becomes 0, as NClamp's NMax step does.
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp, return EmitExt(state, TypeF32(state), GLSLstd450NClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)}); {arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
clamped);
} }
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) { uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
@@ -25,10 +25,6 @@ F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits) {
return cls; return cls;
} }
F32Class EmitClassifyF32(EmitterState& state, uint32_t value) {
return EmitClassifyF32Bits(state, EmitBitcastF32ToU32(state, value));
}
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit, uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
uint32_t class_match) { uint32_t class_match) {
const auto selected = const auto selected =
@@ -100,51 +96,53 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
return EmitClassMaskBits(state, bits, mask, true); return EmitClassMaskBits(state, bits, mask, true);
} }
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, uint32_t lhs_bits,
const F32Class& lhs_class, const F32Class& rhs_class, uint32_t rhs_bits, bool max_value) {
bool max_value) { // RDNA2 V_MIN/V_MAX_F32 return the other source for a NaN source (S1 when both are NaN)
const auto numeric_cond = state.builder.AllocateId(); // and order -0 below +0. An unordered compare keeps S0 for a NaN S1 and, unlike NMin/NMax,
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan, // always returns the exact bits of one source.
TypeBool(state), numeric_cond, lhs, rhs); const auto either = EmitOrU32(state, lhs_bits, rhs_bits);
const auto ordered_bits = const auto both_zero = EmitCompareU32Constant(state, spv::OpIEqual,
EmitSelectValueU32(state, numeric_cond, lhs_class.bits, rhs_class.bits); EmitAndConstant(state, either, 0x7fffffffu), 0);
const auto zero =
const auto both_zero = EmitLogicalAndBool(state, lhs_class.zero, rhs_class.zero); EmitBitcastU32ToF32(state, max_value ? EmitAndU32(state, lhs_bits, rhs_bits) : either);
const auto zero_bits = max_value ? EmitAndU32(state, lhs_class.bits, rhs_class.bits) const auto keep_lhs =
: EmitOrU32(state, lhs_class.bits, rhs_class.bits); Binary(state, max_value ? spv::OpFUnordGreaterThanEqual : spv::OpFUnordLessThan,
const auto numeric_bits = EmitSelectValueU32(state, both_zero, zero_bits, ordered_bits); TypeBool(state), lhs, rhs);
const auto ordered = Select(state, TypeF32(state), keep_lhs, lhs, rhs);
const auto rhs_nan_bits = const auto numeric = Select(state, TypeF32(state), both_zero, zero, ordered);
EmitSelectValueU32(state, rhs_class.nan, lhs_class.bits, numeric_bits); return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs), rhs,
const auto result_bits = EmitSelectValueU32(state, lhs_class.nan, rhs_class.bits, rhs_nan_bits); numeric);
return EmitBitcastU32ToF32(state, result_bits);
} }
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) { uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs); return EmitMinMaxF32(state, lhs, rhs, EmitBitcastF32ToU32(state, lhs),
const auto rhs_class = EmitClassifyF32(state, rhs); EmitBitcastF32ToU32(state, rhs), max_value);
return EmitMinMaxF32(state, lhs, rhs, lhs_class, rhs_class, max_value);
} }
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) { uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto a_class = EmitClassifyF32(state, a); const auto a_bits = EmitBitcastF32ToU32(state, a);
const auto b_class = EmitClassifyF32(state, b); const auto b_bits = EmitBitcastF32ToU32(state, b);
const auto c_class = EmitClassifyF32(state, c); const auto c_bits = EmitBitcastF32ToU32(state, c);
const auto min_ab = EmitMinMaxF32(state, a, b, a_class, b_class, false); const auto min_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, false);
const auto min_ab_class = EmitClassifyF32(state, min_ab); const auto min3 =
const auto min3 = EmitMinMaxF32(state, min_ab, c, min_ab_class, c_class, false); EmitMinMaxF32(state, min_ab, c, EmitBitcastF32ToU32(state, min_ab), c_bits, false);
const auto max_ab = EmitMinMaxF32(state, a, b, a_class, b_class, true); const auto max_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, true);
const auto max_ab_class = EmitClassifyF32(state, max_ab); const auto max3 =
const auto max3 = EmitMinMaxF32(state, max_ab, c, max_ab_class, c_class, true); EmitMinMaxF32(state, max_ab, c, EmitBitcastF32ToU32(state, max_ab), c_bits, true);
const auto max_bc = EmitMinMaxF32(state, b, c, b_class, c_class, true); // RDNA2 compares numerically against MAX3, including ties between signed zeros, then takes
const auto max_ac = EmitMinMaxF32(state, a, c, a_class, c_class, true); // V_MAX_F32 of the other two sources in their original order.
// RDNA2 compares numerically against MAX3, including ties between signed zeros.
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a); const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b); const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
const auto median = Select(state, TypeF32(state), a_is_max, max_bc, const auto x = Select(state, TypeF32(state), a_is_max, b, a);
Select(state, TypeF32(state), b_is_max, max_ac, max_ab)); const auto y =
const auto nan_ab = EmitLogicalOrBool(state, a_class.nan, b_class.nan); Select(state, TypeF32(state), EmitLogicalOrBool(state, a_is_max, b_is_max), c, b);
const auto any_nan = EmitLogicalOrBool(state, nan_ab, c_class.nan); const auto median = EmitMinMaxF32Value(state, x, y, true);
const auto is_nan = [&](uint32_t value) {
return Unary(state, spv::OpIsNan, TypeBool(state), value);
};
const auto any_nan =
EmitLogicalOrBool(state, EmitLogicalOrBool(state, is_nan(a), is_nan(b)), is_nan(c));
return Select(state, TypeF32(state), any_nan, min3, median); return Select(state, TypeF32(state), any_nan, min3, median);
} }
@@ -510,8 +510,6 @@ inline constexpr auto EmitLogicalNotBool = EmitNative<spv::OpLogicalNot, IR::Typ
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits); F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits);
F32Class EmitClassifyF32(EmitterState& state, uint32_t value);
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit, uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
uint32_t class_match); uint32_t class_match);
+2 -2
View File
@@ -27914,8 +27914,8 @@ TestCase VectorMed3F32NanUsesMin3Path(u32 wave) {
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MED3_F32, test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MED3_F32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}; O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.ir_counts = {{"FPMedTri32", std::size(cases)}}; test.ir_counts = {{"FPMedTri32", std::size(cases)}};
// Measured 17,593/36,607 words; keep classification growth below a single MED3. // Measured 16,140/33,705 words; keep min/max expansion growth below a single MED3.
test.max_spirv_words = wave == 32u ? 17600u : 36620u; test.max_spirv_words = wave == 32u ? 16150u : 33720u;
return test; return test;
} }