wsgl-writer: emit inf, nan, subnormal as hex float

Signed zeros are emitted.
Subormal numbers are emitted as hex float.

Handling Inf and NaN is unresolved in the spec.
See https://github.com/gpuweb/gpuweb/issues/1769

NaN tests are disabled, due to platform dependence.
Windows x86-64 seems to always set the high mantissa bit
on NaNs.

Fixed: tint:76
Change-Id: I06c69b93b04c869a63ec2f574022996acc7a6dbe
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/52321
Commit-Queue: David Neto <dneto@google.com>
Auto-Submit: David Neto <dneto@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
This commit is contained in:
David Neto 2021-05-27 20:46:46 +00:00 committed by Tint LUCI CQ
parent 0131ce2205
commit d5f4ea22f0
7 changed files with 393 additions and 2 deletions

View File

@ -789,6 +789,7 @@ if(${TINT_BUILD_TESTS})
writer/wgsl/generator_impl_identifier_test.cc
writer/wgsl/generator_impl_if_test.cc
writer/wgsl/generator_impl_loop_test.cc
writer/wgsl/generator_impl_literal_test.cc
writer/wgsl/generator_impl_member_accessor_test.cc
writer/wgsl/generator_impl_return_test.cc
writer/wgsl/generator_impl_switch_test.cc

View File

@ -14,9 +14,14 @@
#include "src/writer/float_to_string.h"
#include <cmath>
#include <cstring>
#include <iomanip>
#include <limits>
#include <sstream>
#include "src/debug.h"
namespace tint {
namespace writer {
@ -33,5 +38,105 @@ std::string FloatToString(float f) {
return str;
}
std::string FloatToBitPreservingString(float f) {
// For the NaN case, avoid handling the number as a floating point value.
// Some machines will modify the top bit in the mantissa of a NaN.
std::stringstream ss;
uint32_t float_bits = 0u;
std::memcpy(&float_bits, &f, sizeof(float_bits));
// Handle the sign.
const uint32_t kSignMask = 1u << 31;
if (float_bits & kSignMask) {
// If `f` is -0.0 print -0.0.
ss << '-';
// Strip sign bit.
float_bits = float_bits & (~kSignMask);
}
switch (std::fpclassify(f)) {
case FP_ZERO:
case FP_NORMAL:
std::memcpy(&f, &float_bits, sizeof(float_bits));
ss << FloatToString(f);
break;
default: {
// Infinity, NaN, and Subnormal
// TODO(dneto): It's unclear how Infinity and NaN should be handled.
// See https://github.com/gpuweb/gpuweb/issues/1769
// std::hexfloat prints 'nan' and 'inf' instead of an
// explicit representation like we want. Split it out
// manually.
const int kExponentBias = 127;
const int kExponentMask = 0x7f800000;
const int kMantissaMask = 0x007fffff;
const int kMantissaBits = 23;
int mantissaNibbles = (kMantissaBits + 3) / 4;
const int biased_exponent =
static_cast<int>((float_bits & kExponentMask) >> kMantissaBits);
int exponent = biased_exponent - kExponentBias;
uint32_t mantissa = float_bits & kMantissaMask;
ss << "0x";
if (exponent == 128) {
if (mantissa == 0) {
// Infinity case.
ss << "1p+128";
} else {
// NaN case.
// Emit the mantissa bits as if they are left-justified after the
// binary point. This is what SPIRV-Tools hex float emitter does,
// and it's a justifiable choice independent of the bit width
// of the mantissa.
mantissa <<= (4 - (kMantissaBits % 4));
// Remove trailing zeroes, for tidyness.
while (0 == (0xf & mantissa)) {
mantissa >>= 4;
mantissaNibbles--;
}
ss << "1." << std::hex << std::setfill('0')
<< std::setw(mantissaNibbles) << mantissa << "p+128";
}
} else {
// Subnormal, and not zero.
TINT_ASSERT(mantissa != 0);
const int kTopBit = (1 << kMantissaBits);
// Shift left until we get 1.x
while (0 == (kTopBit & mantissa)) {
mantissa <<= 1;
exponent--;
}
// Emit the leading 1, and remove it from the mantissa.
ss << "1";
mantissa = mantissa ^ kTopBit;
mantissa <<= 1;
exponent++;
// Emit the fractional part.
if (mantissa) {
// Remove trailing zeroes, for tidyness
while (0 == (0xf & mantissa)) {
mantissa >>= 4;
mantissaNibbles--;
}
ss << "." << std::hex << std::setfill('0')
<< std::setw(mantissaNibbles) << mantissa;
}
// Emit the exponent
ss << "p" << std::showpos << std::dec << exponent;
}
}
}
return ss.str();
}
} // namespace writer
} // namespace tint

