Network: Send IEEE floats (#7768)
authorSmallJoker <SmallJoker@users.noreply.github.com>
Thu, 13 Dec 2018 10:20:57 +0000 (11:20 +0100)
committerLoïc Blot <nerzhul@users.noreply.github.com>
Thu, 13 Dec 2018 10:20:57 +0000 (11:20 +0100)
src/client/content_cao.cpp
src/genericobject.cpp
src/network/networkprotocol.h
src/unittest/test_serialization.cpp
src/util/CMakeLists.txt
src/util/ieee_float.cpp [new file with mode: 0644]
src/util/ieee_float.h [new file with mode: 0644]
src/util/serialize.cpp
src/util/serialize.h

index e2517f7cabe3750b741ff9a75107d377f9f8c793..9e4725881c4ca844549a103c08d3c73350ad9986 100644 (file)
@@ -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...
index ba5281093a9b4f77b8b2e08197a63bb43facbbed..ceec222aa92899ff7425b81db882576e77aa9b34 100644 (file)
@@ -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();
 }
 
index 12a91e647ad7db4d3220185b1d7286d3825ba81a..0cf77b8c5037801d67ba66ae75149e02dfd92101 100644 (file)
@@ -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
index cfcfec0cd6be17877cfb4543ba29bad2df1bfcbf..b526792c42a8c88dfdd203b871f1bb152a2100d8 100644 (file)
@@ -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 <cmath>
 
 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<f32, u32> 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,
index f571ab22ca75a2746ad8aea95bbdc36bf68dbe4d..bf208693b6effae96aab8a545b6ec1e3b09fd3f3 100644 (file)
@@ -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 (file)
index 0000000..e790936
--- /dev/null
@@ -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 <limits>
+#include <cmath>
+
+// 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<f32>::has_infinity)     
+                               return sign ? -std::numeric_limits<f32>::infinity() :
+                                       std::numeric_limits<f32>::infinity();
+                       return sign ? std::numeric_limits<f32>::max() :
+                               std::numeric_limits<f32>::lowest();
+               }
+               return std::numeric_limits<f32>::has_quiet_NaN ?
+                       std::numeric_limits<f32>::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<f32>::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<f32>::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 (file)
index 0000000..42b8641
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+Minetest
+Copyright (C) 2018 SmallJoker <mk939@ymail.com>
+
+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();
index 53c7c2addeaa3d20440f2f02181a4d2b0347ef1f..f0e177d57694068365709edd8596cfd9a184954f 100644 (file)
@@ -28,6 +28,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <iomanip>
 #include <vector>
 
+FloatType g_serialize_f32_type = FLOATTYPE_UNKNOWN;
+
 ////
 //// BufReader
 ////
index 89bc0b097934dad7c042047fd5e095b7dfe283d5..016491a2c37db04477977eaf1ce5f6f125425911 100644 (file)
@@ -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);
 
 ////