diff --git a/Source/Core/Common/MathUtil.h b/Source/Core/Common/MathUtil.h index 18a0c305ce..2aa2b406ab 100644 --- a/Source/Core/Common/MathUtil.h +++ b/Source/Core/Common/MathUtil.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -30,6 +31,48 @@ constexpr auto Lerp(const T& x, const T& y, const F& a) -> decltype(x + (y - x) return x + (y - x) * a; } +// Casts the specified value to a Dest. The value will be clamped to fit in the destination type. +// Warning: The result of SaturatingCast(NaN) is undefined. +template +constexpr Dest SaturatingCast(T value) +{ + static_assert(std::is_integral()); + + constexpr Dest lo = std::numeric_limits::lowest(); + constexpr Dest hi = std::numeric_limits::max(); + + // T being a signed integer and Dest unsigned is a problematic case because the value will + // be converted into an unsigned integer, and u32(...) < 0 is always false. + if constexpr (std::is_integral() && std::is_signed() && std::is_unsigned()) + { + static_assert(lo == 0); + if (value < 0) + return lo; + // Now that we got rid of negative values, we can safely cast value to an unsigned T + // since unsigned T can represent any positive value signed T could represent. + // The compiler will then promote the LHS or the RHS if necessary. + if (std::make_unsigned_t(value) > hi) + return hi; + } + else if constexpr (std::is_integral() && std::is_unsigned() && std::is_signed()) + { + // value and hi will never be negative, and hi is representable as an unsigned Dest. + if (value > std::make_unsigned_t(hi)) + return hi; + } + else + { + // Do not use std::clamp or a similar function here to avoid overflow. + // For example, if Dest = s64 and T = int, we want integer promotion to convert value to a s64 + // instead of changing lo or hi into an int. + if (value < lo) + return lo; + if (value > hi) + return hi; + } + return static_cast(value); +} + template constexpr bool IsPow2(T imm) { diff --git a/Source/UnitTests/Common/MathUtilTest.cpp b/Source/UnitTests/Common/MathUtilTest.cpp index c0609bbb92..e31777d16d 100644 --- a/Source/UnitTests/Common/MathUtilTest.cpp +++ b/Source/UnitTests/Common/MathUtilTest.cpp @@ -26,3 +26,45 @@ TEST(MathUtil, NextPowerOf2) EXPECT_EQ(8U, MathUtil::NextPowerOf2(6)); EXPECT_EQ(0x40000000U, MathUtil::NextPowerOf2(0x23456789)); } + +TEST(MathUtil, SaturatingCast) +{ + // Cast from an integer type to a smaller type + EXPECT_EQ(255u, (MathUtil::SaturatingCast(1000))); + EXPECT_EQ(255u, (MathUtil::SaturatingCast(1000u))); + EXPECT_EQ(255u, (MathUtil::SaturatingCast(1000))); + + // Cast from a signed integer type + EXPECT_EQ(0u, (MathUtil::SaturatingCast(-1))); + EXPECT_EQ(0u, (MathUtil::SaturatingCast(-1000))); + EXPECT_EQ(0u, (MathUtil::SaturatingCast(-1))); + EXPECT_EQ(-1000, (MathUtil::SaturatingCast(-1000))); + EXPECT_EQ(-1000, (MathUtil::SaturatingCast(-1000))); + EXPECT_EQ(-1000, (MathUtil::SaturatingCast(-1000))); + + // Cast from an unsigned integer type to a smaller integer type + EXPECT_EQ(0x7fff, (MathUtil::SaturatingCast(0xffffffffu))); + EXPECT_EQ(0x7fffffff, (MathUtil::SaturatingCast(0xffffffffu))); + + // Cast from a floating point type to an integer type + EXPECT_EQ(255u, MathUtil::SaturatingCast(1234.0)); + EXPECT_EQ(0u, MathUtil::SaturatingCast(-1234.0)); + EXPECT_EQ(127, MathUtil::SaturatingCast(5678.0)); + EXPECT_EQ(-128, MathUtil::SaturatingCast(-5678.0)); + EXPECT_EQ(65535u, MathUtil::SaturatingCast(999999.0)); + + // Negative zero + EXPECT_EQ(0u, MathUtil::SaturatingCast(0.0)); + EXPECT_EQ(0u, MathUtil::SaturatingCast(-0.0)); + EXPECT_EQ(0, MathUtil::SaturatingCast(0.0)); + EXPECT_EQ(0, MathUtil::SaturatingCast(-0.0)); + + // Edge cases + EXPECT_EQ(std::numeric_limits::max(), + MathUtil::SaturatingCast(std::numeric_limits::infinity())); + EXPECT_EQ(std::numeric_limits::min(), + MathUtil::SaturatingCast(-std::numeric_limits::infinity())); + // 16777217 = 2^24 + 1 is the first integer that cannot be represented correctly with a f32. + EXPECT_EQ(16777216, MathUtil::SaturatingCast(float(16777216))); + EXPECT_EQ(16777216, MathUtil::SaturatingCast(float(16777217))); +}