mirror of
https://github.com/KhronosGroup/SPIRV-Tools
synced 2024-10-18 11:10:05 +00:00
Added HexFloat reading to HexFloat.
This allows reading of hex-encoded floats.
This commit is contained in:
parent
c2887f98bc
commit
0b61c593a2
@ -28,6 +28,7 @@
|
||||
#define _LIBSPIRV_UTIL_HEX_FLOAT_H_
|
||||
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
@ -151,6 +152,19 @@ class HexFloat {
|
||||
"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) {
|
||||
@ -217,6 +231,227 @@ 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_
|
||||
|
@ -35,18 +35,32 @@
|
||||
|
||||
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"},
|
||||
@ -78,40 +92,76 @@ INSTANTIATE_TEST_CASE_P(
|
||||
{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.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user