332 lines
9.0 KiB
C++
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
|
|
|