View File

@ -28,6 +28,11 @@ namespace writer {
/// @return the float f formatted to a string
std::string FloatToString(float f);
/// Converts the float `f` to a string, using hex float notation for infinities,
/// NaNs, or subnormal numbers. Otherwise behaves as FloatToString.
/// @return the float f formatted to a string
std::string FloatToBitPreservingString(float f);
} // namespace writer
} // namespace tint

View File

@ -14,6 +14,8 @@
#include "src/writer/float_to_string.h"
#include <cmath>
#include <cstring>
#include <limits>
#include "gtest/gtest.h"
@ -22,6 +24,25 @@ namespace tint {
namespace writer {
namespace {
// Makes an IEEE 754 binary32 floating point number with
// - 0 sign if sign is 0, 1 otherwise
// - 'exponent_bits' is placed in the exponent space.
// So, the exponent bias must already be included.
float MakeFloat(int sign, int biased_exponent, int mantissa) {
const uint32_t sign_bit = sign ? 0x80000000u : 0u;
// The binary32 exponent is 8 bits, just below the sign.
const uint32_t exponent_bits = (biased_exponent & 0xffu) << 23;
// The mantissa is the bottom 23 bits.
const uint32_t mantissa_bits = (mantissa & 0x7fffffu);
uint32_t bits = sign_bit | exponent_bits | mantissa_bits;
float result = 0.0f;
static_assert(sizeof(result) == sizeof(bits),
"expected float and uint32_t to be the same size");
std::memcpy(&result, &bits, sizeof(bits));
return result;
}
TEST(FloatToStringTest, Zero) {
EXPECT_EQ(FloatToString(0.0f), "0.0");
}
@ -67,6 +88,132 @@ TEST(FloatToStringTest, Lowest) {
"-340282346638528859811704183484516925440.0");
}
// FloatToBitPreservingString
//
// First replicate the tests for FloatToString
TEST(FloatToBitPreservingStringTest, Zero) {
EXPECT_EQ(FloatToBitPreservingString(0.0f), "0.0");
}
TEST(FloatToBitPreservingStringTest, One) {
EXPECT_EQ(FloatToBitPreservingString(1.0f), "1.0");
}
TEST(FloatToBitPreservingStringTest, MinusOne) {
EXPECT_EQ(FloatToBitPreservingString(-1.0f), "-1.0");
}
TEST(FloatToBitPreservingStringTest, Billion) {
EXPECT_EQ(FloatToBitPreservingString(1e9f), "1000000000.0");
}
TEST(FloatToBitPreservingStringTest, Small) {
EXPECT_NE(FloatToBitPreservingString(std::numeric_limits<float>::epsilon()),
"0.0");
}
TEST(FloatToBitPreservingStringTest, Highest) {
const auto highest = std::numeric_limits<float>::max();
const auto expected_highest = 340282346638528859811704183484516925440.0f;
if (highest < expected_highest || highest > expected_highest) {
GTEST_SKIP() << "std::numeric_limits<float>::max() is not as expected for "
"this target";
}
EXPECT_EQ(FloatToBitPreservingString(std::numeric_limits<float>::max()),
"340282346638528859811704183484516925440.0");
}
TEST(FloatToBitPreservingStringTest, Lowest) {
// Some compilers complain if you test floating point numbers for equality.
// So say it via two inequalities.
const auto lowest = std::numeric_limits<float>::lowest();
const auto expected_lowest = -340282346638528859811704183484516925440.0f;
if (lowest < expected_lowest || lowest > expected_lowest) {
GTEST_SKIP()
<< "std::numeric_limits<float>::lowest() is not as expected for "
"this target";
}
EXPECT_EQ(FloatToBitPreservingString(std::numeric_limits<float>::lowest()),
"-340282346638528859811704183484516925440.0");
}
// Special cases for bit-preserving output.
TEST(FloatToBitPreservingStringTest, NegativeZero) {
EXPECT_EQ(FloatToBitPreservingString(std::copysign(0.0f, -5.0f)), "-0.0");
}
TEST(FloatToBitPreservingStringTest, ZeroAsBits) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0)), "0.0");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0)), "-0.0");
}
TEST(FloatToBitPreservingStringTest, OneBits) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 127, 0)), "1.0");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 127, 0)), "-1.0");
}
TEST(FloatToBitPreservingStringTest, SmallestDenormal) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 1)), "0x1p-149");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 1)), "-0x1p-149");
}
TEST(FloatToBitPreservingStringTest, BiggerDenormal) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 2)), "0x1p-148");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 2)), "-0x1p-148");
}
TEST(FloatToBitPreservingStringTest, LargestDenormal) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0x7fffff)),
"0x1.fffffcp-127");
}
TEST(FloatToBitPreservingStringTest, Subnormal_cafebe) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0xcafebe)),
"0x1.2bfaf8p-127");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0xcafebe)),
"-0x1.2bfaf8p-127");
}
TEST(FloatToBitPreservingStringTest, Subnormal_aaaaa) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0xaaaaa)),
"0x1.55554p-130");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0xaaaaa)),
"-0x1.55554p-130");
}
TEST(FloatToBitPreservingStringTest, Infinity) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0)), "0x1p+128");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0)), "-0x1p+128");
}
// TODO(dneto): It's unclear how Infinity and NaN should be handled.
// https://github.com/gpuweb/gpuweb/issues/1769
// Windows x86-64 sets the high mantissa bit on NaNs.
// Disable NaN tests for now.
TEST(FloatToBitPreservingStringTest, DISABLED_NaN_MsbOnly) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0x400000)),
"0x1.8p+128");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0x400000)),
"-0x1.8p+128");
}
TEST(FloatToBitPreservingStringTest, DISABLED_NaN_LsbOnly) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0x1)),
"0x1.000002p+128");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0x1)),
"-0x1.000002p+128");
}
TEST(FloatToBitPreservingStringTest, DISABLED_NaN_NonMsb) {
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0x20101f)),
"0x1.40203ep+128");
EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0x20101f)),
"-0x1.40203ep+128");
}
} // namespace
} // namespace writer
} // namespace tint

