OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
triple_buffer.hpp
1#pragma once
2#include <array>
3#include <atomic>
4#include <cstdint>
5#include <span>
6#include <type_traits>
7#include <utility>
8#include <vector>
9
10namespace ossia
11{
12
13template <typename T>
14class triple_buffer
15{
16 static_assert(std::is_nothrow_swappable_v<T>, "T must be nothrow swappable");
17
18 // State encoding (packed into one atomic uint8_t):
19 // bits [0:1] - middle buffer index (0, 1, or 2)
20 // bit [2] - dirty flag (1 = producer has written new data)
21 //
22 // The producer owns a thread-local `write_idx` and the consumer owns a
23 // thread-local `read_idx`. The only shared atomic is `mid_state`.
24 //
25 // Producer: write to slot[write_idx], then exchange(write_idx | DIRTY).
26 // Consumer: if dirty, exchange(read_idx | 0), then read from new read slot.
27
28private:
29 static constexpr uint8_t index_mask = 0x03;
30 static constexpr uint8_t dirty_bit = 0x04;
31
32 struct alignas(64) slot
33 {
34 T data{};
35
36 slot() = default;
37
38 template <typename U>
39 explicit slot(U&& val)
40 : data(std::forward<U>(val))
41 {
42 }
43 };
44
45 std::array<slot, 3> m_buffers{};
46
47 alignas(64) std::atomic<uint8_t> m_mid_state{1};
48 alignas(64) uint8_t m_write_idx{0};
49 alignas(64) uint8_t m_read_idx{2};
50
51 triple_buffer(const triple_buffer&) = delete;
52 triple_buffer& operator=(const triple_buffer&) = delete;
53 triple_buffer(triple_buffer&&) = delete;
54 triple_buffer& operator=(triple_buffer&&) = delete;
55
56public:
57 explicit triple_buffer(const T& init) noexcept(std::is_nothrow_copy_constructible_v<T>)
58 : m_buffers{slot{init}, slot{init}, slot{init}}
59 {
60 }
61
62 triple_buffer() noexcept(std::is_nothrow_default_constructible_v<T>)
63 requires std::is_default_constructible_v<T>
64 {
65 }
66
67 void produce(T& value) noexcept
68 {
69 using std::swap;
70 swap(m_buffers[m_write_idx].data, value);
71
72 const uint8_t new_mid = static_cast<uint8_t>(m_write_idx | dirty_bit);
73 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
74 m_write_idx = old_mid & index_mask;
75 }
76
77 void produce(T&& value) noexcept(std::is_nothrow_move_assignable_v<T>)
78 {
79 m_buffers[m_write_idx].data = std::move(value);
80
81 const uint8_t new_mid = static_cast<uint8_t>(m_write_idx | dirty_bit);
82 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
83 m_write_idx = old_mid & index_mask;
84 }
85
86 // Consume latest data if available. Returns true and updates `result`
87 // if new data was produced since the last consume(); false otherwise.
88 bool consume(T& result) noexcept
89 {
90 const uint8_t state = m_mid_state.load(std::memory_order_acquire);
91 if(!(state & dirty_bit))
92 {
93 return false;
94 }
95
96 const uint8_t new_mid = m_read_idx;
97 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
98 m_read_idx = old_mid & index_mask;
99
100 using std::swap;
101 swap(result, m_buffers[m_read_idx].data);
102 return true;
103 }
104
105 // Return a const reference to the consumer's current slot.
106 // Only available for copyable types.
107 //
108 // Note: after consume(), the slot holds the value that was swapped *in*
109 // from the caller's `result`. For a stable "last consumed value",
110 // keep your own copy.
111 const T& read_buffer() const noexcept
112 requires std::is_copy_assignable_v<T>
113 {
114 return m_buffers[m_read_idx].data;
115 }
116
117 bool has_new_data() const noexcept
118 {
119 return m_mid_state.load(std::memory_order_acquire) & dirty_bit;
120 }
121};
122
123template <typename T>
124 requires std::is_trivially_copyable_v<T>
125class triple_buffer<T>
126{
127private:
128 static constexpr uint8_t index_mask = 0x03;
129 static constexpr uint8_t dirty_bit = 0x04;
130
131 struct alignas(64) slot
132 {
133 T data{};
134 };
135
136 std::array<slot, 3> m_buffers{};
137
138 alignas(64) std::atomic<uint8_t> m_mid_state{1};
139 alignas(64) uint8_t m_write_idx{0};
140 alignas(64) uint8_t m_read_idx{2};
141
142 // Cached copy of last consumed value so read() is always stable.
143 alignas(64) T m_last_read{};
144
145 triple_buffer(const triple_buffer&) = delete;
146 triple_buffer& operator=(const triple_buffer&) = delete;
147 triple_buffer(triple_buffer&&) = delete;
148 triple_buffer& operator=(triple_buffer&&) = delete;
149
150public:
151 explicit triple_buffer(T init) noexcept
152 : m_buffers{slot{init}, slot{init}, slot{init}}
153 , m_last_read{init}
154 {
155 }
156
157 triple_buffer() noexcept = default;
158
159 void produce(T value) noexcept
160 {
161 m_buffers[m_write_idx].data = value;
162
163 const uint8_t new_mid = static_cast<uint8_t>(m_write_idx | dirty_bit);
164 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
165 m_write_idx = old_mid & index_mask;
166 }
167
168 // Consume latest data if available. Returns true and updates `result`
169 //with the newest value; false if no new data since last consume().
170 bool consume(T& result) noexcept
171 {
172 const uint8_t state = m_mid_state.load(std::memory_order_acquire);
173 if(!(state & dirty_bit))
174 {
175 return false;
176 }
177
178 const uint8_t new_mid = m_read_idx;
179 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
180 m_read_idx = old_mid & index_mask;
181
182 m_last_read = m_buffers[m_read_idx].data;
183 result = m_last_read;
184 return true;
185 }
186
187 // Always valid
188 T read() const noexcept { return m_last_read; }
189
190 bool has_new_data() const noexcept
191 {
192 return m_mid_state.load(std::memory_order_acquire) & dirty_bit;
193 }
194};
195
196template <typename T, typename Container = std::vector<T>>
197class triple_buffer_raw
198{
199 static_assert(
200 std::is_nothrow_swappable_v<Container>, "Container must be nothrow swappable");
201
202private:
203 static constexpr uint8_t index_mask = 0x03;
204 static constexpr uint8_t dirty_bit = 0x04;
205
206 struct alignas(64) slot
207 {
208 Container data{};
209
210 slot() = default;
211
212 template <typename U>
213 explicit slot(U&& val)
214 : data(std::forward<U>(val))
215 {
216 }
217 };
218
219 std::array<slot, 3> m_buffers{};
220
221 alignas(64) std::atomic<uint8_t> m_mid_state{1};
222 alignas(64) uint8_t m_write_idx{0};
223 alignas(64) uint8_t m_read_idx{2};
224
225 triple_buffer_raw(const triple_buffer_raw&) = delete;
226 triple_buffer_raw& operator=(const triple_buffer_raw&) = delete;
227 triple_buffer_raw(triple_buffer_raw&&) = delete;
228 triple_buffer_raw& operator=(triple_buffer_raw&&) = delete;
229
230public:
231 triple_buffer_raw() noexcept(std::is_nothrow_default_constructible_v<Container>)
232 = default;
233
234 // Pre-allocate each slot with a given capacity.
235 explicit triple_buffer_raw(std::size_t initial_capacity)
236 {
237 for(auto& s : m_buffers)
238 s.data.reserve(initial_capacity);
239 }
240
241 // Direct access to the write slot. The producer writes into this
242 // container however it likes, then calls publish().
243 Container& write_buffer() noexcept { return m_buffers[m_write_idx].data; }
244
245 // Publish the current write buffer: swap it into the middle slot
246 // and pick up a new (stale) write slot.
247 void publish() noexcept
248 {
249 const uint8_t new_mid = static_cast<uint8_t>(m_write_idx | dirty_bit);
250 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
251 m_write_idx = old_mid & index_mask;
252 }
253
254 // Convenience: assign from an iterator range, then publish.
255 template <typename InputIt>
256 void produce(InputIt first, InputIt last)
257 {
258 m_buffers[m_write_idx].data.assign(first, last);
259 publish();
260 }
261
262 // Convenience: assign from a span, then publish.
263 void produce(std::span<const T> src)
264 {
265 m_buffers[m_write_idx].data.assign(src.begin(), src.end());
266 publish();
267 }
268
269 // Attempt to consume. Returns true if new data was swapped in.
270 // After a successful consume(), read_span() / read_buffer()
271 // reflect the new data.
272 bool consume() noexcept
273 {
274 const uint8_t state = m_mid_state.load(std::memory_order_acquire);
275 if(!(state & dirty_bit))
276 return false;
277
278 const uint8_t new_mid = m_read_idx;
279 const uint8_t old_mid = m_mid_state.exchange(new_mid, std::memory_order_acq_rel);
280 m_read_idx = old_mid & index_mask;
281 return true;
282 }
283
284 // View of the consumer's current slot.
285 std::span<const T> read_span() const noexcept
286 {
287 const auto& c = m_buffers[m_read_idx].data;
288 return {c.data(), c.size()};
289 }
290
291 const Container& read_buffer() const noexcept { return m_buffers[m_read_idx].data; }
292
293 bool has_new_data() const noexcept
294 {
295 return m_mid_state.load(std::memory_order_acquire) & dirty_bit;
296 }
297};
298}
Definition git_info.h:7