Skip to content

Commit 31fe30e

Browse files
committed
feat: BitStream and Buffer classes
1 parent 81106f1 commit 31fe30e

8 files changed

Lines changed: 462 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,6 @@ add_library(glimmerite::glimmerite ALIAS glimmerite_glimmerite)
9191
target_link_libraries(glimmerite_glimmerite
9292
INTERFACE
9393
glimmerite::math
94+
glimerrite::misc
9495
glimmerite::client
9596
)

include/gmi/misc/BitStream.h

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <bit>
5+
#include <cassert>
6+
#include <cmath>
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <format>
10+
#include <stdexcept>
11+
#include <string_view>
12+
#include <type_traits>
13+
14+
#include <gmi/misc/Buffer.h>
15+
16+
namespace gmi {
17+
18+
class BitStream {
19+
public:
20+
BitStream(uint8_t* data, size_t size) : m_buff(data, size) { };
21+
BitStream(const gmi::Uint8Buffer& buff) : m_buff(buff) { };
22+
23+
[[nodiscard]] size_t size() const {
24+
return m_buff.size();
25+
}
26+
27+
[[nodiscard]] size_t bitSize() const {
28+
return m_buff.size() * 8;
29+
}
30+
31+
[[nodiscard]] size_t index() const {
32+
return std::ceil(m_bit_index / 8.0);
33+
}
34+
35+
[[nodiscard]] size_t bitIndex() const {
36+
return m_bit_index;
37+
}
38+
39+
void setBitIndex(size_t index) {
40+
if (index >= size()) {
41+
throw std::out_of_range(
42+
std::format(
43+
"setIndex: index out of range, size: {}, index: {}",
44+
size(),
45+
index
46+
)
47+
);
48+
}
49+
m_bit_index = index;
50+
}
51+
52+
template<typename T>
53+
requires(std::is_integral_v<T>)
54+
[[nodiscard]] T readBits(uint8_t bitCount);
55+
56+
template<typename T>
57+
requires(std::is_integral_v<T>)
58+
void writeBits(T value, uint8_t bitCount);
59+
60+
#define DECLARE_READ_WRITE_FNS(SIZE) \
61+
void writeUint##SIZE(uint##SIZE##_t value) { \
62+
writeBits<uint##SIZE##_t>(value, SIZE); \
63+
} \
64+
void writeInt##SIZE(int##SIZE##_t value) { \
65+
writeBits<int##SIZE##_t>(value, SIZE); \
66+
} \
67+
[[nodiscard]] uint##SIZE##_t readUint##SIZE() { \
68+
return readBits<uint##SIZE##_t>(SIZE); \
69+
} \
70+
[[nodiscard]] int##SIZE##_t readInt##SIZE() { \
71+
return readBits<int##SIZE##_t>(SIZE); \
72+
}
73+
74+
DECLARE_READ_WRITE_FNS(8)
75+
DECLARE_READ_WRITE_FNS(16)
76+
DECLARE_READ_WRITE_FNS(32)
77+
DECLARE_READ_WRITE_FNS(64)
78+
#undef DECLARE_READ_WRITE_FNS
79+
80+
void writeFloat32(float value) {
81+
writeUint32(std::bit_cast<uint32_t>(value));
82+
}
83+
84+
[[nodiscard]] float readFloat32() {
85+
return std::bit_cast<float>(readUint32());
86+
}
87+
88+
void writeFloat64(double value) {
89+
writeUint64(std::bit_cast<uint64_t>(value));
90+
}
91+
92+
[[nodiscard]] double readFloat64() {
93+
return std::bit_cast<double>(readUint64());
94+
}
95+
96+
void writeFloat(float value, float min, float max, uint8_t bitCount);
97+
98+
float readFloat(float min, float max, uint8_t bitCount);
99+
100+
void writeBool(bool value) {
101+
writeBits<uint8_t>(value ? 1 : 0, 1);
102+
}
103+
104+
[[nodiscard]] bool readBool() {
105+
return readBits<uint8_t>(1) != 0;
106+
}
107+
108+
void writeString(const std::string_view& view, size_t maxSize = 0);
109+
110+
[[nodiscard]] std::string readString(size_t maxSize = 0);
111+
112+
private:
113+
gmi::Uint8Buffer m_buff;
114+
115+
size_t m_bit_index = 0;
116+
};
117+
118+
template<typename T>
119+
requires(std::is_integral_v<T>)
120+
T BitStream::readBits(uint8_t bitCount) {
121+
assert(bitCount <= (sizeof(T) * 8));
122+
123+
size_t byteIndex = this->index();
124+
size_t bitIndex = this->bitIndex();
125+
size_t byteCount = bitCount / 8;
126+
127+
size_t newIndex = (bitIndex + bitCount);
128+
if (newIndex > this->bitSize()) {
129+
throw std::out_of_range(
130+
std::format(
131+
"readBits: trying to read past the end of the buffer, buff size: {}, buff index: {}, bits to read: {}",
132+
this->bitSize(),
133+
bitIndex,
134+
bitCount
135+
)
136+
);
137+
}
138+
139+
T result = 0;
140+
141+
size_t offset = bitIndex;
142+
143+
// Ported from bitBuffer https://github.com/inolen/bit-buffer/blob/7b6ec454866090fac7c1b2e5d67f43ffe9ad6942/bit-buffer.js
144+
// licensed under MIT
145+
for (size_t i = 0; i < bitCount;) {
146+
uint8_t remaining = bitCount - i;
147+
uint8_t bitOffset = offset & 7;
148+
149+
// the max number of bits we can read from the current byte
150+
uint8_t read = std::min<size_t>(remaining, 8 - bitOffset);
151+
152+
// create a mask with the correct bit width
153+
uint8_t mask = (1 << read) - 1;
154+
155+
// shift the bits we want to the start of the byte and mask off the rest
156+
T val = (m_buff[offset / 8] >> bitOffset) & mask;
157+
158+
result |= val << i;
159+
160+
offset += read;
161+
i += read;
162+
}
163+
164+
// if working with a signed number and the bit count is not the full type size
165+
// we need to add the sign bit to the first bit
166+
if (
167+
std::is_signed_v<T>
168+
&& bitCount != (sizeof(T) * 8)
169+
&& result & (1 << (bitCount - 1))
170+
) {
171+
result |= -1 ^ ((1 << bitCount) - 1);
172+
}
173+
174+
m_bit_index = newIndex;
175+
176+
assert(offset == newIndex);
177+
178+
return result;
179+
}
180+
181+
template<typename T>
182+
requires(std::is_integral_v<T>)
183+
void BitStream::writeBits(T value, uint8_t bitCount) {
184+
assert(bitCount <= (sizeof(T) * 8));
185+
186+
size_t byteIndex = this->index();
187+
size_t bitIndex = this->bitIndex();
188+
size_t byteCount = bitCount / 8;
189+
190+
size_t newIndex = (bitIndex + bitCount);
191+
if (newIndex > this->bitSize()) {
192+
throw std::out_of_range(
193+
std::format(
194+
"writeBits: trying to write past the end of the buffer, buff size: {}, buff index: {}, bits to write: {}",
195+
this->bitSize(),
196+
bitIndex,
197+
bitCount
198+
)
199+
);
200+
}
201+
202+
size_t offset = bitIndex;
203+
204+
// Ported from bitBuffer https://github.com/inolen/bit-buffer/blob/7b6ec454866090fac7c1b2e5d67f43ffe9ad6942/bit-buffer.js
205+
// licensed under MIT
206+
for (size_t i = 0; i < bitCount;) {
207+
uint8_t wrote = 0;
208+
209+
// Write an entire byte if we can.
210+
if (bitCount - i >= 8 && (offset & 7) == 0) {
211+
m_buff[offset / 8] = value & 0xff;
212+
wrote = 8;
213+
} else {
214+
uint8_t remaining = bitCount - i;
215+
uint8_t bitOffset = offset & 7;
216+
size_t byteOffset = offset / 8;
217+
wrote = std::min<uint8_t>(remaining, 8 - bitOffset);
218+
219+
// create a mask with the correct bit width
220+
uint8_t mask = ~(0xff << wrote);
221+
// shift the bits we want to the start of the byte and mask off the rest
222+
uint8_t writeBits = value & mask;
223+
224+
// destination mask to zero all the bits we're changing first
225+
uint8_t destMask = ~(mask << bitOffset);
226+
227+
m_buff[byteOffset] = (m_buff[byteOffset] & destMask) | (writeBits << bitOffset);
228+
}
229+
230+
offset += wrote;
231+
i += wrote;
232+
value = value >> wrote;
233+
}
234+
235+
m_bit_index = newIndex;
236+
237+
assert(offset == newIndex);
238+
}
239+
240+
}

