util/half: Add double_to_half tests

These currently fail because our double to half rounding is wrong.

Reviewed-by: Erik Faye-Lund <erik.faye-lund@collabora.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/41295>
This commit is contained in:
Faith Ekstrand 2026-05-05 18:01:50 -04:00 committed by Marge Bot
parent e00a841a9f
commit f23d183b7d
2 changed files with 173 additions and 0 deletions

View file

@ -33,6 +33,7 @@
#include "util/detect_cc.h"
#include "util/double.h"
#include "util/u_cpu_detect.h"
#include "util/u_math.h"
#if DETECT_ARCH_X86_64
#include <xmmintrin.h>

View file

@ -21,6 +21,7 @@
* IN THE SOFTWARE.
*/
#include <inttypes.h>
#include <math.h>
#include <gtest/gtest.h>
@ -367,3 +368,174 @@ TEST(float_to_float16_rd, rounding)
{
test_float_to_half_rounding(_mesa_float_to_float16_rd, RD);
}
static void
test_double_to_half_limits(uint16_t (*func)(double))
{
/* Positive and negative 0. */
EXPECT_EQ(func(0.0f), 0);
EXPECT_EQ(func(-0.0f), 0x8000);
/* Max normal number */
EXPECT_EQ(func(65504.0f), 0x7bff);
uint16_t nan = func(TEST_NAN);
EXPECT_EQ((nan & 0xfc00), 0x7c00); /* exponent is all 1s */
EXPECT_TRUE(nan & (1 << 9)); /* mantissa is quiet nan */
EXPECT_EQ(func(TEST_POS_INF), HALF_POS_INF);
EXPECT_EQ(func(TEST_NEG_INF), HALF_NEG_INF);
}
TEST(double_to_float16_rtne, limits)
{
test_double_to_half_limits(_mesa_double_to_float16_rtne);
}
TEST(double_to_float16_rtz, limits)
{
test_double_to_half_limits(_mesa_double_to_float16_rtz);
}
static void
test_double_to_half_roundtrip(uint16_t (*func)(double))
{
unsigned i;
unsigned roundtrip_fails = 0;
for(i = 0; i < 1 << 16; ++i)
{
uint16_t h = (uint16_t) i;
union di d;
uint16_t rh;
d.d = _mesa_half_to_float(h);
rh = func(d.d);
if (h != rh && !(util_is_half_nan(h) && util_is_half_nan(rh))) {
printf("Roundtrip failed: %x -> %" PRIx64 " = %f -> %x\n",
h, d.ui, d.d, rh);
++roundtrip_fails;
}
}
EXPECT_EQ(roundtrip_fails, 0);
}
TEST(double_to_float16_rtne, roundtrip)
{
test_double_to_half_roundtrip(_mesa_double_to_float16_rtne);
}
TEST(double_to_float16_rtz, roundtrip)
{
test_double_to_half_roundtrip(_mesa_double_to_float16_rtz);
}
static uint64_t
rand_m_low42(unsigned i)
{
uint32_t r0 = rand_u32(rand_u32(i));
uint32_t r1 = rand_u32(r0);
uint64_t low28 = r0 & BITFIELD_MASK(28);
uint64_t m_key = r1 & BITFIELD_MASK(6);
uint64_t mid11 = (r1 >> 6) & BITFIELD_MASK(11);
/* Generate the tie bits manually so they flip at a high rate. We
* especially want to smash bits 28 and 29 since those are what affect
* how a conversion from double to float rounds.
*/
uint64_t tf = (m_key >> 0) & 0x3;
uint64_t th = (m_key >> 2) & 1;
/* Smash the low 28 bits to 0 every so often */
if ((m_key >> 3) & 1)
low28 = 0;
/* Smash the middle 11 bits to 0 or ~0 every so often */
if (((m_key >> 4) & 0x3) == 0)
mid11 = 0;
else if (((m_key >> 4) & 0x3) == 1)
mid11 = BITFIELD_MASK(11);
return low28 | (tf << 28) | (mid11 << 30) | th << 41;
}
static void
test_double_to_half_rounding(uint16_t (*func)(double),
enum rounding_mode rounding)
{
for (unsigned i = 0; i < 1024; ++i) {
const uint16_t h_rtz = rand_half(i);
const bool is_neg = h_rtz & BITFIELD_BIT(15);
/* Generate a double */
union di d;
d.d = _mesa_half_to_float(h_rtz);
EXPECT_EQ(d.ui & BITFIELD64_MASK(42), 0);
/* For an exactly representable value, we should get h_rtz back */
EXPECT_EQ(func(d.d), h_rtz);
/* Generate a random non-zero low 42 bits */
const uint64_t m_low42 = rand_m_low42(i);
if (m_low42 == 0)
continue;
if (h_rtz & 0x7c00) {
d.ui |= m_low42;
} else {
/* For zero or denormal, we can't just OR in our low bits */
double delta = ldexp(m_low42, -(42 + 10 + 14));
if (is_neg)
delta = -delta;
d.d += delta;
}
uint16_t h_expected;
switch (rounding) {
case RTNE:
if (m_low42 & BITFIELD64_MASK(41)) {
/* It's not a tie */
if (m_low42 & BITFIELD64_BIT(41))
h_expected = next_half(h_rtz);
else
h_expected = h_rtz;
} else {
/* It's a tie because we know m_low42 != 0, round towards even */
assert(m_low42 & BITFIELD64_BIT(41));
if (h_rtz & 1)
h_expected = next_half(h_rtz);
else
h_expected = h_rtz;
}
break;
case RTZ:
h_expected = h_rtz;
break;
case RU:
h_expected = is_neg ? h_rtz : next_half(h_rtz);
break;
case RD:
h_expected = is_neg ? next_half(h_rtz) : h_rtz;
break;
}
uint16_t h_actual = func(d.d);
EXPECT_EQ(h_actual, h_expected);
}
}
TEST(double_to_float16_rtne, rounding)
{
test_double_to_half_rounding(_mesa_double_to_float16_rtne, RTNE);
}
TEST(double_to_float16_rtz, rounding)
{
test_double_to_half_rounding(_mesa_double_to_float16_rtz, RTZ);
}