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:
Gleb Mazovetskiy
2026-10-11 21:55:08 +01:00
co-authored by Claude Sonnet 5
parent 3755190275
commit c2080753e6
3 changed files with 288 additions and 0 deletions
+1
View File
@@ -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
+183
View File
@@ -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
+104
View File
@@ -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