#define _LIBSPIRV_UTIL_HEX_FLOAT_H_
#include <cassert>
+#include <cctype>
#include <cmath>
#include <cstdint>
#include <iomanip>
"The number of bits do not fit");
};
+// Returns 4 bits represented by the hex character.
+inline uint8_t get_nibble_from_character(char character) {
+ const char* dec = "0123456789";
+ const char* lower = "abcdef";
+ const char* upper = "ABCDEF";
+ if (auto p = strchr(dec, character)) return p - dec;
+ if (auto p = strchr(lower, character)) return p - lower + 0xa;
+ if (auto p = strchr(upper, character)) return p - upper + 0xa;
+
+ assert(false && "This was called with a non-hex character");
+ return 0;
+}
+
// Outputs the given HexFloat to the stream.
template <typename T, typename Traits>
std::ostream& operator<<(std::ostream& os, const HexFloat<T, Traits>& value) {
os << "p" << std::dec << (int_exponent >= 0 ? "+" : "") << int_exponent;
return os;
}
+
+template <typename T, typename Traits>
+inline std::istream& ParseNormalFloat(
+ std::istream& is, bool negate_value, HexFloat<T, Traits>& value) {
+ T val;
+ is >> val;
+ if (negate_value) {
+ val = -val;
+ }
+ value.set_value(val);
+ return is;
+}
+
+// Reads a HexFloat from the given stream.
+// If the float is not encoded as a hex-float then it will be parsed
+// as a regular float.
+// This may fail if your stream does not support at least one unget.
+// Nan values can be encoded with "0x1.<not zero>p+exponent_bias".
+// This would normally overflow a float and round to
+// infinity but this special pattern is the exact representation for a NaN,
+// and therefore is actually encoded as the correct NaN. To encode inf,
+// either 0x0p+exponent_bias can be spcified or any exponent greater than
+// exponent_bias.
+// Examples using IEEE 32-bit float encoding.
+// 0x1.0p+128 (+inf)
+// -0x1.0p-128 (-inf)
+//
+// 0x1.1p+128 (+Nan)
+// -0x1.1p+128 (-Nan)
+//
+// 0x1p+129 (+inf)
+// -0x1p+129 (-inf)
+template <typename T, typename Traits>
+std::istream& operator>>(std::istream& is, HexFloat<T, Traits>& value) {
+ using HF = HexFloat<T, Traits>;
+ using uint_type = typename HF::uint_type;
+ using int_type = typename HF::int_type;
+
+ value.set_value(T(0));
+
+ if (is.flags() & std::ios::skipws) {
+ // If the user wants to skip whitespace , then we should obey that.
+ while (std::isspace(is.peek())) {
+ is.get();
+ }
+ }
+
+ char next_char = is.peek();
+ bool negate_value = false;
+
+ if (next_char != '-' && next_char != '0') {
+ return ParseNormalFloat(is, negate_value, value);
+ }
+
+ if (next_char == '-') {
+ negate_value = true;
+ is.get();
+ next_char = is.peek();
+ }
+
+ if (next_char == '0') {
+ is.get(); // We may have to unget this.
+ char maybe_hex_start = is.peek();
+ if (maybe_hex_start != 'x' && maybe_hex_start != 'X') {
+ is.unget();
+ return ParseNormalFloat(is, negate_value, value);
+ } else {
+ is.get(); // Throw away the 'x';
+ }
+ } else {
+ return ParseNormalFloat(is, negate_value, value);
+ }
+
+ // This "looks" like a hex-float so treat it as one.
+ bool seen_p = false;
+ bool seen_dot = false;
+ uint_type fraction_index = 0;
+
+ uint_type fraction = 0;
+ int_type exponent = HF::exponent_bias;
+
+ // Strip off leading zeros so we don't have to special-case them later.
+ while ((next_char = is.peek()) == '0') {
+ is.get();
+ }
+
+ bool is_denorm =
+ true; // Assume denorm "representation" until we hear otherwise.
+ // NB: This does not mean the value is actually denorm,
+ // it just means that it was written 0.
+ bool bits_written = false; // Stays false until we write a bit.
+ while (!seen_p && !seen_dot) {
+ // Handle characters that are left of the fractional part.
+ if (next_char == '.') {
+ seen_dot = true;
+ } else if (next_char == 'p') {
+ seen_p = true;
+ } else if (::isxdigit(next_char)) {
+ // We know this is not denormalized since we have stripped all leading
+ // zeroes and we are not a ".".
+ is_denorm = false;
+ uint8_t number = get_nibble_from_character(next_char);
+ for (int i = 0; i < 4; ++i, number <<= 1) {
+ uint_type write_bit = (number & 0x8) ? 0x1 : 0x0;
+ if (bits_written) {
+ // If we are here the bits represented belong in the fractional
+ // part of the float, and we have to adjust the exponent accordingly.
+ fraction |= write_bit << (HF::top_bit_left_shift - fraction_index++);
+ exponent += 1;
+ }
+ bits_written |= write_bit;
+ }
+ } else {
+ // We have not found our exponent yet, so we have to fail.
+ is.setstate(std::ios::failbit);
+ return is;
+ }
+ is.get();
+ next_char = is.peek();
+ }
+ bits_written = false;
+ while (seen_dot && !seen_p) {
+ // Handle only fractional parts now.
+ if (next_char == 'p') {
+ seen_p = true;
+ } else if (::isxdigit(next_char)) {
+ int number = get_nibble_from_character(next_char);
+ for (int i = 0; i < 4; ++i, number <<= 1) {
+ uint_type write_bit = (number & 0x8) ? 0x01 : 0x00;
+ bits_written |= write_bit;
+ if (is_denorm && !bits_written) {
+ // Handle modifying the exponent here this way we can handle
+ // an arbitrary number of hex values without overflowing our
+ // integer.
+ exponent -= 1;
+ } else {
+ fraction |= write_bit << (HF::top_bit_left_shift - fraction_index++);
+ }
+ }
+ } else {
+ // We still have not found our 'p' exponent yet, so this is not a valid
+ // hex-float.
+ is.setstate(std::ios::failbit);
+ return is;
+ }
+ is.get();
+ next_char = is.peek();
+ }
+
+ bool seen_sign = false;
+ int8_t exponent_sign = 1;
+ int_type written_exponent = 0;
+ while (true) {
+ if ((next_char == '-' || next_char == '+')) {
+ if (seen_sign) {
+ is.setstate(std::ios::failbit);
+ return is;
+ }
+ seen_sign = true;
+ exponent_sign = (next_char == '-') ? -1 : 1;
+ } else if (::isdigit(next_char)) {
+ // Hex-floats express their exponent as decimal.
+ written_exponent *= 10;
+ written_exponent += next_char - '0';
+ } else {
+ break;
+ }
+ is.get();
+ next_char = is.peek();
+ }
+
+ written_exponent *= exponent_sign;
+ exponent += written_exponent;
+
+ bool is_zero = is_denorm && (fraction == 0);
+ if (is_denorm && !is_zero) {
+ fraction <<= 1;
+ exponent -= 1;
+ } else if (is_zero) {
+ exponent = 0;
+ }
+
+ if (exponent <= 0 && !is_zero) {
+ fraction >>= 1;
+ fraction |= static_cast<uint_type>(1) << HF::top_bit_left_shift;
+ }
+
+ fraction = (fraction >> HF::fraction_right_shift) & HF::fraction_encode_mask;
+
+ const uint_type max_exponent =
+ SetBits<uint_type, 0, HF::num_exponent_bits>::get;
+
+ // Handle actual denorm numbers
+ while (exponent < 0 && !is_zero) {
+ fraction >>= 1;
+ exponent += 1;
+
+ fraction &= HF::fraction_encode_mask;
+ if (fraction == 0) {
+ // We have underflowed our fraction. We should clamp to zero.
+ is_zero = true;
+ exponent = 0;
+ }
+ }
+
+ // We have overflowed so we should be inf/-inf.
+ if (exponent > max_exponent) {
+ exponent = max_exponent;
+ fraction = 0;
+ }
+
+ uint_type output_bits = static_cast<uint_type>(negate_value ? 1 : 0)
+ << HF::top_bit_left_shift;
+ output_bits |= fraction;
+ output_bits |= (exponent << HF::exponent_left_shift) & HF::exponent_mask;
+
+ T output_float = spvutils::BitwiseCast<T>(output_bits);
+ value.set_value(output_float);
+
+ return is;
+}
}
#endif // _LIBSPIRV_UTIL_HEX_FLOAT_H_
namespace {
using ::testing::Eq;
+using spvutils::BitwiseCast;
-using HexFloatEncodeTest =
+using HexFloatTest =
::testing::TestWithParam<std::pair<float, std::string>>;
+using DecodeHexFloatTest =
+ ::testing::TestWithParam<std::pair<std::string, float>>;
-TEST_P(HexFloatEncodeTest, EncodeCorrectly) {
+TEST_P(HexFloatTest, EncodeCorrectly) {
std::stringstream ss;
ss << spvutils::HexFloat<float>(std::get<0>(GetParam()));
EXPECT_THAT(ss.str(), Eq(std::get<1>(GetParam())));
}
+TEST_P(HexFloatTest, DecodeCorrectly) {
+ std::stringstream ss(std::get<1>(GetParam()));
+ spvutils::HexFloat<float> myFloat(0.f);
+ ss >> myFloat;
+ float expected = std::get<0>(GetParam());
+ // Check that the two floats are bitwise equal. We do it this way because
+ // nan != nan, and so the test would fail even if they were exactly the same.
+ EXPECT_THAT(BitwiseCast<uint32_t>(myFloat.value()),
+ Eq(BitwiseCast<uint32_t>(std::get<0>(GetParam()))));
+}
+
INSTANTIATE_TEST_CASE_P(
- Float32Tests, HexFloatEncodeTest,
+ Float32Tests, HexFloatTest,
::testing::ValuesIn(std::vector<std::pair<float, std::string>>({
{0.f, "0x0p+0"},
{1.f, "0x1p+0"},
{1.0f / -1024.f - 1.0f / 8.f, "-0x1.02p-3"},
// lowest non-denorm
- {1.0 / (powf(2.0f, 126.0f)), "0x1p-126"},
- {-1.0 / (powf(2.0f, 126.0f)), "-0x1p-126"},
+ {ldexp(1.0f, -126), "0x1p-126"},
+ {ldexp(-1.0f, -126), "-0x1p-126"},
// Denormalized values
- {1.0 / (powf(2.0f, 127.0f)), "0x1p-127"},
- {(1.0 / (powf(2.0f, 127.0f))) / 2.0f, "0x1p-128"},
- {(1.0 / (powf(2.0f, 127.0f))) / 4.0f, "0x1p-129"},
- {(1.0 / (powf(2.0f, 127.0f))) / 8.0f, "0x1p-130"},
- {-1.0 / (powf(2.0f, 127.0f)), "-0x1p-127"},
- {(-1.0 / (powf(2.0f, 127.0f))) / 2.0f, "-0x1p-128"},
- {(-1.0 / (powf(2.0f, 127.0f))) / 4.0f, "-0x1p-129"},
- {(-1.0 / (powf(2.0f, 127.0f))) / 8.0f, "-0x1p-130"},
-
- {(1.0 / (powf(2.0f, 127.0f))) +
- ((1.0 / (powf(2.0f, 127.0f))) / 2.0f), "0x1.8p-127"},
- {(1.0 / (powf(2.0f, 127.0f)) / 2.0f) +
- ((1.0 / (powf(2.0f, 127.0f))) / 4.0f), "0x1.8p-128"},
-
+ {ldexp(1.0f, -127), "0x1p-127"},
+ {ldexp(1.0f, -127) / 2.0f, "0x1p-128"},
+ {ldexp(1.0f, -127) / 4.0f, "0x1p-129"},
+ {ldexp(1.0f, -127) / 8.0f, "0x1p-130"},
+ {ldexp(-1.0f, -127), "-0x1p-127"},
+ {ldexp(-1.0f, -127) / 2.0f, "-0x1p-128"},
+ {ldexp(-1.0f, -127) / 4.0f, "-0x1p-129"},
+ {ldexp(-1.0f, -127) / 8.0f, "-0x1p-130"},
+
+ {ldexp(1.0, -127) + (ldexp(1.0, -127) / 2.0f), "0x1.8p-127"},
+ {ldexp(1.0, -127) / 2.0 + (ldexp(1.0, -127) / 4.0f), "0x1.8p-128"},
// Various NAN and INF cases
- {spvutils::BitwiseCast<float>(0xFF800000), "-0x1p+128"}, // -inf
- {spvutils::BitwiseCast<float>(0x7F800000), "0x1p+128"}, // inf
- {spvutils::BitwiseCast<float>(0xFF800000), "-0x1p+128"}, // -nan
- {spvutils::BitwiseCast<float>(0xFF800100), "-0x1.0002p+128"}, // -nan
- {spvutils::BitwiseCast<float>(0xFF800c00), "-0x1.0018p+128"}, // -nan
- {spvutils::BitwiseCast<float>(0xFF80F000), "-0x1.01ep+128"}, // -nan
- {spvutils::BitwiseCast<float>(0xFFFFFFFF), "-0x1.fffffep+128"}, // -nan
- {spvutils::BitwiseCast<float>(0x7F800000), "0x1p+128"}, // +nan
- {spvutils::BitwiseCast<float>(0x7F800100), "0x1.0002p+128"}, // +nan
- {spvutils::BitwiseCast<float>(0x7F800c00), "0x1.0018p+128"}, // +nan
- {spvutils::BitwiseCast<float>(0x7F80F000), "0x1.01ep+128"}, // +nan
- {spvutils::BitwiseCast<float>(0x7FFFFFFF), "0x1.fffffep+128"}, // +nan
+ {BitwiseCast<float>(0xFF800000), "-0x1p+128"}, // -inf
+ {BitwiseCast<float>(0x7F800000), "0x1p+128"}, // inf
+ {BitwiseCast<float>(0xFFC00000), "-0x1.8p+128"}, // -nan
+ {BitwiseCast<float>(0xFF800100), "-0x1.0002p+128"}, // -nan
+ {BitwiseCast<float>(0xFF800c00), "-0x1.0018p+128"}, // -nan
+ {BitwiseCast<float>(0xFF80F000), "-0x1.01ep+128"}, // -nan
+ {BitwiseCast<float>(0xFFFFFFFF), "-0x1.fffffep+128"}, // -nan
+ {BitwiseCast<float>(0x7FC00000), "0x1.8p+128"}, // +nan
+ {BitwiseCast<float>(0x7F800100), "0x1.0002p+128"}, // +nan
+ {BitwiseCast<float>(0x7f800c00), "0x1.0018p+128"}, // +nan
+ {BitwiseCast<float>(0x7F80F000), "0x1.01ep+128"}, // +nan
+ {BitwiseCast<float>(0x7FFFFFFF), "0x1.fffffep+128"}, // +nan
})));
+TEST_P(DecodeHexFloatTest, DecodeCorrectly) {
+ std::stringstream ss(std::get<0>(GetParam()));
+ spvutils::HexFloat<float> myFloat(0.f);
+ ss >> myFloat;
+ float expected = std::get<1>(GetParam());
+ // Check that the two floats are bitwise equal. We do it this way because
+ // nan != nan, and so the test would fail even if they were exactly the same.
+ EXPECT_THAT(BitwiseCast<uint32_t>(myFloat.value()),
+ Eq(BitwiseCast<uint32_t>(std::get<1>(GetParam()))));
+}
+
+INSTANTIATE_TEST_CASE_P(
+ Float32DecodeTests, DecodeHexFloatTest,
+ ::testing::ValuesIn(std::vector<std::pair<std::string, float>>({
+ {"0x0p+000", 0.f},
+ {"0x0p0", 0.f},
+ {"0x0p-0", 0.f},
+
+ // inf cases
+ {"-0x1p+128", BitwiseCast<float>(0xFF800000)}, // -inf
+ {"0x32p+127", BitwiseCast<float>(0x7F800000)}, // inf
+ {"0x32p+500", BitwiseCast<float>(0x7F800000)}, // inf
+ {"-0x32p+127", BitwiseCast<float>(0xFF800000)}, // -inf
+
+ // flush to zero cases
+ {"0x1p-500", 0.f}, // Exponent underflows.
+ {"-0x1p-500", -0.f},
+ {"0x0.00000000001p-126", 0.f}, // Fraction causes underfloat.
+ {"-0x0.0000000001p-127", -0.f},
+ {"-0x0.01p-142", -0.f}, // Fraction causes undeflow to underflow more.
+ {"0x0.01p-142", 0.f},
+
+ // Some floats that do not encode the same way as the decode.
+ {"0x2p+0", 2.f},
+ {"0xFFp+0", 255.f},
+ {"0x0.8p+0", 0.5f},
+ {"0x0.4p+0", 0.25f},
+ })));
+// TODO(awoloszyn): Add some more decoding tests with "abnormal" formats.
// TODO(awoloszyn): Add double tests
// TODO(awoloszyn): Add fp16 tests and HexFloatTraits.