include/gmi/misc/Buffer.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include <cassert>
2+
#include <cstddef>
3+
#include <cstdint>
4+
5+
namespace gmi {
6+
7+
template<typename T>
8+
class Buffer {
9+
public:
10+
Buffer(T* data, size_t size) : m_data(data), m_size(size) { }
11+
12+
[[nodiscard]] size_t size() const {
13+
return m_size;
14+
}
15+
16+
[[nodiscard]] T* data() const {
17+
return m_data;
18+
}
19+
20+
[[nodiscard]] uint8_t operator[](size_t index) const {
21+
assert(index < m_size);
22+
return m_data[index];
23+
}
24+
25+
[[nodiscard]] uint8_t& operator[](size_t index) {
26+
assert(index < m_size);
27+
return m_data[index];
28+
}
29+
30+
private:
31+
T* m_data;
32+
size_t m_size;
33+
};
34+
35+
using Uint8Buffer = Buffer<uint8_t>;
36+
}

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ if (BUILD_CLIENT)
33
endif()
44

55
add_subdirectory(math)
6+
add_subdirectory(misc)

src/misc/BitStream.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#include "gmi/misc/BitStream.h"
2+
#include <cmath>
3+
4+
using gmi::BitStream;
5+
6+
void BitStream::writeFloat(float value, float min, float max, uint8_t bitCount) {
7+
assert(bitCount <= 32);
8+
9+
if (value < min || value > max) {
10+
throw std::out_of_range(
11+
std::format("writeFloat: value out of range: ${}, range: [{}, {}]", value, min, max)
12+
);
13+
}
14+
15+
uint32_t range = (1 << bitCount) - 1;
16+
float t = (value - min) / (max - min);
17+
uint32_t valueToWrite = std::round(t * range + 0.5);
18+
writeBits<uint32_t>(valueToWrite, bitCount);
19+
}
20+
21+
float BitStream::readFloat(float min, float max, uint8_t bitCount) {
22+
assert(bitCount <= 32);
23+
24+
uint32_t range = (1 << bitCount) - 1;
25+
26+
float read = readBits<uint32_t>(bitCount);
27+
float t = read / range;
28+
29+
return min + t * (max - min);
30+
}
31+
32+
void BitStream::writeString(const std::string_view& view, size_t maxSize) {
33+
bool noMaxSize = maxSize == 0;
34+
35+
size_t sizeToWrite = noMaxSize
36+
? view.size()
37+
: std::min(view.size(), maxSize);
38+
39+
size_t i = 0;
40+
for (; i < sizeToWrite; i++) {
41+
writeUint8(view[i]);
42+
43+
if (view[i] == 0) {
44+
return;
45+
}
46+
}
47+
48+
// add null terminator if:
49+
// we have a max size and we wrote less than it
50+
// or if we don't have a max size
51+
if (i != maxSize || noMaxSize) {
52+
writeUint8(0);
53+
}
54+
}
55+
56+
std::string BitStream::readString(size_t maxSize) {
57+
std::string str;
58+
59+
size_t i = 0;
60+
while (true) {
61+
i++;
62+
63+
uint8_t val = readUint8();
64+
65+
// break if we found a null terminator
66+
if (val == 0) {
67+
break;
68+
}
69+
70+
str.push_back(val);
71+
72+
if (maxSize != 0 && i >= maxSize) {
73+
break;
74+
}
75+
}
76+
77+
return str;
78+
}

src/misc/CMakeLists.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
add_library(glimmerite_misc STATIC
2+
BitStream.cpp
3+
)
4+
5+
set(GMI_MISC_INCLUDE_DIR "${GMI_INCLUDE_DIR}/gmi/misc")
6+
target_sources(
7+
glimmerite_misc
8+
PUBLIC
9+
FILE_SET HEADERS
10+
TYPE HEADERS
11+
BASE_DIRS ${GMI_INCLUDE_DIR}
12+
FILES
13+
${GMI_MISC_INCLUDE_DIR}/BitStream.h
14+
${GMI_MISC_INCLUDE_DIR}/Buffer.h
15+
)
16+
17+
set_target_properties(glimmerite_misc PROPERTIES LINKER_LANGUAGE CXX)
18+
19+
add_library(glimmerite::misc ALIAS glimmerite_misc)

0 commit comments

Comments
 (0)