diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index e2517f7ca..9e4725881 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -1347,19 +1347,19 @@ void GenericCAO::processMessage(const std::string &data) } else if (cmd == GENERIC_CMD_UPDATE_POSITION) { // Not sent by the server if this object is an attachment. // We might however get here if the server notices the object being detached before the client. - m_position = readV3F1000(is); - m_velocity = readV3F1000(is); - m_acceleration = readV3F1000(is); + m_position = readV3F32(is); + m_velocity = readV3F32(is); + m_acceleration = readV3F32(is); if (std::fabs(m_prop.automatic_rotate) < 0.001f) - m_rotation = readV3F1000(is); + m_rotation = readV3F32(is); else - readV3F1000(is); + readV3F32(is); m_rotation = wrapDegrees_0_360_v3f(m_rotation); bool do_interpolate = readU8(is); bool is_end_position = readU8(is); - float update_interval = readF1000(is); + float update_interval = readF32(is); // Place us a bit higher if we're physical, to not sink into // the ground due to sucky collision detection... diff --git a/src/genericobject.cpp b/src/genericobject.cpp index ba5281093..ceec222aa 100644 --- a/src/genericobject.cpp +++ b/src/genericobject.cpp @@ -49,19 +49,19 @@ std::string gob_cmd_update_position( // command writeU8(os, GENERIC_CMD_UPDATE_POSITION); // pos - writeV3F1000(os, position); + writeV3F32(os, position); // velocity - writeV3F1000(os, velocity); + writeV3F32(os, velocity); // acceleration - writeV3F1000(os, acceleration); + writeV3F32(os, acceleration); // rotation - writeV3F1000(os, rotation); + writeV3F32(os, rotation); // do_interpolate writeU8(os, do_interpolate); // is_end_position (for interpolation) writeU8(os, is_movement_end); // update_interval (for interpolation) - writeF1000(os, update_interval); + writeF32(os, update_interval); return os.str(); } diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index 12a91e647..0cf77b8c5 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -191,6 +191,7 @@ with this program; if not, write to the Free Software Foundation, Inc., PROTOCOL VERSION 37: Redo detached inventory sending Add TOCLIENT_NODEMETA_CHANGED + New network float format */ #define LATEST_PROTOCOL_VERSION 37 diff --git a/src/unittest/test_serialization.cpp b/src/unittest/test_serialization.cpp index cfcfec0cd..b526792c4 100644 --- a/src/unittest/test_serialization.cpp +++ b/src/unittest/test_serialization.cpp @@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/string.h" #include "util/serialize.h" +#include class TestSerialization : public TestBase { public: @@ -43,6 +44,7 @@ public: void testVecPut(); void testStringLengthLimits(); void testBufReader(); + void testFloatFormat(); std::string teststring2; std::wstring teststring2_w; @@ -70,6 +72,7 @@ void TestSerialization::runTests(IGameDef *gamedef) TEST(testVecPut); TEST(testStringLengthLimits); TEST(testBufReader); + TEST(testFloatFormat); } //////////////////////////////////////////////////////////////////////////////// @@ -631,6 +634,75 @@ void TestSerialization::testBufReader() UASSERT(!buf.getRawDataNoEx(raw_data, sizeof(raw_data))); } +void TestSerialization::testFloatFormat() +{ + FloatType type = getFloatSerializationType(); + u32 i; + f32 fs, fm; + + // Check precision of float calculations on this platform + const std::unordered_map float_results = { + { 0.0f, 0x00000000UL }, + { 1.0f, 0x3F800000UL }, + { -1.0f, 0xBF800000UL }, + { 0.1f, 0x3DCCCCCDUL }, + { -0.1f, 0xBDCCCCCDUL }, + { 1945329.25f, 0x49ED778AUL }, + { -23298764.f, 0xCBB1C166UL }, + { 0.5f, 0x3F000000UL }, + { -0.5f, 0xBF000000UL } + }; + for (const auto &v : float_results) { + i = f32Tou32Slow(v.first); + if (std::abs((s64)v.second - i) > 32) { + printf("Inaccurate float values on %.9g, expected 0x%X, actual 0x%X\n", + v.first, v.second, i); + UASSERT(false); + } + + fs = u32Tof32Slow(v.second); + if (std::fabs(v.first - fs) > std::fabs(v.first * 0.000005f)) { + printf("Inaccurate float values on 0x%X, expected %.9g, actual 0x%.9g\n", + v.second, v.first, fs); + UASSERT(false); + } + } + + if (type == FLOATTYPE_SLOW) { + // conversion using memcpy is not possible + // Skip exact float comparison checks below + return; + } + + auto test_single = [&fs, &fm](const u32 &i) -> bool { + memcpy(&fm, &i, 4); + fs = u32Tof32Slow(i); + if (fm != fs) { + printf("u32Tof32Slow failed on 0x%X, expected %.9g, actual %.9g\n", + i, fm, fs); + return false; + } + if (f32Tou32Slow(fs) != i) { + printf("f32Tou32Slow failed on %.9g, expected 0x%X, actual 0x%X\n", + fs, i, f32Tou32Slow(fs)); + return false; + } + return true; + }; + + // Use step of prime 277 to speed things up from 3 minutes to a few seconds + // Test from 0 to 0xFF800000UL (positive) + for (i = 0x00000000UL; i <= 0x7F800000UL; i += 277) + UASSERT(test_single(i)); + + // Ensure +inf and -inf are tested + UASSERT(test_single(0x7F800000UL)); + UASSERT(test_single(0xFF800000UL)); + + // Test from 0x80000000UL to 0xFF800000UL (negative) + for (i = 0x80000000UL; i <= 0xFF800000UL; i += 277) + UASSERT(test_single(i)); +} const u8 TestSerialization::test_serialized_data[12 * 13] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index f571ab22c..bf208693b 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -4,6 +4,7 @@ set(UTIL_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/base64.cpp ${CMAKE_CURRENT_SOURCE_DIR}/directiontables.cpp ${CMAKE_CURRENT_SOURCE_DIR}/enriched_string.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ieee_float.cpp ${CMAKE_CURRENT_SOURCE_DIR}/numeric.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pointedthing.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serialize.cpp diff --git a/src/util/ieee_float.cpp b/src/util/ieee_float.cpp new file mode 100644 index 000000000..e7909360f --- /dev/null +++ b/src/util/ieee_float.cpp @@ -0,0 +1,136 @@ +/* + * Conversion of f32 to IEEE-754 and vice versa. + * + * © Copyright 2018 Pedro Gimeno Fortea. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "ieee_float.h" +#include "log.h" +#include "porting.h" +#include +#include + +// Given an unsigned 32-bit integer representing an IEEE-754 single-precision +// float, return the float. +f32 u32Tof32Slow(u32 i) +{ + // clang-format off + int exp = (i >> 23) & 0xFF; + u32 sign = i & 0x80000000UL; + u32 imant = i & 0x7FFFFFUL; + if (exp == 0xFF) { + // Inf/NaN + if (imant == 0) { + if (std::numeric_limits::has_infinity) + return sign ? -std::numeric_limits::infinity() : + std::numeric_limits::infinity(); + return sign ? std::numeric_limits::max() : + std::numeric_limits::lowest(); + } + return std::numeric_limits::has_quiet_NaN ? + std::numeric_limits::quiet_NaN() : -0.f; + } + + if (!exp) { + // Denormal or zero + return sign ? -ldexpf((f32)imant, -149) : ldexpf((f32)imant, -149); + } + + return sign ? -ldexpf((f32)(imant | 0x800000UL), exp - 150) : + ldexpf((f32)(imant | 0x800000UL), exp - 150); + // clang-format on +} + +// Given a float, return an unsigned 32-bit integer representing the f32 +// in IEEE-754 single-precision format. +u32 f32Tou32Slow(f32 f) +{ + u32 signbit = std::copysign(1.0f, f) == 1.0f ? 0 : 0x80000000UL; + if (f == 0.f) + return signbit; + if (std::isnan(f)) + return signbit | 0x7FC00000UL; + if (std::isinf(f)) + return signbit | 0x7F800000UL; + int exp; + f32 mant = frexpf(f, &exp); + u32 imant = (u32)std::floor((signbit ? -16777216.f : 16777216.f) * mant); + exp += 126; + if (exp <= 0) { + // Denormal + return signbit | (exp <= -31 ? 0 : imant >> (1 - exp)); + } + + if (exp >= 255) { + // Overflow due to the platform having exponents bigger than IEEE ones. + // Return signed infinity. + return signbit | 0x7F800000UL; + } + + // Regular number + return signbit | (exp << 23) | (imant & 0x7FFFFFUL); +} + +// This test needs the following requisites in order to work: +// - The float type must be a 32 bits IEEE-754 single-precision float. +// - The endianness of f32s and integers must match. +FloatType getFloatSerializationType() +{ + // clang-format off + const f32 cf = -22220490.f; + const u32 cu = 0xCBA98765UL; + if (std::numeric_limits::is_iec559 && sizeof(cf) == 4 && + sizeof(cu) == 4 && !memcmp(&cf, &cu, 4)) { + // u32Tof32Slow and f32Tou32Slow are not needed, use memcpy + return FLOATTYPE_SYSTEM; + } + + // Run quick tests to ensure the custom functions provide acceptable results + warningstream << "floatSerialization: f32 and u32 endianness are " + "not equal or machine is not IEEE-754 compliant" << std::endl; + u32 i; + char buf[128]; + + // NaN checks aren't included in the main loop + if (!std::isnan(u32Tof32Slow(0x7FC00000UL))) { + porting::mt_snprintf(buf, sizeof(buf), + "u32Tof32Slow(0x7FC00000) failed to produce a NaN, actual: %.9g", + u32Tof32Slow(0x7FC00000UL)); + infostream << buf << std::endl; + } + if (!std::isnan(u32Tof32Slow(0xFFC00000UL))) { + porting::mt_snprintf(buf, sizeof(buf), + "u32Tof32Slow(0xFFC00000) failed to produce a NaN, actual: %.9g", + u32Tof32Slow(0xFFC00000UL)); + infostream << buf << std::endl; + } + + i = f32Tou32Slow(std::numeric_limits::quiet_NaN()); + // check that it corresponds to a NaN encoding + if ((i & 0x7F800000UL) != 0x7F800000UL || (i & 0x7FFFFFUL) == 0) { + porting::mt_snprintf(buf, sizeof(buf), + "f32Tou32Slow(NaN) failed to encode NaN, actual: 0x%X", i); + infostream << buf << std::endl; + } + + return FLOATTYPE_SLOW; + // clang-format on +} diff --git a/src/util/ieee_float.h b/src/util/ieee_float.h new file mode 100644 index 000000000..42b8641d0 --- /dev/null +++ b/src/util/ieee_float.h @@ -0,0 +1,34 @@ +/* +Minetest +Copyright (C) 2018 SmallJoker + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "irrlichttypes.h" + +enum FloatType +{ + FLOATTYPE_UNKNOWN, + FLOATTYPE_SLOW, + FLOATTYPE_SYSTEM +}; + +f32 u32Tof32Slow(u32 i); +u32 f32Tou32Slow(f32 f); + +FloatType getFloatSerializationType(); diff --git a/src/util/serialize.cpp b/src/util/serialize.cpp index 53c7c2add..f0e177d57 100644 --- a/src/util/serialize.cpp +++ b/src/util/serialize.cpp @@ -28,6 +28,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +FloatType g_serialize_f32_type = FLOATTYPE_UNKNOWN; + //// //// BufReader //// diff --git a/src/util/serialize.h b/src/util/serialize.h index 89bc0b097..016491a2c 100644 --- a/src/util/serialize.h +++ b/src/util/serialize.h @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes_bloated.h" #include "exceptions.h" // for SerializationError #include "debug.h" // for assert +#include "ieee_float.h" #include "config.h" #if HAVE_ENDIAN_H @@ -60,6 +61,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #define LONG_STRING_MAX_LEN (64 * 1024 * 1024) +extern FloatType g_serialize_f32_type; + #if HAVE_ENDIAN_H // use machine native byte swapping routines // Note: memcpy below is optimized out by modern compilers @@ -188,6 +191,25 @@ inline f32 readF1000(const u8 *data) return (f32)readS32(data) / FIXEDPOINT_FACTOR; } +inline f32 readF32(const u8 *data) +{ + u32 u = readU32(data); + + switch (g_serialize_f32_type) { + case FLOATTYPE_SYSTEM: { + f32 f; + memcpy(&f, &u, 4); + return f; + } + case FLOATTYPE_SLOW: + return u32Tof32Slow(u); + case FLOATTYPE_UNKNOWN: // First initialization + g_serialize_f32_type = getFloatSerializationType(); + return readF32(data); + } + throw SerializationError("readF32: Unreachable code"); +} + inline video::SColor readARGB8(const u8 *data) { video::SColor p(readU32(data)); @@ -245,6 +267,15 @@ inline v3f readV3F1000(const u8 *data) return p; } +inline v3f readV3F32(const u8 *data) +{ + v3f p; + p.X = (float)readF32(&data[0]); + p.Y = (float)readF32(&data[4]); + p.Z = (float)readF32(&data[8]); + return p; +} + /////////////// write routines //////////////// inline void writeU8(u8 *data, u8 i) @@ -259,7 +290,7 @@ inline void writeS8(u8 *data, s8 i) inline void writeS16(u8 *data, s16 i) { - writeU16(data, (u16)i); + writeU16(data, (u16)i); } inline void writeS32(u8 *data, s32 i) @@ -278,6 +309,23 @@ inline void writeF1000(u8 *data, f32 i) writeS32(data, i * FIXEDPOINT_FACTOR); } +inline void writeF32(u8 *data, f32 i) +{ + switch (g_serialize_f32_type) { + case FLOATTYPE_SYSTEM: { + u32 u; + memcpy(&u, &i, 4); + return writeU32(data, u); + } + case FLOATTYPE_SLOW: + return writeU32(data, f32Tou32Slow(i)); + case FLOATTYPE_UNKNOWN: // First initialization + g_serialize_f32_type = getFloatSerializationType(); + return writeF32(data, i); + } + throw SerializationError("writeF32: Unreachable code"); +} + inline void writeARGB8(u8 *data, video::SColor p) { writeU32(data, p.color); @@ -322,6 +370,13 @@ inline void writeV3F1000(u8 *data, v3f p) writeF1000(&data[8], p.Z); } +inline void writeV3F32(u8 *data, v3f p) +{ + writeF32(&data[0], p.X); + writeF32(&data[4], p.Y); + writeF32(&data[8], p.Z); +} + //// //// Iostream wrapper for data read/write //// @@ -351,12 +406,14 @@ MAKE_STREAM_READ_FXN(s16, S16, 2); MAKE_STREAM_READ_FXN(s32, S32, 4); MAKE_STREAM_READ_FXN(s64, S64, 8); MAKE_STREAM_READ_FXN(f32, F1000, 4); +MAKE_STREAM_READ_FXN(f32, F32, 4); MAKE_STREAM_READ_FXN(v2s16, V2S16, 4); MAKE_STREAM_READ_FXN(v3s16, V3S16, 6); MAKE_STREAM_READ_FXN(v2s32, V2S32, 8); MAKE_STREAM_READ_FXN(v3s32, V3S32, 12); MAKE_STREAM_READ_FXN(v2f, V2F1000, 8); MAKE_STREAM_READ_FXN(v3f, V3F1000, 12); +MAKE_STREAM_READ_FXN(v3f, V3F32, 12); MAKE_STREAM_READ_FXN(video::SColor, ARGB8, 4); MAKE_STREAM_WRITE_FXN(u8, U8, 1); @@ -368,12 +425,14 @@ MAKE_STREAM_WRITE_FXN(s16, S16, 2); MAKE_STREAM_WRITE_FXN(s32, S32, 4); MAKE_STREAM_WRITE_FXN(s64, S64, 8); MAKE_STREAM_WRITE_FXN(f32, F1000, 4); +MAKE_STREAM_WRITE_FXN(f32, F32, 4); MAKE_STREAM_WRITE_FXN(v2s16, V2S16, 4); MAKE_STREAM_WRITE_FXN(v3s16, V3S16, 6); MAKE_STREAM_WRITE_FXN(v2s32, V2S32, 8); MAKE_STREAM_WRITE_FXN(v3s32, V3S32, 12); MAKE_STREAM_WRITE_FXN(v2f, V2F1000, 8); MAKE_STREAM_WRITE_FXN(v3f, V3F1000, 12); +MAKE_STREAM_WRITE_FXN(v3f, V3F32, 12); MAKE_STREAM_WRITE_FXN(video::SColor, ARGB8, 4); ////