OXYGENHUB
◆ main openmsx / src / utils / SerializeBuffer.hh
6.2 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef SERIALIZEBUFFER_HH
2
#define SERIALIZEBUFFER_HH
3
4
#include "MemBuffer.hh"
5
#include "inline.hh"
6
7
#include <algorithm>
8
#include <cassert>
9
#include <cstdint>
10
#include <cstring>
11
#include <span>
12
#include <tuple>
13
14
namespace openmsx {
15
16
/** Memory output buffer
17
 *
18
  * Acts as a replacement for std::vector<uint8_t>. You can insert data in the
19
  * buffer and the buffer will automatically grow. Like std::vector it manages
20
  * an internal memory buffer that will automatically reallocate and grow
21
  * exponentially.
22
  *
23
  * This class is much less general than std::vector and optimized for the case
24
  * of lots of (small) inserts at the end of the buffer (the main use case of
25
  * in-memory savestates). This makes it more efficient than std::vector.
26
  * std::vector is far from inefficient, but for savestates this is used A LOT,
27
  * so even small improvements matter a lot.
28
  */
29
class OutputBuffer
30
{
31
public:
32
	/** Create an empty output buffer.
33
	 */
34
	OutputBuffer();
35
36
	/** Insert data at the end of this buffer.
37
	  * This will automatically grow this buffer.
38
	  */
39
	void insert(const void* __restrict data, size_t len)
40
	{
41
#ifdef __GNUC__
42
		if (__builtin_constant_p(len)) {
43
			if        (len == 1) {
44
				insertN<1>(data); return;
45
			} else if (len == 2) {
46
				insertN<2>(data); return;
47
			} else if (len == 4) {
48
				insertN<4>(data); return;
49
			} else if (len == 8) {
50
				insertN<8>(data); return;
51
			}
52
		}
53
#endif
54
		insertN(data, len);
55
	}
56
#ifdef __GNUC__
57
	template<size_t N> void insertN(const void* __restrict data);
58
#endif
59
	void insertN(const void* __restrict data, size_t len);
60
61
	/** Insert all the elements of the given tuple.
62
	  * Equivalent to repeatedly calling insert() for all the elements of
63
	  * the tuple. Though using this method the implementation only has to
64
	  * check once whether enough memory is allocated.
65
	  */
66
	template<typename TUPLE> ALWAYS_INLINE void insert_tuple_ptr(const TUPLE& tuple)
67
	{
68
		size_t len = 0;
69
		auto accum = [&](auto* p) { len += sizeof(*p); };
70
		std::apply([&](auto&&... args) { (accum(args), ...); }, tuple);
71
72
		if (const uint8_t* newEnd = end + len; newEnd > buf.end()) [[unlikely]] {
73
			grow(len); // reallocates, thus newEnd is no longer valid.
74
		}
75
76
		uint8_t* dst = end;
77
		auto write = [&](auto* src) {
78
			memcpy(dst, src, sizeof(*src));
79
			dst += sizeof(*src);
80
		};
81
		std::apply([&](auto&&... args) { (write(args), ...); }, tuple);
82
		assert(dst == (end + len));
83
84
		end = dst;
85
	}
86
87
	template<typename T> ALWAYS_INLINE void insert_tuple_ptr(const std::tuple<T*>& tuple)
88
	{
89
		// single-element tuple -> use insert() because it's better tuned
90
		insert(std::get<0>(tuple), sizeof(T));
91
	}
92
93
	/** Insert data at a given position. This will overwrite the old data.
94
	  * It's not possible to grow the buffer via this method (so the buffer
95
	  * must already be big enough to hold the new data).
96
	  */
97
	void insertAt(size_t pos, const void* __restrict data, size_t len)
98
	{
99
		assert(buf.data() + pos + len <= buf.end());
100
		memcpy(buf.data() + pos, data, len);
101
	}
102
103
	/** Reserve space to insert the given number of bytes.
104
	  * The returned pointer is only valid until the next internal
105
	  * reallocate, so till the next call to insert() or allocate().
106
	  *
107
	  * If you don't know yet exactly how much memory to allocate (e.g.
108
	  * when the buffer will be used for gzip output data), you can request
109
	  * the maximum size and deallocate the unused space later.
110
	  */
111
	[[nodiscard]] std::span<uint8_t> allocate(size_t len)
112
	{
113
		auto* newEnd = end + len;
114
		// Make sure the next OutputBuffer will start with an initial size
115
		// that can hold this much space plus some slack.
116
		size_t newSize = newEnd - buf.data();
117
		lastSize = std::max(lastSize, newSize + 1000);
118
		if (newEnd <= buf.end()) {
119
			uint8_t* result = end;
120
			end = newEnd;
121
			return {result, len};
122
		} else {
123
			return {allocateGrow(len), len};
124
		}
125
	}
126
127
	/** Free part of a previously allocated buffer.
128
	 *
129
	  * The parameter must point right after the last byte of the used
130
	  * portion of the buffer. So it must be in the range [buf, buf+num]
131
	  * with buf and num respectively the return value and the parameter
132
	  * of the last allocate() call.
133
	  *
134
	  * See comment in allocate(). This call must be done right after the
135
	  * allocate() call, there cannot be any other (non-const) call to this
136
	  * object in between.
137
	  */
138
	/*void deallocate(uint8_t* pos)
139
	{
140
		assert(buf.data() <= pos);
141
		assert(pos <= end);
142
		end = pos;
143
	}*/
144
145
	/** Get the current size of the buffer.
146
	 */
147
	[[nodiscard]] size_t getPosition() const
148
	{
149
		return end - buf.data();
150
	}
151
152
	/** Release ownership of the buffer.
153
	 * Returns both the buffer and its size.
154
	 */
155
	[[nodiscard]] MemBuffer<uint8_t> release() &&;
156
157
private:
158
	void insertGrow(const void* __restrict data, size_t len);
159
	[[nodiscard]] uint8_t* allocateGrow(size_t len);
160
	void grow(size_t len);
161
162
	MemBuffer<uint8_t> buf; // allocated memory
163
	uint8_t* end;           // points right after the last used byte
164
	                        // so   end - buf == size_in_use
165
166
	static inline size_t lastSize = 50000; // initial estimate
167
};
168
169
170
/** This class is complementary to the OutputBuffer class.
171
  * Instead of filling an initially empty buffer it starts from a filled buffer
172
  * and allows to retrieve items starting from the start of the buffer.
173
  */
174
class InputBuffer
175
{
176
public:
177
	/** Construct new InputBuffer, typically the buf_ parameter
178
	  * will come from a MemBuffer object.
179
	  */
180
	explicit InputBuffer(std::span<const uint8_t> buf_)
181
		: buf(buf_) {}
182
183
	/** Read the given number of bytes.
184
	  * This 'consumes' the read bytes, so a future read() will continue
185
	  * where this read stopped.
186
	  */
187
	void read(void* __restrict result, size_t len)
188
	{
189
		assert(buf.size() >= len);
190
		memcpy(result, buf.data(), len);
191
		buf = buf.subspan(len);
192
	}
193
194
	/** Skip the given number of bytes.
195
	  * This is similar to a read(), but it will only consume the data, not
196
	  * copy it.
197
	  */
198
	void skip(size_t len)
199
	{
200
		assert(buf.size() >= len);
201
		buf = buf.subspan(len);
202
	}
203
204
	/** Return a pointer to the current position in the buffer.
205
	  * This is useful if you don't want to copy the data, but e.g. use it
206
	  * as input for an uncompress algorithm. You can later use skip() to
207
	  * actually consume the data.
208
	  */
209
	[[nodiscard]] const uint8_t* getCurrentPos() const { return buf.data(); }
210
211
private:
212
	std::span<const uint8_t> buf;
213
};
214
215
} // namespace openmsx
216
217
#endif