mirror of
https://github.com/diasurgical/DevilutionX.git
synced 2026-10-11 22:29:54 +00:00
Add FixedPoint<StorageT, FractionalBits> type
Introduces a strongly-typed wrapper for the 10.6 fixed-point values used throughout the engine (e.g. hit points, mana), with Fixed10_6 (int16_t storage) and Fixed26_6 (int32_t storage) aliases. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3755190275
commit
c2080753e6
@@ -55,6 +55,7 @@ set(standalone_tests
|
||||
crawl_test
|
||||
data_file_test
|
||||
file_util_test
|
||||
fixed_point_test
|
||||
format_int_test
|
||||
ini_test
|
||||
mod_identity_test
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
#include "utils/attributes.h"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
namespace fixed_point_detail {
|
||||
|
||||
template <typename T>
|
||||
struct DoubleWidthImpl;
|
||||
|
||||
template <>
|
||||
struct DoubleWidthImpl<int16_t> {
|
||||
using Type = int32_t;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct DoubleWidthImpl<int32_t> {
|
||||
using Type = int64_t;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using DoubleWidth = typename DoubleWidthImpl<T>::Type;
|
||||
|
||||
} // namespace fixed_point_detail
|
||||
|
||||
/**
|
||||
* @brief A fixed point number with `FractionalBits` fractional bits, backed by `StorageT`.
|
||||
*
|
||||
* Used throughout the engine for values such as hit points and mana, where the
|
||||
* fractional part allows regeneration/damage to accumulate sub-point precision.
|
||||
*/
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
class FixedPoint {
|
||||
public:
|
||||
using StorageType = StorageT;
|
||||
static constexpr unsigned FractionalBitsV = FractionalBits;
|
||||
|
||||
FixedPoint() = default;
|
||||
|
||||
/**
|
||||
* @brief Widens (or narrows) a fixed point value with the same fractional bits but a different storage type.
|
||||
*/
|
||||
template <typename OtherStorageT>
|
||||
DVL_ALWAYS_INLINE explicit constexpr FixedPoint(FixedPoint<OtherStorageT, FractionalBits> other)
|
||||
: raw_(static_cast<StorageT>(other.raw()))
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE static constexpr FixedPoint fromRaw(StorageT raw)
|
||||
{
|
||||
FixedPoint result;
|
||||
result.raw_ = raw;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE static constexpr FixedPoint fromInt(int whole)
|
||||
{
|
||||
return fromRaw(static_cast<StorageT>(whole << FractionalBits));
|
||||
}
|
||||
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr StorageT raw() const
|
||||
{
|
||||
return raw_;
|
||||
}
|
||||
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr StorageT whole() const
|
||||
{
|
||||
return static_cast<StorageT>(raw_ >> FractionalBits);
|
||||
}
|
||||
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr StorageT fractional() const
|
||||
{
|
||||
return static_cast<StorageT>(raw_ & ((1 << FractionalBits) - 1));
|
||||
}
|
||||
|
||||
DVL_ALWAYS_INLINE constexpr FixedPoint &operator+=(FixedPoint other)
|
||||
{
|
||||
raw_ = static_cast<StorageT>(raw_ + other.raw_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DVL_ALWAYS_INLINE constexpr FixedPoint &operator-=(FixedPoint other)
|
||||
{
|
||||
raw_ = static_cast<StorageT>(raw_ - other.raw_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DVL_ALWAYS_INLINE constexpr FixedPoint &operator*=(int factor)
|
||||
{
|
||||
raw_ = static_cast<StorageT>(raw_ * factor);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DVL_ALWAYS_INLINE constexpr FixedPoint &operator/=(int factor)
|
||||
{
|
||||
raw_ = static_cast<StorageT>(raw_ / factor);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Multiplies by another fixed point value of the same type, rescaling the result.
|
||||
*/
|
||||
DVL_ALWAYS_INLINE constexpr FixedPoint &operator*=(FixedPoint factor)
|
||||
{
|
||||
using Wide = fixed_point_detail::DoubleWidth<StorageT>;
|
||||
const Wide product = static_cast<Wide>(raw_) * static_cast<Wide>(factor.raw_);
|
||||
raw_ = static_cast<StorageT>(product >> FractionalBits);
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr bool operator==(FixedPoint other) const { return raw_ == other.raw_; }
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr bool operator!=(FixedPoint other) const { return raw_ != other.raw_; }
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr bool operator<(FixedPoint other) const { return raw_ < other.raw_; }
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr bool operator<=(FixedPoint other) const { return raw_ <= other.raw_; }
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr bool operator>(FixedPoint other) const { return raw_ > other.raw_; }
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr bool operator>=(FixedPoint other) const { return raw_ >= other.raw_; }
|
||||
|
||||
private:
|
||||
StorageT raw_;
|
||||
};
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator+(FixedPoint<StorageT, FractionalBits> a, FixedPoint<StorageT, FractionalBits> b)
|
||||
{
|
||||
a += b;
|
||||
return a;
|
||||
}
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator-(FixedPoint<StorageT, FractionalBits> a, FixedPoint<StorageT, FractionalBits> b)
|
||||
{
|
||||
a -= b;
|
||||
return a;
|
||||
}
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator*(FixedPoint<StorageT, FractionalBits> a, int factor)
|
||||
{
|
||||
a *= factor;
|
||||
return a;
|
||||
}
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator*(int factor, FixedPoint<StorageT, FractionalBits> a)
|
||||
{
|
||||
a *= factor;
|
||||
return a;
|
||||
}
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator/(FixedPoint<StorageT, FractionalBits> a, int factor)
|
||||
{
|
||||
a /= factor;
|
||||
return a;
|
||||
}
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator*(FixedPoint<StorageT, FractionalBits> a, FixedPoint<StorageT, FractionalBits> b)
|
||||
{
|
||||
a *= b;
|
||||
return a;
|
||||
}
|
||||
|
||||
template <typename StorageT, unsigned FractionalBits>
|
||||
[[nodiscard]] DVL_ALWAYS_INLINE constexpr FixedPoint<StorageT, FractionalBits> operator-(FixedPoint<StorageT, FractionalBits> a)
|
||||
{
|
||||
return FixedPoint<StorageT, FractionalBits>::fromRaw(static_cast<StorageT>(-a.raw()));
|
||||
}
|
||||
|
||||
using Fixed10_6 = FixedPoint<int16_t, 6>;
|
||||
using Fixed26_6 = FixedPoint<int32_t, 6>;
|
||||
|
||||
template <typename T>
|
||||
concept FixedPointType = std::same_as<T, FixedPoint<typename T::StorageType, T::FractionalBitsV>>;
|
||||
|
||||
template <typename T>
|
||||
concept Fixed6Type = FixedPointType<T> && T::FractionalBitsV == 6;
|
||||
|
||||
} // namespace devilution
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "utils/fixed_point.hpp"
|
||||
|
||||
namespace devilution {
|
||||
namespace {
|
||||
|
||||
TEST(FixedPointTest, FromInt_RoundTripsThroughWhole)
|
||||
{
|
||||
const Fixed10_6 value = Fixed10_6::fromInt(5);
|
||||
EXPECT_EQ(value.whole(), 5);
|
||||
EXPECT_EQ(value.fractional(), 0);
|
||||
EXPECT_EQ(value.raw(), 5 << 6);
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, FromRaw_SplitsWholeAndFractional)
|
||||
{
|
||||
const Fixed10_6 value = Fixed10_6::fromRaw((3 << 6) + 17);
|
||||
EXPECT_EQ(value.whole(), 3);
|
||||
EXPECT_EQ(value.fractional(), 17);
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, Addition)
|
||||
{
|
||||
const Fixed10_6 a = Fixed10_6::fromInt(2);
|
||||
const Fixed10_6 b = Fixed10_6::fromRaw(10);
|
||||
EXPECT_EQ((a + b).raw(), (2 << 6) + 10);
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, Subtraction)
|
||||
{
|
||||
const Fixed10_6 a = Fixed10_6::fromInt(5);
|
||||
const Fixed10_6 b = Fixed10_6::fromInt(2);
|
||||
EXPECT_EQ((a - b), Fixed10_6::fromInt(3));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, Negation)
|
||||
{
|
||||
const Fixed10_6 value = Fixed10_6::fromInt(4);
|
||||
EXPECT_EQ(-value, Fixed10_6::fromInt(-4));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, MultiplyByScalar)
|
||||
{
|
||||
const Fixed10_6 value = Fixed10_6::fromInt(3);
|
||||
EXPECT_EQ(value * 4, Fixed10_6::fromInt(12));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, DivideByScalar)
|
||||
{
|
||||
const Fixed10_6 value = Fixed10_6::fromInt(12);
|
||||
EXPECT_EQ(value / 4, Fixed10_6::fromInt(3));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, CompoundAssignment)
|
||||
{
|
||||
Fixed10_6 value = Fixed10_6::fromInt(1);
|
||||
value += Fixed10_6::fromInt(2);
|
||||
value *= 3;
|
||||
EXPECT_EQ(value, Fixed10_6::fromInt(9));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, Comparisons)
|
||||
{
|
||||
const Fixed10_6 a = Fixed10_6::fromInt(1);
|
||||
const Fixed10_6 b = Fixed10_6::fromInt(2);
|
||||
EXPECT_LT(a, b);
|
||||
EXPECT_LE(a, a);
|
||||
EXPECT_GT(b, a);
|
||||
EXPECT_GE(b, b);
|
||||
EXPECT_NE(a, b);
|
||||
EXPECT_EQ(a, Fixed10_6::fromInt(1));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, WiderStorageAvoidsOverflow)
|
||||
{
|
||||
const Fixed26_6 value = Fixed26_6::fromInt(2000);
|
||||
EXPECT_EQ(value.whole(), 2000);
|
||||
EXPECT_EQ(value.raw(), 2000 << 6);
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, MultiplyByFixedPoint)
|
||||
{
|
||||
const Fixed10_6 half = Fixed10_6::fromRaw(32); // 0.5
|
||||
EXPECT_EQ(Fixed10_6::fromInt(10) * half, Fixed10_6::fromInt(5));
|
||||
EXPECT_EQ(Fixed10_6::fromInt(3) * Fixed10_6::fromInt(4), Fixed10_6::fromInt(12));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, MultiplyByFixedPointDoesNotOverflowWideStorage)
|
||||
{
|
||||
const Fixed26_6 large = Fixed26_6::fromInt(2000);
|
||||
const Fixed26_6 doubled = Fixed26_6::fromInt(2);
|
||||
EXPECT_EQ(large * doubled, Fixed26_6::fromInt(4000));
|
||||
}
|
||||
|
||||
TEST(FixedPointTest, WideningConversion)
|
||||
{
|
||||
const Fixed10_6 narrow = Fixed10_6::fromInt(5);
|
||||
const Fixed26_6 widened { narrow };
|
||||
EXPECT_EQ(widened, Fixed26_6::fromInt(5));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace devilution
|
||||
Reference in New Issue
Block a user