Line data Source code
1 : #include "BulkTX.hpp"
2 :
3 : #include <AH/Arduino-Wrapper.h>
4 : #include <AH/Containers/CRTP.hpp>
5 :
6 : #include <algorithm>
7 :
8 : #include <cassert>
9 : #include <cstring>
10 :
11 : #ifdef FATAL_ERRORS
12 : #define CS_MIDI_USB_ASSERT(a) assert((a))
13 : #else
14 : #define CS_MIDI_USB_ASSERT(a)
15 : #endif
16 :
17 : BEGIN_CS_NAMESPACE
18 :
19 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
20 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write(MessageType msg) {
21 : write(&msg, 1);
22 : }
23 :
24 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
25 67496 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write(
26 : const MessageType *msgs, uint32_t num_msgs) {
27 67496 : const uint32_t *end = msgs + num_msgs;
28 170411 : while (msgs != end) msgs += write_impl(msgs, end - msgs);
29 67496 : }
30 :
31 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
32 : uint32_t BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write_nonblock(
33 : const MessageType *msgs, uint32_t num_msgs) {
34 : uint32_t total_sent = 0, sent = 1;
35 : while (total_sent < num_msgs && sent != 0) {
36 : sent = write_impl(msgs + total_sent, num_msgs - total_sent, true);
37 : total_sent += sent;
38 : }
39 : return total_sent;
40 : }
41 :
42 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
43 8333 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::send_now() {
44 8333 : auto buffer = writing.send_later.exchange(nullptr, mo_acq);
45 8333 : if (buffer == nullptr)
46 : // Either the write function or the timeout_handler already cleared
47 : // the send_later flag.
48 1824 : return;
49 6509 : CRTP(Derived).cancel_timeout();
50 :
51 : // Indicate to any handlers interrupting us that we intend to send a buffer.
52 6509 : writing.send_now.store(buffer, mo_seq); // (5)
53 :
54 : // Try to acquire the sending lock.
55 6509 : wbuffer_t *old = nullptr;
56 6509 : if (!writing.sending.compare_exchange_strong(old, buffer, mo_seq)) // (6)
57 : // If we couldn't get the lock, whoever has the lock will send the
58 : // data and clear send-now.
59 0 : return;
60 :
61 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
62 : // We now own the sending lock
63 :
64 : // It is possible that the tx_callback ran between (5) and (6), so check
65 : // the send-now flag again.
66 6509 : auto send_now = writing.send_now.load(mo_seq);
67 6509 : if (send_now == nullptr) {
68 : // If the buffer was already sent, release the sending lock and return.
69 0 : writing.sending.store(nullptr, mo_seq); // ▲▲▲
70 0 : return;
71 : }
72 :
73 : // Us having the sending lock also means that the timeout and the
74 : // tx_callback cannot have concurrent access to writing.active_writebuffer
75 : // Therefore, acquiring it must succeed.
76 6509 : auto send_buffer = writing.active_writebuffer.exchange(nullptr, mo_seq);
77 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
78 : CS_MIDI_USB_ASSERT(send_buffer == buffer);
79 : CS_MIDI_USB_ASSERT(send_now == buffer);
80 : // Now that we own both the buffer and the sending lock, we can send it
81 6509 : writing.send_now.store(nullptr, mo_rlx);
82 : // Prepare the other buffer to be filled
83 6509 : auto next_buffer = other_buf(send_buffer);
84 6509 : next_buffer->size = 0;
85 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
86 6509 : writing.active_writebuffer.store(next_buffer, mo_rel);
87 : // Send the current buffer
88 6509 : CRTP(Derived).tx_start(send_buffer->buffer, send_buffer->size);
89 : // ------------------------------------------------------------------------- (sending lock is released by the tx_callback)
90 6509 : return;
91 : }
92 :
93 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
94 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::reset(
95 : uint16_t packet_size) {
96 : writing.packet_size = packet_size;
97 : writing.buffers[0].size = 0;
98 : writing.buffers[1].size = 0;
99 : writing.active_writebuffer.store(&writing.buffers[0], mo_rlx);
100 : writing.send_later.store(nullptr, mo_rlx);
101 : writing.send_now.store(nullptr, mo_rlx);
102 : writing.sending.store(nullptr, mo_rlx);
103 : }
104 :
105 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
106 1 : bool BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::is_done() const {
107 2 : return writing.sending.load(mo_acq) == nullptr &&
108 2 : writing.send_later.load(mo_acq) == nullptr &&
109 2 : writing.send_now.load(mo_acq) == nullptr;
110 : }
111 :
112 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
113 102915 : uint32_t BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write_impl(
114 : const MessageType *msgs, uint32_t num_msgs) {
115 102915 : if (num_msgs == 0)
116 0 : return 0;
117 :
118 : // Try to get access to an available buffer
119 102915 : wbuffer_t *buffer = writing.active_writebuffer.exchange(nullptr, mo_acq);
120 : // If that failed, return without blocking, caller may retry until we get
121 : // a buffer we can use
122 102915 : if (buffer == nullptr)
123 0 : return 0;
124 :
125 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
126 : // At this point we have a buffer, and we have exclusive access to it,
127 : // but it may still be full
128 : // CS_MIDI_USB_ASSERT(writing.send_now.load(mo_rlx) == nullptr);
129 :
130 102915 : auto size = buffer->size;
131 : CS_MIDI_USB_ASSERT(size != SizeReserved);
132 102915 : size_t avail_size = writing.packet_size - size;
133 102915 : auto copy_size_zu = std::min<size_t>(avail_size, num_msgs * sizeof(*msgs));
134 102915 : auto copy_size = static_cast<uint16_t>(copy_size_zu);
135 102915 : if (copy_size > 0) {
136 102915 : std::memcpy(buffer->buffer + size, msgs, copy_size);
137 102915 : buffer->size = size + copy_size;
138 : }
139 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
140 : // Release access to the buffer
141 102915 : writing.active_writebuffer.store(buffer, mo_seq);
142 :
143 102915 : if (copy_size > 0) {
144 : // If we completely filled this buffer in one go, send it now.
145 102915 : if (size == 0 && copy_size == writing.packet_size) {
146 : CS_MIDI_USB_ASSERT(writing.send_later.load(mo_rlx) == nullptr);
147 : CS_MIDI_USB_ASSERT(writing.send_now.load(mo_rlx) == nullptr);
148 31751 : writing.send_now.store(buffer, mo_rel); // TODO: can be relaxed
149 : }
150 : // If this is the first data in the buffer, schedule it to be sent later.
151 71164 : else if (size == 0) {
152 : CS_MIDI_USB_ASSERT(writing.send_later.load(mo_rlx) == nullptr);
153 63187 : writing.send_later.store(buffer, mo_rel);
154 63187 : CRTP(Derived).start_timeout();
155 : }
156 : // If this buffer was partially filled before and now full, send it now.
157 7977 : else if (size + copy_size == writing.packet_size) {
158 5982 : auto send_buffer = writing.send_later.exchange(nullptr, mo_acq);
159 5982 : if (send_buffer != nullptr) {
160 : CS_MIDI_USB_ASSERT(writing.send_now.load(mo_rlx) == nullptr);
161 : CS_MIDI_USB_ASSERT(send_buffer == buffer);
162 5982 : CRTP(Derived).cancel_timeout();
163 5982 : writing.send_now.store(buffer, mo_rel); // TODO: can be relaxed
164 : }
165 : }
166 : }
167 :
168 : // An interrupt may have attempted to send the buffer while we owned it.
169 102915 : if (writing.send_now.load(mo_seq) == nullptr) // (1)
170 : // If that's not the case, we can safely return.
171 65182 : return copy_size / sizeof(*msgs);
172 :
173 : // Otherwise, it's our job to send the data that the interrupt failed to
174 : // send.
175 :
176 : // Try to acquire the sending lock.
177 37733 : wbuffer_t *old = nullptr;
178 37733 : if (!writing.sending.compare_exchange_strong(old, buffer, mo_seq)) // (2)
179 : // If we couldn't get the lock, whoever has the lock will send the
180 : // data and clear send-now.
181 0 : return copy_size / sizeof(*msgs);
182 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
183 : // We now own the sending lock
184 :
185 : // It is possible that the tx_callback ran between (1) and (2), so check
186 : // the send-now flag again.
187 37733 : auto send_now = writing.send_now.load(mo_seq);
188 37733 : if (send_now == nullptr) {
189 : // If the buffer was already sent, release the sending lock and return.
190 1 : writing.sending.store(nullptr, mo_seq); // ▲▲▲
191 1 : return copy_size / sizeof(*msgs);
192 : }
193 :
194 : // Us having the sending lock also means that the timeout and the
195 : // tx_callback cannot have concurrent access to writing.active_writebuffer
196 : // Therefore, acquiring it must succeed.
197 37732 : auto send_buffer = writing.active_writebuffer.exchange(nullptr, mo_seq);
198 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
199 : CS_MIDI_USB_ASSERT(send_buffer == buffer);
200 : CS_MIDI_USB_ASSERT(send_now == buffer);
201 : // Now that we own both the buffer and the sending lock, we can send it
202 37732 : writing.send_now.store(nullptr, mo_rlx);
203 : // Prepare the other buffer to be filled
204 37732 : auto next_buffer = other_buf(send_buffer);
205 37732 : next_buffer->size = 0;
206 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
207 37732 : writing.active_writebuffer.store(next_buffer, mo_rel);
208 : // Send the current buffer
209 37732 : CRTP(Derived).tx_start(send_buffer->buffer, send_buffer->size);
210 : // ------------------------------------------------------------------------- (sending lock is released by the tx_callback)
211 :
212 : // TODO: if copy_size == 0, we could retry here
213 :
214 37732 : return copy_size / sizeof(*msgs);
215 : }
216 :
217 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
218 50699 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::timeout_callback() {
219 50699 : auto buffer = writing.send_later.exchange(nullptr, mo_acq);
220 50699 : if (buffer == nullptr)
221 : // Either the write function or the send_now function already cleared
222 : // the send_later flag.
223 3 : return;
224 :
225 : // Indicate to any handlers interrupting us that we intend to send a buffer.
226 50696 : writing.send_now.store(buffer, mo_seq); // (7)
227 :
228 : // Try to acquire the sending lock.
229 50696 : wbuffer_t *old = nullptr;
230 50696 : if (!writing.sending.compare_exchange_strong(old, buffer, mo_seq)) // (8)
231 : // If we couldn't get the lock, whoever has the lock will send the
232 : // data and clear send-now.
233 0 : return;
234 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
235 : // We now own the sending lock
236 :
237 : // It is possible that the tx_callback ran between (7) and (8), so check
238 : // the send-now flag again.
239 50696 : auto send_now = writing.send_now.load(mo_seq);
240 50696 : if (send_now == nullptr) {
241 : // If the buffer was already sent, release the sending lock and return.
242 0 : writing.sending.store(nullptr, mo_seq); // ▲▲▲
243 0 : return;
244 : }
245 :
246 : // Us having the sending lock also means that the timeout and the
247 : // tx_callback cannot have concurrent access to writing.active_writebuffer
248 : // Therefore, acquiring it must succeed.
249 50696 : if (auto act = writing.active_writebuffer.exchange(nullptr, mo_seq)) {
250 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
251 : CS_MIDI_USB_ASSERT(act == buffer);
252 : CS_MIDI_USB_ASSERT(send_now == buffer);
253 : // Now that we own both the buffer and the sending lock, we can send it
254 50696 : writing.send_now.store(nullptr, mo_rlx);
255 : // Prepare the other buffer to be filled
256 50696 : auto next_buffer = other_buf(act);
257 50696 : next_buffer->size = 0;
258 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
259 50696 : writing.active_writebuffer.store(next_buffer, mo_rel);
260 : // Send the current buffer
261 50696 : CRTP(Derived).tx_start_timeout(buffer->buffer, buffer->size);
262 : // --------------------------------------------------------------------- (sending lock is released by the tx_callback)
263 50696 : return;
264 : }
265 : // The write function owns the buffer.
266 : // TODO: only valid if this is an interrupt handler, see tx_callback comment
267 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
268 0 : writing.sending.store(nullptr, mo_seq);
269 0 : return;
270 : }
271 :
272 : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
273 94938 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::tx_callback() {
274 : // ------------------------------------------------------------------------- (we still own the sending lock)
275 94938 : wbuffer_t *sent_buffer = writing.sending.load(mo_acq);
276 : CS_MIDI_USB_ASSERT(sent_buffer != nullptr);
277 :
278 : // Check if anyone tried to send the next buffer while the previous one
279 : // was still being sent
280 94938 : wbuffer_t *send_next = writing.send_now.load(mo_seq);
281 94938 : if (send_next) {
282 : CS_MIDI_USB_ASSERT(send_next != sent_buffer);
283 : // We already own the sending lock.
284 1 : if (auto act = writing.active_writebuffer.exchange(nullptr, mo_seq)) {
285 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
286 : CS_MIDI_USB_ASSERT(act == send_next);
287 1 : writing.send_now.store(nullptr, mo_rlx);
288 1 : sent_buffer->size = 0;
289 1 : writing.sending.store(send_next, mo_seq);
290 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
291 1 : writing.active_writebuffer.store(sent_buffer, mo_seq);
292 1 : CRTP(Derived).tx_start_isr(send_next->buffer, send_next->size);
293 1 : return;
294 : // ----------------------------------------------------------------- (sending lock is released by the next tx_callback)
295 : }
296 : // Someone else already holds the active_buffer lock.
297 : // We own the sending lock, but not the buffer to be sent. Since the
298 : // timeout and send_now can only own the buffer if they also own the
299 : // sending lock, it must be the write function that owns the buffer.
300 : // The write function always checks the send-now flag after releasing
301 : // the buffer, so we can safely release our sending lock and return.
302 : // In the case of interrupts/signal handlers, our release of the lock
303 : // happens-before the release of the release of the active buffer in
304 : // the write function, so there would be no need to check send_now
305 : // again after releasing. In different threads, we would need to check
306 : // again, because the write thread could try to acquire the sending
307 : // lock before we released it.
308 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
309 0 : writing.sending.store(nullptr, mo_seq);
310 0 : return;
311 :
312 : // TODO: can we do it for the threaded case? At first sight, we should
313 : // try to acquire the active write buffer first, and then try
314 : // to acquire the sending lock again.
315 : // Alternatively, we could add a “main-should-wait” flag to
316 : // indicate that the write function should do a busy wait because
317 : // we'll release the sending lock soon.
318 : }
319 :
320 : // Release the “sending” lock
321 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
322 94937 : writing.sending.store(nullptr, mo_seq);
323 :
324 : // Someone may have tried sending the other buffer while we were still
325 : // holding the sending lock, so check the send_now flag (again).
326 94937 : send_next = writing.send_now.load(mo_seq); // (3)
327 94937 : if (send_next == nullptr)
328 : // No one tried to send before, and if they try to send later, they will
329 : // be able to get the lock to do so.
330 94937 : return;
331 :
332 : // Someone tried to send. We must try to send now.
333 0 : wbuffer_t *old = nullptr;
334 0 : if (!writing.sending.compare_exchange_strong(old, send_next, mo_seq)) // (4)
335 : // If this fails, someone else must have been able to acquire the
336 : // sending lock in the meantime, and will be able to make progress.
337 0 : return;
338 :
339 : // The send_now flag must still be true: either the timeout fired between
340 : // (3) and (4) and clears it, in which case it holds on to the sending lock,
341 : // or it failed to get both the sending lock and the buffer, and in that
342 : // case it doesn't clear send-now. Similar for the main program.
343 : CS_MIDI_USB_ASSERT(writing.send_now.load(mo_seq) == send_next);
344 :
345 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
346 : // We now hold the sending lock (again)
347 :
348 : // This is the same as earlier
349 0 : if (auto act = writing.active_writebuffer.exchange(nullptr, mo_seq)) {
350 : // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
351 : CS_MIDI_USB_ASSERT(act == send_next);
352 0 : writing.send_now.store(nullptr, mo_rlx);
353 0 : sent_buffer->size = 0;
354 0 : writing.sending.store(send_next, mo_seq);
355 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
356 0 : writing.active_writebuffer.store(sent_buffer, mo_seq);
357 0 : CRTP(Derived).tx_start_isr(send_next->buffer, send_next->size);
358 0 : return;
359 : // --------------------------------------------------------------------- (sending lock is released by the next tx_callback)
360 : }
361 : // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
362 0 : writing.sending.store(nullptr, mo_seq);
363 :
364 : // TODO: same as above
365 : }
366 :
367 : END_CS_NAMESPACE
368 :
369 : #undef CS_MIDI_USB_ASSERT
|