Files
MushMachine/framework/s6zer/src/mm/s6zer/stream.hpp
Green Sky f27300a8fa add new serializer based on bitpacking
it has the important stuff, but still misses conviniences like buffers
and optimizations like varints.
only works for little endian but theoretically only needs endian
detection to be able to support bigendian
2022-12-07 00:35:25 +01:00

332 lines
9.0 KiB
C++

#pragma once
#include <cstddef> // size_t
#include <cstdint> // uint8_t, etc
#include <type_traits>
#include <cassert>
// TODO: make asserts redefinable
namespace MM::s6zer {
// this is heavily inspired by Glenn Fiedler's (Gaffer On Games) serializers
// https://www.gafferongames.com/post/reading_and_writing_packets/
// https://www.gafferongames.com/post/serialization_strategies/
// internal helpers
namespace detail {
// TODO: ugly, replace when c++20
[[nodiscard]] constexpr size_t first_bit_set8(const uint8_t number) {
return
(number & 0b10000000) ? 8 :
(number & 0b01000000) ? 7 :
(number & 0b00100000) ? 6 :
(number & 0b00010000) ? 5 :
(number & 0b00001000) ? 4 :
(number & 0b00000100) ? 3 :
(number & 0b00000010) ? 2 :
(number & 0b00000001) ? 1 :
0
;
}
[[nodiscard]] constexpr size_t first_bit_set32(const uint32_t number) {
return
(number & 0xff000000) ? first_bit_set8((number >> 24) & 0xff) + 24 :
(number & 0x00ff0000) ? first_bit_set8((number >> 16) & 0xff) + 16 :
(number & 0x0000ff00) ? first_bit_set8((number >> 8) & 0xff) + 8 :
(number & 0x000000ff) ? first_bit_set8(number & 0xff) :
0
;
}
[[nodiscard]] constexpr uint32_t byte_swap(const uint32_t value) noexcept {
return
((value & 0xff000000) >> 24) |
((value & 0x00ff0000) >> 8) |
((value & 0x0000ff00) << 8) |
((value & 0x000000ff) << 24)
;
}
[[nodiscard]] constexpr uint16_t byte_swap(const uint16_t value) noexcept {
return
((value & 0xff00) >> 8) |
((value & 0x00ff) << 8)
;
}
[[nodiscard]] constexpr uint8_t byte_swap(const uint8_t value) noexcept {
// noop
return value;
}
template<typename T>
[[nodiscard]] constexpr const T& max(const T& a, const T& b) noexcept {
return (a < b) ? b : a;
}
} // detail
// TODO: maybe 64bit?
// TODO: is this detail?
[[nodiscard]] constexpr size_t bits_required(const uint32_t numbers) {
return detail::first_bit_set32(numbers);
}
[[nodiscard]] constexpr uint32_t serialize_byte_order(const uint32_t value) {
// TODO: only works on little endian for now
if constexpr (true) { // native is little endian
return value;
} else { // native is big endian
return detail::byte_swap(value);
}
}
// helper for fake exceptions
#ifndef MM_S6ZER_BAIL
#define MM_S6ZER_BAIL(...) { \
if (! __VA_ARGS__) { \
return false; \
} \
}
#endif
struct StreamWriter {
StreamWriter(void) = delete;
StreamWriter(uint32_t* data, size_t size) : _data(data), _data_size(size) {
assert(size != 0);
assert(size % sizeof(uint32_t) == 0);
assert(data != nullptr);
}
// do i still need them?
[[nodiscard]] static constexpr bool isWriting(void) noexcept { return true; }
[[nodiscard]] static constexpr bool isReading(void) noexcept { return false; }
[[nodiscard]] bool flush(void) noexcept {
if (_scratch_bits != 0) {
// check if space in buffer
if (_data_size < (_word_index + 1) * sizeof(uint32_t)) {
return false;
}
_data[_word_index] = serialize_byte_order(static_cast<uint32_t>(_scratch & 0xffffffff));
_scratch >>= 32; // new bits are allways unset, so we can just allways 32
// we dont like negative
_scratch_bits = detail::max(static_cast<int32_t>(_scratch_bits) - 32, 0);
_word_index++;
}
return true;
}
template<typename T>
[[nodiscard]] bool serializeBits(const T value, const size_t number_of_bits = sizeof(T)*8) noexcept {
static_assert(std::is_integral_v<T>, "type needs to be an integer");
static_assert(std::is_unsigned_v<T>, "type needs to be unsigned");
static_assert(sizeof(T) <= 4, "not yet defined for > 32bit");
assert(number_of_bits <= sizeof(T)*8);
assert(number_of_bits > 0);
// do scratching
_scratch |= static_cast<uint64_t>(value) << _scratch_bits;
_scratch_bits += number_of_bits;
_bits_written += number_of_bits;
if (_scratch_bits >= 32) {
return flush();
}
return true;
}
[[nodiscard]] bool serializeBool(const bool value) noexcept {
return serializeBits(static_cast<uint32_t>(value), 1);
}
template<typename T>
[[nodiscard]] bool serializeInt(const T value, const T min, const T max) noexcept {
static_assert(std::is_integral_v<T>, "type needs to be an integer");
static_assert(sizeof(T) <= 4, "not yet defined for > 32bit");
assert(max >= min);
assert(value >= min);
assert(value <= max);
const size_t bits = bits_required(max - min);
return serializeBits(static_cast<uint32_t>(value - min), bits);
}
[[nodiscard]] bool serializeFloat(const float value) noexcept {
// TODO: dont use loop
for (size_t i = 0; i < sizeof(float); i++) {
MM_S6ZER_BAIL(serializeBits(reinterpret_cast<const uint8_t*>(&value)[i], 8));
}
return true;
}
[[nodiscard]] bool serializeDouble(const double value) noexcept {
// TODO: dont use loop
for (size_t i = 0; i < sizeof(double); i++) {
MM_S6ZER_BAIL(serializeBits(reinterpret_cast<const uint8_t*>(&value)[i], 8));
}
return true;
}
[[nodiscard]] bool serializeFloatCompressed(const float value, const float min, const float max, const float resolution) noexcept {
assert(max >= min);
assert(value >= min);
assert(value <= max);
// TODO: handle those rounding errors
const float numbers = (max - min) / resolution;
const size_t bits = bits_required(static_cast<uint32_t>(numbers));
const uint32_t tmp_value = static_cast<uint32_t>((value - min) / resolution);
return serializeBits(tmp_value, bits);
}
[[nodiscard]] size_t bytesWritten(void) noexcept {
// TODO: is this assert valid?
assert(_scratch_bits == 0);
//return _bits_written/8 + ((_bits_written % 8) ? 1 : 0);
return (_bits_written+7) / 8;
}
uint32_t* _data {nullptr};
size_t _data_size {0};
uint64_t _scratch {0};
size_t _scratch_bits {0};
size_t _word_index {0};
size_t _bits_written {0}; // includes bits still in scratch
};
struct StreamReader {
StreamReader(void) = delete;
// !! StreamReader assumes the data buffer has whole uint32_t,
// so at the end, even though data_size might be less then 4 bytes,
// here is actually a full, empty uint32
// !! enable AddressSanitzier during development and testing
StreamReader(const uint32_t* data, size_t size) : _data(data), _data_size(size) {
assert(size != 0);
//assert(size % sizeof(uint32_t) == 0);
assert(data != nullptr);
}
// do i still need them?
[[nodiscard]] static constexpr bool isWriting(void) noexcept { return false; }
[[nodiscard]] static constexpr bool isReading(void) noexcept { return true; }
template<typename T>
[[nodiscard]] bool serializeBits(T& value, const size_t number_of_bits = sizeof(T)*8) noexcept {
static_assert(std::is_integral_v<T>, "type needs to be an integer");
static_assert(std::is_unsigned_v<T>, "type needs to be unsigned");
static_assert(sizeof(T) <= 4, "not yet defined for > 32bit");
assert(number_of_bits <= sizeof(T)*8);
assert(number_of_bits > 0);
if (_scratch_bits < number_of_bits) {
if (_bits_read + number_of_bits > _data_size*8) {
// would read past end
return false;
}
_scratch |= static_cast<uint64_t>(serialize_byte_order(_data[_word_index])) << _scratch_bits;
_word_index++;
_scratch_bits += 32;
}
value = _scratch & ((uint64_t(1) << number_of_bits) - 1);
_scratch >>= number_of_bits;
_scratch_bits -= number_of_bits;
_bits_read += number_of_bits;
return true;
}
[[nodiscard]] bool serializeBool(bool& value) noexcept {
uint32_t tmp_value {0};
MM_S6ZER_BAIL(serializeBits(tmp_value, 1));
// :)
value = tmp_value != 0;
return true;
}
template<typename T>
[[nodiscard]] bool serializeInt(T& value, const T min, const T max) noexcept {
static_assert(std::is_integral_v<T>, "type needs to be an integer");
static_assert(sizeof(T) <= 4, "not yet defined for > 32bit");
assert(max >= min);
const size_t bits = bits_required(max - min);
uint32_t tmp_val {0};
MM_S6ZER_BAIL(serializeBits(tmp_val, bits));
value = static_cast<T>(tmp_val) + min;
return true;
}
[[nodiscard]] bool serializeFloat(float& value) noexcept {
// TODO: dont use loop
for (size_t i = 0; i < sizeof(float); i++) {
MM_S6ZER_BAIL(serializeBits(reinterpret_cast<uint8_t*>(&value)[i], 8));
}
return true;
}
[[nodiscard]] bool serializeDouble(double& value) noexcept {
// TODO: dont use loop
for (size_t i = 0; i < sizeof(double); i++) {
MM_S6ZER_BAIL(serializeBits(reinterpret_cast<uint8_t*>(&value)[i], 8));
}
return true;
}
[[nodiscard]] bool serializeFloatCompressed(float& value, const float min, const float max, const float resolution) noexcept {
assert(max >= min);
// TODO: use rounding, rn it snaps (floor)
const float numbers = (max - min) / resolution;
const size_t bits = bits_required(static_cast<uint32_t>(numbers));
uint32_t tmp_value {0};
MM_S6ZER_BAIL(serializeBits(tmp_value, bits));
value = static_cast<float>(tmp_value) * resolution + min;
return true;
}
[[nodiscard]] size_t bytesRead(void) noexcept {
return (_bits_read+7) / 8;
}
const uint32_t* _data {nullptr};
size_t _data_size {0};
uint64_t _scratch {0};
size_t _scratch_bits {0};
size_t _word_index {0};
size_t _bits_read {0};
};
} // MM::s6zer