diff --git a/maths.hpp b/maths.hpp index a83a7753..27d12a72 100644 --- a/maths.hpp +++ b/maths.hpp @@ -376,33 +376,120 @@ smoothstep [[gnu::pure]] (T a, T b, T x) #include "types/string.hpp" -//----------------------------------------------------------------------------- + +/////////////////////////////////////////////////////////////////////////////// +// renormalisation of unit floating point and/or normalised integers + +// int -> float template -U +constexpr +typename std::enable_if< + !std::is_floating_point::value && std::is_floating_point::value, U +>::type renormalise [[gnu::pure]] (T t) { - static const T T_max = std::numeric_limits::max (); - static const U U_max = std::numeric_limits::max (); - static const bool shrinking = sizeof (U) < sizeof (T); - static const bool T_float = std::is_floating_point::value; - static const bool U_float = std::is_floating_point::value; - - if (T_float && U_float) - return U (t); - - if (T_float) { - return U (limit (t, 0, 1) * U_max); - } - - if (U_float) - return U(U (t) / T_max); - - if (shrinking) - return U (t / (sizeof (T) / sizeof (U))); - else - return U (t) * (sizeof (U) / sizeof (T)); + return t / static_cast (std::numeric_limits::max ()); } + +// float -> int +template +constexpr +typename std::enable_if< + std::is_floating_point::value && !std::is_floating_point::value, U +>::type +renormalise [[gnu::pure]] (T t) +{ + // Ideally std::ldexp would be involved but it complicates handing + // integers with greater precision than our floating point type. Also it + // would prohibit constexpr and involve errno. + + size_t usable = std::numeric_limits::digits; + size_t available = sizeof (U) * 8; + size_t shift = std::max (available, usable) - usable; + + t = limit (t, 0, 1); + + // construct an integer of the float's mantissa size, multiply it by our + // parameter, then shift it back into the full range of the integer type. + U in = std::numeric_limits::max () >> shift; + U mid = static_cast (t * in); + U out = mid << shift; + + // use the top bits of the output to fill the bottom bits which through + // shifting would otherwise be zero. this gives us the full extent of the + // integer range, while varying predictably through the entire output + // space. + return out | out >> (available - shift); +} + + +// float -> float, avoid identity conversion as we don't want to create +// ambiguous overloads +template +constexpr +typename std::enable_if< + std::is_floating_point::value && + std::is_floating_point::value && + !std::is_same::value, U +>::type +renormalise [[gnu::pure]] (T t) +{ + return static_cast (t); +} + + +// hi_int -> lo_int +template +constexpr +typename std::enable_if< + std::is_integral::value && + std::is_integral::value && + (sizeof (T) > sizeof (U)), U +>::type +renormalise [[gnu::pure]] (T t) +{ + // we have excess bits ,just shift and return + constexpr auto shift = 8 * (sizeof (T) - sizeof (U)); + return t >> shift; +} + + +// lo_int -> hi_int +template +constexpr +typename std::enable_if< + std::is_integral::value && + std::is_integral::value && + sizeof (T) < sizeof (U), U +>::type +renormalise [[gnu::pure]] (T t) +{ + // we need to create bits. fill the output integer with copies of ourself. + // this is approximately correct in the general case (introducing a small + // linear positive bias), but allows us to fill the output space in the + // case of input maximum. + + static_assert (sizeof (U) % sizeof (T) == 0, + "assumes integer multiple of sizes"); + + U out = 0; + + for (size_t i = 0; i < sizeof (U) / sizeof (T); ++i) + out |= t << sizeof (U) * 8 * i; + + return out; +} + + +template +constexpr +typename std::enable_if< + std::is_same::value, U +>::type +renormalise [[gnu::pure]] (T t) +{ return t; } + #include "maths.ipp" #endif // __MATHS_HPP diff --git a/test/maths.cpp b/test/maths.cpp index 341baacd..0d51ea68 100644 --- a/test/maths.cpp +++ b/test/maths.cpp @@ -77,24 +77,53 @@ test_normalisations (util::TAP::logger &tap) { bool success = true; - struct { + static const struct { float a; uint8_t b; } TESTS[] = { - { 1.f, 255 }, - { 0.f, 0 }, - { 2.f, 255 }, - { -1.f, 0 } + { 1.f, 255 }, + { 0.5f, 127 }, + { 2.f, 255 }, + { -1.f, 0 } }; for (auto i: TESTS) { - std::cerr << "x"; auto v = renormalise (i.a); success = success && almost_equal (unsigned{v}, unsigned{i.b}); } tap.expect (success, "float-u8 normalisation"); } + + // float to uint32 + // exercises an integer type that has more precision than float + { + bool success = true; + + static const struct { + float a; + uint32_t b; + } TESTS[] { + { 0.f, 0x00000000 }, // lo range + { 1.f, 0xffffffff }, // hi range + { 0.5f, 0x7fffff7f }, // 31 bits + { 0.001953125f, 0x007fff00 }, // 23 bits + }; + + for (auto t: TESTS) { + auto v = renormalise (t.a); + success = success && almost_equal (t.b, v); + } + + tap.expect (success, "float-u32 normalisation"); + } + + std::cerr << renormalise (0xff) << '\n'; + + tap.expect_eq (renormalise (0xff), 0xffffffff, "normalise hi u8-to-u32"); + tap.expect_eq (renormalise (0x00), 0x00000000, "normalise lo u8-to-u32"); + + tap.expect_eq (renormalise (0xffffffff), 0xff, "normalise hi u32-to-u8"); }