View File

@ -253,7 +253,7 @@ bool GeneratorImpl::EmitLiteral(ast::Literal* lit) {
if (auto* bl = lit->As<ast::BoolLiteral>()) {
out_ << (bl->IsTrue() ? "true" : "false");
} else if (auto* fl = lit->As<ast::FloatLiteral>()) {
out_ << FloatToString(fl->value());
out_ << FloatToBitPreservingString(fl->value());
} else if (auto* sl = lit->As<ast::SintLiteral>()) {
out_ << sl->value();
} else if (auto* ul = lit->As<ast::UintLiteral>()) {

View File

@ -0,0 +1,132 @@
// Copyright 2020 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <cstring>
#include "src/ast/override_decoration.h"
#include "src/writer/wgsl/test_helper.h"
namespace tint {
namespace writer {
namespace wgsl {
namespace {
// Makes an IEEE 754 binary32 floating point number with
// - 0 sign if sign is 0, 1 otherwise
// - 'exponent_bits' is placed in the exponent space.
// So, the exponent bias must already be included.
float MakeFloat(int sign, int biased_exponent, int mantissa) {
const uint32_t sign_bit = sign ? 0x80000000u : 0u;
// The binary32 exponent is 8 bits, just below the sign.
const uint32_t exponent_bits = (biased_exponent & 0xffu) << 23;
// The mantissa is the bottom 23 bits.
const uint32_t mantissa_bits = (mantissa & 0x7fffffu);
uint32_t bits = sign_bit | exponent_bits | mantissa_bits;
float result = 0.0f;
static_assert(sizeof(result) == sizeof(bits),
"expected float and uint32_t to be the same size");
std::memcpy(&result, &bits, sizeof(bits));
return result;
}
struct FloatData {
float value;
std::string expected;
};
inline std::ostream& operator<<(std::ostream& out, FloatData data) {
out << "{" << data.value << "," << data.expected << "}";
return out;
}
using WgslGenerator_FloatLiteralTest = TestParamHelper<FloatData>;
TEST_P(WgslGenerator_FloatLiteralTest, Emit) {
auto* v = Expr(GetParam().value);
SetResolveOnBuild(false);
GeneratorImpl& gen = Build();
ASSERT_TRUE(gen.EmitScalarConstructor(v)) << gen.error();
EXPECT_EQ(gen.result(), GetParam().expected);
}
INSTANTIATE_TEST_SUITE_P(Zero,
WgslGenerator_FloatLiteralTest,
::testing::ValuesIn(std::vector<FloatData>{
{0.0f, "0.0"},
{MakeFloat(0, 0, 0), "0.0"},
{MakeFloat(1, 0, 0), "-0.0"}}));
INSTANTIATE_TEST_SUITE_P(Normal,
WgslGenerator_FloatLiteralTest,
::testing::ValuesIn(std::vector<FloatData>{
{1.0f, "1.0"},
{-1.0f, "-1.0"},
{101.375, "101.375"}}));
INSTANTIATE_TEST_SUITE_P(
Subnormal,
WgslGenerator_FloatLiteralTest,
::testing::ValuesIn(std::vector<FloatData>{
{MakeFloat(0, 0, 1), "0x1p-149"}, // Smallest
{MakeFloat(1, 0, 1), "-0x1p-149"},
{MakeFloat(0, 0, 2), "0x1p-148"},
{MakeFloat(1, 0, 2), "-0x1p-148"},
{MakeFloat(0, 0, 0x7fffff), "0x1.fffffcp-127"}, // Largest
{MakeFloat(1, 0, 0x7fffff), "-0x1.fffffcp-127"}, // Largest
{MakeFloat(0, 0, 0xcafebe), "0x1.2bfaf8p-127"}, // Scattered bits
{MakeFloat(1, 0, 0xcafebe), "-0x1.2bfaf8p-127"}, // Scattered bits
{MakeFloat(0, 0, 0xaaaaa), "0x1.55554p-130"}, // Scattered bits
{MakeFloat(1, 0, 0xaaaaa), "-0x1.55554p-130"}, // Scattered bits
}));
INSTANTIATE_TEST_SUITE_P(Infinity,
WgslGenerator_FloatLiteralTest,
::testing::ValuesIn(std::vector<FloatData>{
{MakeFloat(0, 255, 0), "0x1p+128"},
{MakeFloat(1, 255, 0), "-0x1p+128"}}));
INSTANTIATE_TEST_SUITE_P(
// TODO(dneto): It's unclear how Infinity and NaN should be handled.
// https://github.com/gpuweb/gpuweb/issues/1769
// This test fails on Windows x86-64 because the machine sets the high
// mantissa bit on NaNs.
DISABLED_NaN,
// In the NaN case, the top bit in the mantissa is often used to encode
// whether the NaN is signalling or quiet, but no agreement between
// different machine architectures on whether 1 means signalling or
// if 1 means quiet.
WgslGenerator_FloatLiteralTest,
::testing::ValuesIn(std::vector<FloatData>{
// LSB only. Smallest mantissa.
{MakeFloat(0, 255, 1), "0x1.000002p+128"}, // Smallest mantissa
{MakeFloat(1, 255, 1), "-0x1.000002p+128"},
// MSB only.
{MakeFloat(0, 255, 0x400000), "0x1.8p+128"},
{MakeFloat(1, 255, 0x400000), "-0x1.8p+128"},
// All 1s in the mantissa.
{MakeFloat(0, 255, 0x7fffff), "0x1.fffffep+128"},
{MakeFloat(1, 255, 0x7fffff), "-0x1.fffffep+128"},
// Scattered bits, with 0 in top mantissa bit.
{MakeFloat(0, 255, 0x20101f), "0x1.40203ep+128"},
{MakeFloat(1, 255, 0x20101f), "-0x1.40203ep+128"},
// Scattered bits, with 1 in top mantissa bit.
{MakeFloat(0, 255, 0x40101f), "0x1.80203ep+128"},
{MakeFloat(1, 255, 0x40101f), "-0x1.80203ep+128"}}));
} // namespace
} // namespace wgsl
} // namespace writer
} // namespace tint

View File

@ -258,9 +258,9 @@ tint_unittests_source_set("tint_unittests_core_src") {
"../src/resolver/intrinsic_test.cc",
"../src/resolver/is_host_shareable_test.cc",
"../src/resolver/is_storeable_test.cc",
"../src/resolver/pipeline_overridable_constant_test.cc",
"../src/resolver/ptr_ref_test.cc",
"../src/resolver/ptr_ref_validation_test.cc",
"../src/resolver/pipeline_overridable_constant_test.cc",
"../src/resolver/resolver_test.cc",
"../src/resolver/resolver_test_helper.cc",
"../src/resolver/resolver_test_helper.h",
@ -511,6 +511,7 @@ tint_unittests_source_set("tint_unittests_wgsl_writer_src") {
"../src/writer/wgsl/generator_impl_global_decl_test.cc",
"../src/writer/wgsl/generator_impl_identifier_test.cc",
"../src/writer/wgsl/generator_impl_if_test.cc",
"../src/writer/wgsl/generator_impl_literal_test.cc",
"../src/writer/wgsl/generator_impl_loop_test.cc",
"../src/writer/wgsl/generator_impl_member_accessor_test.cc",
"../src/writer/wgsl/generator_impl_return_test.cc",