From 1aea23d7650e0cf08a08ff3b1e9612b414cb1512 Mon Sep 17 00:00:00 2001 From: Pablo Andres Fuente Date: Tue, 1 Oct 2024 23:41:13 -0300 Subject: [PATCH] Add half precision floating point support to `StreamPeer` Closes godotengine/godot-proposals#5983 Adds put/get methods to `StreamPeer` that handles half precision floating point values. Adds endode/decode half precision floating point to `marshalls`. Adds `get_half` and `store_half` to `FileAccess` Co-Authored-By: "Alfonso J. Ramos" --- core/io/file_access.cpp | 10 ++ core/io/file_access.h | 2 + core/io/marshalls.h | 10 ++ core/io/stream_peer.cpp | 26 ++++++ core/io/stream_peer.h | 2 + doc/classes/FileAccess.xml | 13 +++ doc/classes/StreamPeer.xml | 13 +++ tests/core/io/test_file_access.h | 92 +++++++++++++++++++ tests/core/io/test_marshalls.h | 21 +++++ tests/core/io/test_stream_peer.h | 22 +++++ tests/data/floating_point_big_endian.bin | 1 + tests/data/floating_point_little_endian.bin | 1 + ...lf_precision_floating_point_big_endian.bin | 1 + ...precision_floating_point_little_endian.bin | 1 + 14 files changed, 215 insertions(+) create mode 100644 tests/data/floating_point_big_endian.bin create mode 100644 tests/data/floating_point_little_endian.bin create mode 100644 tests/data/half_precision_floating_point_big_endian.bin create mode 100644 tests/data/half_precision_floating_point_little_endian.bin diff --git a/core/io/file_access.cpp b/core/io/file_access.cpp index d919243e6b7a..46ee91a2eb1b 100644 --- a/core/io/file_access.cpp +++ b/core/io/file_access.cpp @@ -263,6 +263,10 @@ uint64_t FileAccess::get_64() const { return data; } +float FileAccess::get_half() const { + return Math::half_to_float(get_16()); +} + float FileAccess::get_float() const { MarshallFloat m; m.i = get_32(); @@ -522,6 +526,10 @@ void FileAccess::store_real(real_t p_real) { } } +void FileAccess::store_half(float p_dest) { + store_16(Math::make_half_float(p_dest)); +} + void FileAccess::store_float(float p_dest) { MarshallFloat m; m.f = p_dest; @@ -828,6 +836,7 @@ void FileAccess::_bind_methods() { ClassDB::bind_method(D_METHOD("get_16"), &FileAccess::get_16); ClassDB::bind_method(D_METHOD("get_32"), &FileAccess::get_32); ClassDB::bind_method(D_METHOD("get_64"), &FileAccess::get_64); + ClassDB::bind_method(D_METHOD("get_half"), &FileAccess::get_half); ClassDB::bind_method(D_METHOD("get_float"), &FileAccess::get_float); ClassDB::bind_method(D_METHOD("get_double"), &FileAccess::get_double); ClassDB::bind_method(D_METHOD("get_real"), &FileAccess::get_real); @@ -846,6 +855,7 @@ void FileAccess::_bind_methods() { ClassDB::bind_method(D_METHOD("store_16", "value"), &FileAccess::store_16); ClassDB::bind_method(D_METHOD("store_32", "value"), &FileAccess::store_32); ClassDB::bind_method(D_METHOD("store_64", "value"), &FileAccess::store_64); + ClassDB::bind_method(D_METHOD("store_half", "value"), &FileAccess::store_half); ClassDB::bind_method(D_METHOD("store_float", "value"), &FileAccess::store_float); ClassDB::bind_method(D_METHOD("store_double", "value"), &FileAccess::store_double); ClassDB::bind_method(D_METHOD("store_real", "value"), &FileAccess::store_real); diff --git a/core/io/file_access.h b/core/io/file_access.h index 2f4d1a860435..dd59ff85b6b0 100644 --- a/core/io/file_access.h +++ b/core/io/file_access.h @@ -142,6 +142,7 @@ class FileAccess : public RefCounted { virtual uint32_t get_32() const; ///< get 32 bits uint virtual uint64_t get_64() const; ///< get 64 bits uint + virtual float get_half() const; virtual float get_float() const; virtual double get_double() const; virtual real_t get_real() const; @@ -173,6 +174,7 @@ class FileAccess : public RefCounted { virtual void store_32(uint32_t p_dest); ///< store 32 bits uint virtual void store_64(uint64_t p_dest); ///< store 64 bits uint + virtual void store_half(float p_dest); virtual void store_float(float p_dest); virtual void store_double(double p_dest); virtual void store_real(real_t p_real); diff --git a/core/io/marshalls.h b/core/io/marshalls.h index 6f015ac386e3..82c760c28d22 100644 --- a/core/io/marshalls.h +++ b/core/io/marshalls.h @@ -84,6 +84,12 @@ static inline unsigned int encode_uint32(uint32_t p_uint, uint8_t *p_arr) { return sizeof(uint32_t); } +static inline unsigned int encode_half(float p_float, uint8_t *p_arr) { + encode_uint16(Math::make_half_float(p_float), p_arr); + + return sizeof(uint16_t); +} + static inline unsigned int encode_float(float p_float, uint8_t *p_arr) { MarshallFloat mf; mf.f = p_float; @@ -172,6 +178,10 @@ static inline uint32_t decode_uint32(const uint8_t *p_arr) { return u; } +static inline float decode_half(const uint8_t *p_arr) { + return Math::half_to_float(decode_uint16(p_arr)); +} + static inline float decode_float(const uint8_t *p_arr) { MarshallFloat mf; mf.i = decode_uint32(p_arr); diff --git a/core/io/stream_peer.cpp b/core/io/stream_peer.cpp index c49e15a3a0e2..65fabf17acc3 100644 --- a/core/io/stream_peer.cpp +++ b/core/io/stream_peer.cpp @@ -178,6 +178,18 @@ void StreamPeer::put_64(int64_t p_val) { put_data(buf, 8); } +void StreamPeer::put_half(float p_val) { + uint8_t buf[2]; + + encode_half(p_val, buf); + uint16_t *p16 = (uint16_t *)buf; + if (big_endian) { + *p16 = BSWAP16(*p16); + } + + put_data(buf, 2); +} + void StreamPeer::put_float(float p_val) { uint8_t buf[4]; @@ -294,6 +306,18 @@ int64_t StreamPeer::get_64() { return r; } +float StreamPeer::get_half() { + uint8_t buf[2]; + get_data(buf, 2); + + uint16_t *p16 = (uint16_t *)buf; + if (big_endian) { + *p16 = BSWAP16(*p16); + } + + return decode_half(buf); +} + float StreamPeer::get_float() { uint8_t buf[4]; get_data(buf, 4); @@ -385,6 +409,7 @@ void StreamPeer::_bind_methods() { ClassDB::bind_method(D_METHOD("put_u32", "value"), &StreamPeer::put_u32); ClassDB::bind_method(D_METHOD("put_64", "value"), &StreamPeer::put_64); ClassDB::bind_method(D_METHOD("put_u64", "value"), &StreamPeer::put_u64); + ClassDB::bind_method(D_METHOD("put_half", "value"), &StreamPeer::put_half); ClassDB::bind_method(D_METHOD("put_float", "value"), &StreamPeer::put_float); ClassDB::bind_method(D_METHOD("put_double", "value"), &StreamPeer::put_double); ClassDB::bind_method(D_METHOD("put_string", "value"), &StreamPeer::put_string); @@ -399,6 +424,7 @@ void StreamPeer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_u32"), &StreamPeer::get_u32); ClassDB::bind_method(D_METHOD("get_64"), &StreamPeer::get_64); ClassDB::bind_method(D_METHOD("get_u64"), &StreamPeer::get_u64); + ClassDB::bind_method(D_METHOD("get_half"), &StreamPeer::get_half); ClassDB::bind_method(D_METHOD("get_float"), &StreamPeer::get_float); ClassDB::bind_method(D_METHOD("get_double"), &StreamPeer::get_double); ClassDB::bind_method(D_METHOD("get_string", "bytes"), &StreamPeer::get_string, DEFVAL(-1)); diff --git a/core/io/stream_peer.h b/core/io/stream_peer.h index 29cdb8261516..44bbfbf1d524 100644 --- a/core/io/stream_peer.h +++ b/core/io/stream_peer.h @@ -73,6 +73,7 @@ class StreamPeer : public RefCounted { void put_u32(uint32_t p_val); void put_64(int64_t p_val); void put_u64(uint64_t p_val); + void put_half(float p_val); void put_float(float p_val); void put_double(double p_val); void put_string(const String &p_string); @@ -87,6 +88,7 @@ class StreamPeer : public RefCounted { int32_t get_32(); uint64_t get_u64(); int64_t get_64(); + float get_half(); float get_float(); double get_double(); String get_string(int p_bytes = -1); diff --git a/doc/classes/FileAccess.xml b/doc/classes/FileAccess.xml index 71a1fc8e09f5..157fbdb1a181 100644 --- a/doc/classes/FileAccess.xml +++ b/doc/classes/FileAccess.xml @@ -173,6 +173,12 @@ Returns the next 32 bits from the file as a floating-point number. + + + + Returns the next 16 bits from the file as a half-precision floating-point number. + + @@ -470,6 +476,13 @@ Stores a floating-point number as 32 bits in the file. + + + + + Stores a half-precision floating-point number as 16 bits in the file. + + diff --git a/doc/classes/StreamPeer.xml b/doc/classes/StreamPeer.xml index ad5c5472b895..acff5cf60493 100644 --- a/doc/classes/StreamPeer.xml +++ b/doc/classes/StreamPeer.xml @@ -59,6 +59,12 @@ Gets a single-precision float from the stream. + + + + Gets a half-precision float from the stream. + + @@ -162,6 +168,13 @@ Puts a single-precision float into the stream. + + + + + Puts a half-precision float into the stream. + + diff --git a/tests/core/io/test_file_access.h b/tests/core/io/test_file_access.h index a4d3fd1d7048..5c74e2cc2548 100644 --- a/tests/core/io/test_file_access.h +++ b/tests/core/io/test_file_access.h @@ -107,6 +107,98 @@ TEST_CASE("[FileAccess] Get as UTF-8 String") { CHECK(s_cr == "Hello darkness\rMy old friend\rI've come to talk\rWith you again\r"); CHECK(s_cr_nocr == "Hello darknessMy old friendI've come to talkWith you again"); } + +TEST_CASE("[FileAccess] Get/Store floating point values") { + // BigEndian Hex: 0x40490E56 + // LittleEndian Hex: 0x560E4940 + float value = 3.1415f; + + SUBCASE("Little Endian") { + const String file_path = TestUtils::get_data_path("floating_point_little_endian.bin"); + const String file_path_new = TestUtils::get_data_path("floating_point_little_endian_new.bin"); + + Ref f = FileAccess::open(file_path, FileAccess::READ); + REQUIRE(!f.is_null()); + CHECK_EQ(f->get_float(), value); + + Ref fw = FileAccess::open(file_path_new, FileAccess::WRITE); + REQUIRE(!fw.is_null()); + fw->store_float(value); + fw->close(); + + CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path)); + + DirAccess::remove_file_or_error(file_path_new); + } + + SUBCASE("Big Endian") { + const String file_path = TestUtils::get_data_path("floating_point_big_endian.bin"); + const String file_path_new = TestUtils::get_data_path("floating_point_big_endian_new.bin"); + + Ref f = FileAccess::open(file_path, FileAccess::READ); + REQUIRE(!f.is_null()); + f->set_big_endian(true); + CHECK_EQ(f->get_float(), value); + + Ref fw = FileAccess::open(file_path_new, FileAccess::WRITE); + REQUIRE(!fw.is_null()); + fw->set_big_endian(true); + fw->store_float(value); + fw->close(); + + CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path)); + + DirAccess::remove_file_or_error(file_path_new); + } +} + +TEST_CASE("[FileAccess] Get/Store floating point half precision values") { + // IEEE 754 half-precision binary floating-point format: + // sign exponent (5 bits) fraction (10 bits) + // 0 01101 0101010101 + // BigEndian Hex: 0x3555 + // LittleEndian Hex: 0x5535 + float value = 0.33325195f; + + SUBCASE("Little Endian") { + const String file_path = TestUtils::get_data_path("half_precision_floating_point_little_endian.bin"); + const String file_path_new = TestUtils::get_data_path("half_precision_floating_point_little_endian_new.bin"); + + Ref f = FileAccess::open(file_path, FileAccess::READ); + REQUIRE(!f.is_null()); + CHECK_EQ(f->get_half(), value); + + Ref fw = FileAccess::open(file_path_new, FileAccess::WRITE); + REQUIRE(!fw.is_null()); + fw->store_half(value); + fw->close(); + + CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path)); + + DirAccess::remove_file_or_error(file_path_new); + } + + SUBCASE("Big Endian") { + const String file_path = TestUtils::get_data_path("half_precision_floating_point_big_endian.bin"); + const String file_path_new = TestUtils::get_data_path("half_precision_floating_point_big_endian_new.bin"); + + Ref f = FileAccess::open(file_path, FileAccess::READ); + REQUIRE(!f.is_null()); + f->set_big_endian(true); + CHECK_EQ(f->get_half(), value); + + Ref fw = FileAccess::open(file_path_new, FileAccess::WRITE); + REQUIRE(!fw.is_null()); + fw->set_big_endian(true); + fw->store_half(value); + fw->close(); + + CHECK_EQ(FileAccess::get_sha256(file_path_new), FileAccess::get_sha256(file_path)); + + DirAccess::remove_file_or_error(file_path_new); + } +} + } // namespace TestFileAccess #endif // TEST_FILE_ACCESS_H diff --git a/tests/core/io/test_marshalls.h b/tests/core/io/test_marshalls.h index de8d6e14067f..29e713c3a970 100644 --- a/tests/core/io/test_marshalls.h +++ b/tests/core/io/test_marshalls.h @@ -90,6 +90,20 @@ TEST_CASE("[Marshalls] Unsigned 64 bit integer decoding") { CHECK(decode_uint64(arr) == 0x0f123456789abcdef); } +TEST_CASE("[Marshalls] Floating point half precision encoding") { + uint8_t arr[2]; + + // Decimal: 0.33325195 + // IEEE 754 half-precision binary floating-point format: + // sign exponent (5 bits) fraction (10 bits) + // 0 01101 0101010101 + // Hexadecimal: 0x3555 + unsigned int actual_size = encode_half(0.33325195f, arr); + CHECK(actual_size == sizeof(uint16_t)); + CHECK(arr[0] == 0x55); + CHECK(arr[1] == 0x35); +} + TEST_CASE("[Marshalls] Floating point single precision encoding") { uint8_t arr[4]; @@ -126,6 +140,13 @@ TEST_CASE("[Marshalls] Floating point double precision encoding") { CHECK(arr[7] == 0x3f); } +TEST_CASE("[Marshalls] Floating point half precision decoding") { + uint8_t arr[] = { 0x55, 0x35 }; + + // See floating point half precision encoding test case for details behind expected values. + CHECK(decode_half(arr) == 0.33325195f); +} + TEST_CASE("[Marshalls] Floating point single precision decoding") { uint8_t arr[] = { 0x00, 0x00, 0x20, 0x3e }; diff --git a/tests/core/io/test_stream_peer.h b/tests/core/io/test_stream_peer.h index 31bd69edd0ad..961b4ac07096 100644 --- a/tests/core/io/test_stream_peer.h +++ b/tests/core/io/test_stream_peer.h @@ -127,6 +127,17 @@ TEST_CASE("[StreamPeer] Get and sets through StreamPeerBuffer") { CHECK_EQ(spb->get_u64(), value); } + SUBCASE("A half-precision float value") { + float value = 3.1415927f; + float expected = 3.14062f; + + spb->clear(); + spb->put_half(value); + spb->seek(0); + + CHECK(spb->get_half() == doctest::Approx(expected)); + } + SUBCASE("A float value") { float value = 42.0f; @@ -255,6 +266,17 @@ TEST_CASE("[StreamPeer] Get and sets big endian through StreamPeerBuffer") { CHECK_EQ(spb->get_float(), value); } + SUBCASE("A half-precision float value") { + float value = 3.1415927f; + float expected = 3.14062f; + + spb->clear(); + spb->put_half(value); + spb->seek(0); + + CHECK(spb->get_half() == doctest::Approx(expected)); + } + SUBCASE("A double value") { double value = 42.0; diff --git a/tests/data/floating_point_big_endian.bin b/tests/data/floating_point_big_endian.bin new file mode 100644 index 000000000000..9534605ce18a --- /dev/null +++ b/tests/data/floating_point_big_endian.bin @@ -0,0 +1 @@ +@IV \ No newline at end of file diff --git a/tests/data/floating_point_little_endian.bin b/tests/data/floating_point_little_endian.bin new file mode 100644 index 000000000000..8cd66219d8f6 --- /dev/null +++ b/tests/data/floating_point_little_endian.bin @@ -0,0 +1 @@ +VI@ \ No newline at end of file diff --git a/tests/data/half_precision_floating_point_big_endian.bin b/tests/data/half_precision_floating_point_big_endian.bin new file mode 100644 index 000000000000..6519f7500ae5 --- /dev/null +++ b/tests/data/half_precision_floating_point_big_endian.bin @@ -0,0 +1 @@ +5U \ No newline at end of file diff --git a/tests/data/half_precision_floating_point_little_endian.bin b/tests/data/half_precision_floating_point_little_endian.bin new file mode 100644 index 000000000000..4f748ab1e92b --- /dev/null +++ b/tests/data/half_precision_floating_point_little_endian.bin @@ -0,0 +1 @@ +U5 \ No newline at end of file