MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
RingBuffer.hpp
Go to the documentation of this file.
1#pragma once
2
3namespace MayaFlux::Memory {
4
5/**
6 * @concept TriviallyCopyable
7 * @brief Constraint for types that can be safely copied bitwise
8 *
9 * Required for lock-free ring buffers to ensure atomic operations
10 * don't invoke copy constructors during concurrent access.
11 */
12template <typename T>
13concept TriviallyCopyable = std::is_trivially_copyable_v<T>;
14
15/**
16 * @struct FixedStorage
17 * @brief Compile-time fixed-capacity storage using std::array
18 *
19 * Provides zero-overhead compile-time buffer allocation with capacity
20 * known at compile time. Required for lock-free contexts and real-time
21 * audio processing where dynamic allocation is forbidden.
22 *
23 * Capacity must be a power of 2 for efficient modulo operations using
24 * bitwise AND instead of division.
25 *
26 * @tparam T Element type
27 * @tparam Capacity Buffer size (must be power of 2)
28 *
29 * **Memory Layout:**
30 * - Stack-allocated std::array<T, Capacity>
31 * - Zero initialization overhead
32 * - Cache-friendly contiguous memory
33 *
34 * **Use Cases:**
35 * - Lock-free queues for input events
36 * - Real-time logging buffers
37 * - Fixed-size delay lines
38 * - MIDI message queues
39 *
40 * @code{.cpp}
41 * // 4096-element lock-free queue for input events
42 * using InputQueue = RingBuffer<InputValue,
43 * FixedStorage<InputValue, 4096>,
44 * LockFreePolicy,
45 * QueueAccess>;
46 * @endcode
47 */
48template <typename T, size_t Capacity>
50 static_assert((Capacity & (Capacity - 1)) == 0,
51 "FixedStorage capacity must be power of 2 for efficient modulo. "
52 "Use 64, 128, 256, 512, 1024, 2048, 4096, 8192, etc.");
53
54 using storage_type = std::array<T, Capacity>;
55 static constexpr size_t capacity_value = Capacity;
56 static constexpr bool is_resizable = false;
57
59
60 [[nodiscard]] constexpr size_t capacity() const noexcept { return Capacity; }
61};
62
63/**
64 * @struct DynamicStorage
65 * @brief Runtime-resizable storage using std::vector
66 *
67 * Provides dynamic capacity management for non-realtime contexts where
68 * buffer size cannot be determined at compile time. Supports resize()
69 * operations but may trigger heap allocations.
70 *
71 * @tparam T Element type
72 *
73 * **Memory Layout:**
74 * - Heap-allocated std::vector<T>
75 * - Resizable via resize() method
76 * - May reallocate on capacity change
77 *
78 * **Use Cases:**
79 * - Variable-length delay lines in non-realtime contexts
80 * - Recording buffers with unknown duration
81 * - Feedback processors with modulated delay times
82 * - Non-audio processing chains
83 */
84template <typename T>
86 using storage_type = std::vector<T>;
87 static constexpr bool is_resizable = true;
88
90
91 explicit DynamicStorage(size_t initial_capacity = 64)
92 : buffer(initial_capacity)
93 {
94 }
95
96 [[nodiscard]] size_t capacity() const noexcept { return buffer.size(); }
97
98 void resize(size_t new_capacity) { buffer.resize(new_capacity); }
99};
100
101/**
102 * @struct LockFreePolicy
103 * @brief Lock-free SPSC (Single Producer Single Consumer) concurrency
104 *
105 * Implements wait-free push and lock-free pop using C++20 atomic operations
106 * with careful memory ordering. Designed for real-time producer threads
107 * writing to non-realtime consumer threads.
108 *
109 * **Thread Safety:**
110 * - ONE writer thread (producer)
111 * - ONE reader thread (consumer)
112 * - NO mutex locks (real-time safe)
113 * - Cache-line aligned atomics prevent false sharing
114 *
115 * **Memory Ordering:**
116 * - relaxed: Initial atomic read (no synchronization needed)
117 * - acquire: Read from other thread (synchronizes-with release)
118 * - release: Write for other thread (synchronizes-with acquire)
119 *
120 * **SPSC Safety with Non-Trivial Types:**
121 * Unlike MPMC (multi-producer multi-consumer), SPSC queues are safe for
122 * non-trivially-copyable types because:
123 * - Each slot written by ONLY ONE thread (producer)
124 * - Each slot read by ONLY ONE thread (consumer)
125 * - No concurrent access to same slot (guaranteed by SPSC protocol)
126 * - Assignment `buffer[i] = value` is sequentially consistent per slot
127 *
128 * This means InputValue with std::string/std::vector is SAFE here.
129 * The atomic indices synchronize which slots are readable/writable,
130 * but the actual data copy happens in exclusive ownership.
131 *
132 * **Requirements:**
133 * - Storage MUST be FixedStorage (compile-time capacity)
134 * - Capacity MUST be power of 2
135 * - SPSC only (single producer, single consumer)
136 *
137 * **Performance:**
138 * - Wait-free push: O(1) without blocking
139 * - Lock-free pop: O(1) without mutex
140 * - ~10-20 CPU cycles per operation
141 *
142 * @code{.cpp}
143 * // Works with complex types (SPSC safe)
144 * LockFreeQueue<InputValue, 4096> input_queue; // Contains strings/vectors
145 *
146 * // Producer thread
147 * void enqueue_event(const InputValue& event) {
148 * input_queue.push(event); // Safe: exclusive write to slot
149 * }
150 *
151 * // Consumer thread
152 * void process_events() {
153 * while (auto event = input_queue.pop()) {
154 * handle(*event); // Safe: exclusive read from slot
155 * }
156 * }
157 * @endcode
158 */
160 static constexpr bool is_thread_safe = true;
161 static constexpr bool requires_trivial_copyable = false;
162 static constexpr bool requires_fixed_storage = true;
163
164 template <typename T, typename Storage>
165 struct State {
166 static_assert(!Storage::is_resizable,
167 "LockFreePolicy requires FixedStorage<T, N>. "
168 "For dynamic capacity, use SingleThreadedPolicy with DynamicStorage.");
169
170 alignas(64) std::atomic<size_t> write_index { 0 };
171 alignas(64) std::atomic<size_t> read_index { 0 };
172
173 static constexpr size_t increment(size_t index, size_t capacity) noexcept
174 {
175 return (index + 1) & (capacity - 1);
176 }
177 };
178};
179
180/**
181 * @struct SingleThreadedPolicy
182 * @brief Non-atomic single-threaded operation
183 *
184 * Provides minimal overhead when all access is from a single thread or
185 * when synchronization is handled externally (e.g., mutex-protected).
186 *
187 * **Thread Safety:**
188 * - NO thread synchronization
189 * - Use when externally synchronized OR single-threaded
190 *
191 * **Benefits:**
192 * - No atomic overhead (~2-5 CPU cycles faster per operation)
193 * - Works with any element type (no TriviallyCopyable requirement)
194 * - Compatible with both FixedStorage and DynamicStorage
195 *
196 * **When to Use:**
197 * - DSP delay lines in audio callback (single thread)
198 * - Polynomial node history buffers (single thread)
199 * - Non-realtime data structures
200 * - Already protected by external mutex
201 *
202 * @code{.cpp}
203 * // Single-threaded delay line for audio DSP
204 * using AudioDelay = RingBuffer<double,
205 * DynamicStorage<double>,
206 * SingleThreadedPolicy,
207 * HomogeneousAccess>;
208 *
209 * void audio_process_callback() {
210 * AudioDelay delay(1000); // No thread sync overhead
211 * delay.push(input_sample);
212 * auto delayed = delay.peek_oldest();
213 * }
214 * @endcode
215 */
217 static constexpr bool is_thread_safe = false;
218 static constexpr bool requires_trivial_copyable = false;
219 static constexpr bool requires_fixed_storage = false;
220
221 template <typename T, typename Storage>
222 struct State {
223 size_t write_index { 0 };
224 size_t read_index { 0 };
225
226 static constexpr size_t increment(size_t index, size_t capacity) noexcept
227 {
228 return (index + 1) % capacity;
229 }
230 };
231};
232
233/**
234 * @brief Helper to detect if a policy is MPSC
235 *
236 * Default: false. Specialization for MPSCPolicy sets true.
237 */
238template <typename Policy>
239static constexpr bool policy_is_mpsc = false;
240
241/**
242 * @brief Specialization for MPSCPolicy
243 *
244 * Detects if a given policy is MPSC by checking for the presence of
245 * the `is_mpsc` static member. If present, sets true; otherwise false.
246 */
247template <typename Policy>
248 requires requires { Policy::is_mpsc; }
249static constexpr bool policy_is_mpsc<Policy> = Policy::is_mpsc;
250
251/**
252 * @struct MPSCPolicy
253 * @brief Lock-free MPSC (Multi-Producer Single-Consumer) concurrency.
254 *
255 * Producers claim a slot atomically via fetch_add on a shared claim index,
256 * write into that slot, then mark it ready via a per-slot atomic flag.
257 * The single consumer spins on the ready flag of its current read slot
258 * before consuming - the spin is bounded because producers only stall the
259 * consumer for the duration of one slot write, not an unbounded critical
260 * section.
261 *
262 * Push is wait-free per producer in the uncontended case (one fetch_add,
263 * one store, one flag set). Under contention producers do not interfere
264 * with each other's slots - each owns its claimed index exclusively.
265 *
266 * Pop is wait-free when the ready flag is already set. It spins only if
267 * a producer has claimed the slot but not yet finished writing - this
268 * window is a handful of cycles (the slot assignment), not a lock hold.
269 *
270 * **Requirements:**
271 * - Storage MUST be FixedStorage (compile-time capacity, power of 2).
272 * - Single consumer only.
273 * - Multiple concurrent producers are safe.
274 *
275 * **Not suitable for:**
276 * - Multiple concurrent consumers.
277 * - DynamicStorage (ready flag array is fixed at compile time).
278 *
279 * **Approximate metrics:**
280 * - size() and empty() compare claim_index to read_index. A slot claimed
281 * but not yet written counts as occupied. Both are safe to call but
282 * may overcount transiently under concurrent push.
283 */
285 static constexpr bool is_thread_safe = true;
286 static constexpr bool is_mpsc = true;
287 static constexpr bool requires_trivial_copyable = false;
288 static constexpr bool requires_fixed_storage = true;
289
290 template <typename T, typename Storage>
291 struct State {
292 static_assert(!Storage::is_resizable,
293 "MPSCPolicy requires FixedStorage<T, N>. "
294 "For dynamic capacity, use SingleThreadedPolicy with DynamicStorage.");
295
296 static constexpr size_t N = Storage::capacity_value;
297
298 alignas(64) std::atomic<size_t> claim_index { 0 };
299 alignas(64) std::atomic<size_t> read_index { 0 };
300 std::array<std::atomic<bool>, N> ready_flags {};
301
303 {
304 for (auto& f : ready_flags)
305 f.store(false, std::memory_order_relaxed);
306 }
307
308 static constexpr size_t increment(size_t index, size_t capacity) noexcept
309 {
310 return (index + 1) & (capacity - 1);
311 }
312 };
313};
314
315/**
316 * @struct QueueAccess
317 * @brief FIFO queue semantics (oldest data first)
318 *
319 * Traditional queue behavior: enqueue at back, dequeue from front.
320 * Data ordering: [0] = oldest inserted, [N-1] = newest inserted.
321 *
322 * **Operations:**
323 * - push(): Add to back of queue
324 * - pop(): Remove from front of queue
325 * - linearized_view(): [oldest → newest]
326 *
327 * **Use Cases:**
328 * - Event queues (input, MIDI, OSC)
329 * - Message passing between threads
330 * - Task queues
331 * - Logging buffers
332 *
333 * @code{.cpp}
334 * LockFreeQueue<InputEvent, 2048> event_queue;
335 *
336 * // Producer: add events as they arrive
337 * event_queue.push(event);
338 *
339 * // Consumer: process in arrival order
340 * while (auto event = event_queue.pop()) {
341 * process(*event); // Oldest event first
342 * }
343 * @endcode
344 */
346 static constexpr bool push_front = false;
347 static constexpr bool pop_front = true;
348 static constexpr const char* name = "Queue (FIFO)";
349};
350
351/**
352 * @struct HistoryBufferAccess
353 * @brief History buffer semantics (newest sample first)
354 *
355 * Maintains temporal ordering where index 0 represents the most recent
356 * sample and higher indices represent progressively older samples.
357 * This is the natural indexing for difference equations and recursive
358 * relations: y[n], y[n-1], y[n-2], ...
359 *
360 * **Operations:**
361 * - push(): Add to front (becomes index 0)
362 * - operator[]: Direct indexing where [0] = newest, [k] = k samples ago
363 * - linearized_view(): Returns mutable span [newest → oldest]
364 *
365 * **Mathematical Context:**
366 * - Difference equations: y[n] = a·y[n-1] + b·y[n-2]
367 * - FIR filters: y[n] = Σ h[k]·x[n-k]
368 * - IIR filters: y[n] = Σ b[k]·x[n-k] - Σ a[k]·y[n-k]
369 * - Recurrence relations: any recursive numerical method
370 *
371 * @code{.cpp}
372 * HistoryBuffer<double> history(100);
373 *
374 * // Difference equation: y[n] = 0.5·y[n-1] + x[n]
375 * history.push(input_sample); // New sample at [0]
376 * double current = history[0]; // Current input
377 * double previous = history[1]; // One sample ago
378 * double output = 0.5 * previous + current;
379 *
380 * // Access via linearized view for convolution
381 * auto view = history.linearized_view(); // [newest → oldest]
382 * for (size_t k = 0; k < view.size(); ++k) {
383 * output += view[k] * coefficients[k];
384 * }
385 * @endcode
386 */
388 static constexpr bool push_front = true;
389 static constexpr bool pop_front = false;
390 static constexpr const char* name = "HistoryBuffer (newest-first)";
391};
392
393/**
394 * @brief History buffer for difference equations and recursive relations
395 *
396 * Specialized ring buffer for maintaining temporal history in mathematical
397 * computations. Pre-initialized to full capacity with zeros to match
398 * standard mathematical notation where y[n-k] is defined for all k < N
399 * from the start (initial conditions = 0).
400 *
401 * **Key Differences from Generic RingBuffer:**
402 * - Pre-filled to capacity on construction (all zeros)
403 * - Always returns full-size mutable spans (size = capacity)
404 * - Direct mutable element access via operator[]
405 * - Matches mathematical notation: y[0] = newest, y[k] = k steps back
406 *
407 * @tparam T Element type
408 *
409 * @code{.cpp}
410 * // IIR filter: y[n] = 0.8·y[n-1] - 0.5·y[n-2] + x[n]
411 * HistoryBuffer<double> y_history(2); // Pre-filled with [0.0, 0.0]
412 *
413 * for (auto x : input_signal) {
414 * y_history.push(x); // Current input becomes y[0]
415 *
416 * double y_n = y_history[0]; // y[n] (just pushed)
417 * double y_n1 = y_history[1]; // y[n-1]
418 * double y_n2 = y_history[2]; // y[n-2]
419 *
420 * double output = 0.8*y_n1 - 0.5*y_n2 + y_n;
421 * y_history.overwrite_newest(output); // Replace y[n] with computed output
422 * }
423 * @endcode
424 */
425template <typename T>
427public:
428 using value_type = T;
429 using reference = T&;
430 using const_reference = const T&;
431
432 /**
433 * @brief Construct history buffer with specified capacity
434 * @param capacity Maximum number of historical samples to maintain
435 *
436 * **Critical:** Buffer is immediately filled to capacity with default
437 * values (zero for numeric types). This matches mathematical convention
438 * where initial conditions are typically zero.
439 */
440 explicit HistoryBuffer(size_t capacity)
442 , m_data(capacity, T {})
445 {
446 }
447
448 /**
449 * @brief Push new value to front of history
450 * @param value New sample value
451 *
452 * Inserts value at index 0, shifting all previous values back.
453 * Oldest value (at index capacity-1) is discarded.
454 */
455 void push(const T& value)
456 {
457 if (m_capacity == 0)
458 return;
459
460 m_head = (m_head == 0) ? m_capacity - 1 : m_head - 1;
462
463 if (m_count < m_capacity) {
464 ++m_count;
465 }
466 }
467
468 /**
469 * @brief Access element by temporal offset
470 * @param index Temporal offset (0 = newest, k = k samples ago)
471 * @return Mutable reference to element
472 *
473 * Provides direct access to history: [0] = most recent, [1] = one step back, etc.
474 * Matches mathematical notation y[n-k] where index k represents temporal offset.
475 */
476 reference operator[](size_t index)
477 {
478 return m_data[(m_head + index) % m_capacity];
479 }
480
481 const_reference operator[](size_t index) const
482 {
483 return m_data[(m_head + index) % m_capacity];
484 }
485
486 /**
487 * @brief Get newest element (same as [0])
488 * @return Reference to most recent sample
489 */
491 {
492 return m_data[m_head];
493 }
494
496 {
497 return m_data[m_head];
498 }
499
500 /**
501 * @brief Get oldest element (same as [capacity-1])
502 * @return Reference to oldest sample in buffer
503 */
505 {
506 size_t oldest_idx = (m_head + m_count - 1) % m_capacity;
507 return m_data[oldest_idx];
508 }
509
511 {
512 size_t oldest_idx = (m_head + m_count - 1) % m_capacity;
513 return m_data[oldest_idx];
514 }
515
516 /**
517 * @brief Overwrite the newest element without advancing position
518 * @param value New value for current sample
519 *
520 * Critical for recursive algorithms where you push input, compute output,
521 * then replace the pushed value with the computed result.
522 */
523 void overwrite_newest(const T& value)
524 {
526 }
527
528 /**
529 * @brief Get mutable linearized view of entire history
530 * @return Mutable span ordered [newest → oldest], size = capacity
531 *
532 * **Always returns full-size span** (size = capacity), even if fewer
533 * elements have been pushed. This matches mathematical convention where
534 * y[n-k] is defined for all k < N with initial conditions = 0.
535 */
536 std::span<T> linearized_view()
537 {
538 for (size_t i = 0; i < m_count; ++i) {
540 }
541 return { m_linear_view.data(), m_count };
542 }
543
544 /**
545 * @brief Get const linearized view
546 */
547 std::span<const T> linearized_view() const
548 {
549 for (size_t i = 0; i < m_count; ++i) {
551 }
552 return { m_linear_view.data(), m_count };
553 }
554
555 /**
556 * @brief Update element at specific index
557 * @param index Temporal offset (0 = newest, k = k samples ago)
558 * @param value New value
559 */
560 void update(size_t index, const T& value)
561 {
562 if (index >= m_count) {
563 return;
564 }
565 m_data[(m_head + index) % m_capacity] = value;
566 }
567
568 /**
569 * @brief Reset buffer to initial state (all zeros)
570 *
571 * Fills buffer with default values and resets to full capacity.
572 * Matches behavior of mathematical systems with zero initial conditions.
573 */
574 void reset()
575 {
576 std::ranges::fill(m_data, T {});
577 m_head = 0;
579 }
580
581 /**
582 * @brief Set initial conditions
583 * @param values Initial values (ordered newest to oldest)
584 *
585 * Sets the first min(values.size(), capacity) elements to the given values,
586 * fills remainder with zeros. Sets count to full capacity.
587 */
588 void set_initial_conditions(const std::vector<T>& values)
589 {
590 std::ranges::fill(m_data, T {});
591
592 size_t count = std::min(values.size(), m_capacity);
593 for (size_t i = 0; i < count; ++i) {
594 m_data[i] = values[i];
595 }
596
597 m_head = 0;
599 }
600
601 /**
602 * @brief Resize buffer capacity
603 * @param new_capacity New maximum number of samples
604 *
605 * Preserves existing data in temporal order. If growing, new slots
606 * filled with zeros. If shrinking, oldest data discarded.
607 * Count always equals capacity after resize.
608 */
609 void resize(size_t new_capacity)
610 {
611 if (new_capacity == m_capacity)
612 return;
613
614 std::vector<T> current_data;
615 current_data.reserve(m_count);
616 for (size_t i = 0; i < m_count; ++i) {
617 current_data.push_back(m_data[(m_head + i) % m_capacity]);
618 }
619
620 m_capacity = new_capacity;
621 m_data.resize(new_capacity, T {});
622 m_linear_view.resize(new_capacity);
623
624 size_t to_copy = std::min(current_data.size(), new_capacity);
625 for (size_t i = 0; i < to_copy; ++i) {
626 m_data[i] = current_data[i];
627 }
628
629 m_head = 0;
631 }
632
633 /**
634 * @brief Get buffer capacity
635 * @return Maximum number of samples buffer can hold
636 */
637 [[nodiscard]] size_t capacity() const { return m_capacity; }
638
639 /**
640 * @brief Get current count (always equals capacity for HistoryBuffer)
641 * @return Number of valid elements (= capacity)
642 */
643 [[nodiscard]] size_t size() const { return m_count; }
644
645 /**
646 * @brief Check if buffer is empty (always false for HistoryBuffer)
647 */
648 [[nodiscard]] bool empty() const { return false; }
649
650 /**
651 * @brief Save current state for later restoration
652 * @return Vector containing current data in temporal order
653 */
654 [[nodiscard]] std::vector<T> save_state() const
655 {
656 std::vector<T> state;
657 state.reserve(m_count);
658 for (size_t i = 0; i < m_count; ++i) {
659 state.push_back(m_data[(m_head + i) % m_capacity]);
660 }
661 return state;
662 }
663
664 /**
665 * @brief Restore previously saved state
666 * @param state State vector from save_state()
667 */
668 void restore_state(const std::vector<T>& state)
669 {
670 std::ranges::fill(m_data, T {});
671
672 size_t count = std::min(state.size(), m_capacity);
673 for (size_t i = 0; i < count; ++i) {
674 m_data[i] = state[i];
675 }
676
677 m_head = 0;
679 }
680
681private:
683 std::vector<T> m_data;
684 mutable std::vector<T> m_linear_view;
685 size_t m_head {};
686 size_t m_count;
687};
688
689/**
690 * @class RingBuffer
691 * @brief Policy-driven unified circular buffer implementation
692 *
693 * Provides a single, flexible ring buffer that adapts to different use cases
694 * through compile-time policy selection. Policies control storage allocation,
695 * thread safety, and access patterns independently.
696 *
697 * **Design Philosophy:**
698 * - Zero runtime cost: All policy dispatch via templates (no virtual calls)
699 * - Type-safe: Policy constraints enforced at compile time
700 * - Composable: Policies combine orthogonally
701 * - Minimal: No features you don't need based on policy selection
702 *
703 * **Policy Dimensions:**
704 * 1. **Storage**: FixedStorage (compile-time) vs DynamicStorage (runtime)
705 * 2. **Concurrency**: LockFreePolicy (SPSC) vs SingleThreadedPolicy (no sync)
706 * 3. **Access**: QueueAccess (FIFO) vs HistoryBufferAccess (newest-first)
707 *
708 * **Common Configurations:**
709 *
710 * @code{.cpp}
711 * // Lock-free input event queue (realtime → worker thread)
712 * using InputQueue = RingBuffer<InputValue,
713 * FixedStorage<InputValue, 4096>,
714 * LockFreePolicy,
715 * QueueAccess>;
716 *
717 * // Single-threaded audio delay line (DSP processing)
718 * using AudioDelay = RingBuffer<double,
719 * DynamicStorage<double>,
720 * SingleThreadedPolicy,
721 * HistoryBufferAccess>;
722 *
723 * // Lock-free logging buffer (audio thread → disk writer)
724 * using LogBuffer = RingBuffer<RealtimeEntry,
725 * FixedStorage<RealtimeEntry, 8192>,
726 * LockFreePolicy,
727 * QueueAccess>;
728 *
729 * // Polynomial node history buffer (single-threaded DSP)
730 * using NodeHistory = RingBuffer<double,
731 * FixedStorage<double, 64>,
732 * SingleThreadedPolicy,
733 * HistoryBufferAccess>;
734 * @endcode
735 *
736 * **Migration from Legacy Implementations:**
737 *
738 * @tparam T Element type
739 * @tparam StoragePolicy FixedStorage<T,N> or DynamicStorage<T>
740 * @tparam ConcurrencyPolicy LockFreePolicy or SingleThreadedPolicy
741 * @tparam AccessPattern QueueAccess or HistoryBufferAccess
742 */
743template <
744 typename T,
745 typename StoragePolicy,
746 typename ConcurrencyPolicy = SingleThreadedPolicy,
747 typename AccessPattern = QueueAccess>
749
750 static_assert(!ConcurrencyPolicy::requires_fixed_storage || !StoragePolicy::is_resizable,
751 "Selected ConcurrencyPolicy requires FixedStorage<T, N>. "
752 "Either: (1) Use SingleThreadedPolicy, or (2) Use FixedStorage.");
753
754 using State = typename ConcurrencyPolicy::template State<T, StoragePolicy>;
755
756public:
757 using value_type = T;
758 using storage_type = StoragePolicy;
759 using reference = T&;
760 using const_reference = const T&;
761
762 static constexpr bool is_lock_free = ConcurrencyPolicy::is_thread_safe;
763 static constexpr bool is_mpsc = policy_is_mpsc<ConcurrencyPolicy>;
764 static constexpr bool is_resizable = StoragePolicy::is_resizable;
765 static constexpr bool is_delay_line = AccessPattern::push_front;
766
767 /**
768 * @brief Construct ring buffer with runtime capacity (DynamicStorage only)
769 * @param initial_capacity Initial buffer size in elements
770 */
771 explicit RingBuffer(size_t initial_capacity = 64)
772 requires(is_resizable)
773 : m_storage(initial_capacity)
774 , m_linearized(initial_capacity)
775 {
776 }
777
778 /**
779 * @brief Default construct ring buffer (FixedStorage only)
780 * Capacity determined by template parameter.
781 */
783 requires(!is_resizable)
784 = default;
785
786 /**
787 * @brief Push element into buffer
788 * @param value Element to insert
789 * @return true if successful, false if buffer full
790 *
791 * **Behavior by AccessPattern:**
792 * - QueueAccess: Adds to back of queue (oldest at front)
793 * - HistoryBufferAccess: Adds to front (becomes newest element)
794 *
795 * **Thread Safety:**
796 * - LockFreePolicy: Wait-free for single producer
797 * - SingleThreadedPolicy: Not thread-safe
798 *
799 * @code{.cpp}
800 * RingBuffer<double, ...> buffer;
801 * if (!buffer.push(42.0)) {
802 * // Buffer full, oldest data still present
803 * }
804 * @endcode
805 */
806 [[nodiscard]] bool push(const T& value) noexcept
807 {
808 if constexpr (is_mpsc) {
809 return push_mpsc(value);
810 } else if constexpr (is_lock_free) {
811 return push_lockfree(value);
812 } else {
813 return push_singlethread(value);
814 }
815 }
816
817 /**
818 * @brief Pop element from buffer
819 * @return Element if available, nullopt if empty
820 *
821 * **Behavior by AccessPattern:**
822 * - QueueAccess: Removes from front (oldest element)
823 * - HistoryBufferAccess: Not typically used (use peek_oldest instead)
824 *
825 * **Thread Safety:**
826 * - LockFreePolicy: Lock-free for single consumer
827 * - SingleThreadedPolicy: Not thread-safe
828 *
829 * @code{.cpp}
830 * while (auto value = buffer.pop()) {
831 * process(*value); // Process oldest data first
832 * }
833 * @endcode
834 */
835 [[nodiscard]] std::optional<T> pop() noexcept
836 {
837 if constexpr (is_mpsc) {
838 return pop_mpsc();
839 } else if constexpr (is_lock_free) {
840 return pop_lockfree();
841 } else {
842 return pop_singlethread();
843 }
844 }
845
846 /**
847 * @brief Peek at newest element without removing
848 * @return Reference to newest element
849 *
850 * Only available for SingleThreadedPolicy (HistoryBufferAccess).
851 * Returns element at index 0 in delay line ordering.
852 */
853 [[nodiscard]] const_reference peek_newest() const
854 requires(!is_lock_free && is_delay_line)
855 {
856 return m_storage.buffer[m_state.write_index];
857 }
858
859 /**
860 * @brief Peek at oldest element without removing
861 * @return Reference to oldest element
862 *
863 * Only available for SingleThreadedPolicy (HistoryBufferAccess).
864 * Returns element at highest valid index in delay line.
865 */
866 [[nodiscard]] const_reference peek_oldest() const
867 requires(!is_lock_free && is_delay_line)
868 {
869 const size_t count = size();
870 if (count == 0) {
871 return m_storage.buffer[m_state.write_index];
872 }
873
874 const size_t cap = m_storage.capacity();
875 size_t oldest_idx = (m_state.write_index + cap - count + 1) % cap;
876 return m_storage.buffer[oldest_idx];
877 }
878
879 /**
880 * @brief Access element by index (delay line style)
881 * @param index Distance from newest element (0 = newest)
882 * @return Reference to element
883 *
884 * Only available for SingleThreadedPolicy (HistoryBufferAccess).
885 * Provides array-like access where [0] is newest sample.
886 */
887 [[nodiscard]] const_reference operator[](size_t index) const
888 requires(!is_lock_free && is_delay_line)
889 {
890 const size_t cap = m_storage.capacity();
891 size_t actual_idx = (m_state.write_index + cap - index) % cap;
892 return m_storage.buffer[actual_idx];
893 }
894
895 /**
896 * @brief Overwrite newest element without advancing write position
897 * @param value New value for newest element
898 *
899 * Only available for SingleThreadedPolicy (HistoryBufferAccess).
900 * Useful for in-place modification of current sample.
901 *
902 * @code{.cpp}
903 * delay.push(input);
904 * delay.overwrite_newest(input * 0.5); // Modify current sample
905 * @endcode
906 */
907 void overwrite_newest(const T& value)
908 requires(!is_lock_free && is_delay_line)
909 {
910 m_storage.buffer[m_state.write_index] = value;
911 }
912
913 /**
914 * @brief Get linearized view of buffer contents
915 * @return Span ordered by AccessPattern
916 *
917 * Only available for SingleThreadedPolicy.
918 * Returns contiguous view of buffer data in logical order:
919 * - QueueAccess: [oldest → newest]
920 * - HistoryBufferAccess: [newest → oldest]
921 *
922 * **Not Real-time Safe**: Copies data to linear buffer.
923 * Use sparingly in audio processing paths.
924 */
925 [[nodiscard]] std::span<T> linearized_view() const
926 requires(!is_lock_free)
927 {
928 const size_t cap = m_storage.capacity();
929 const size_t count = size();
930
931 if constexpr (AccessPattern::push_front) {
932 for (size_t i = 0; i < count; ++i) {
933 size_t idx = (m_state.write_index + cap - i) % cap;
934 m_linearized[i] = m_storage.buffer[idx];
935 }
936 } else {
937 for (size_t i = 0; i < count; ++i) {
938 size_t idx = (m_state.read_index + i) % cap;
939 m_linearized[i] = m_storage.buffer[idx];
940 }
941 }
942
943 return { m_linearized.data(), count };
944 }
945
946 /**
947 * @brief Get mutable linearized view for modification
948 * @return Mutable span ordered by AccessPattern
949 *
950 * Same as linearized_view() but returns mutable references.
951 * Changes to span elements affect underlying buffer.
952 *
953 * @code{.cpp}
954 * auto history = delay.linearized_view_mut();
955 * for (auto& sample : history) {
956 * sample *= 0.9; // Apply gain reduction
957 * }
958 * @endcode
959 */
960 [[nodiscard]] std::span<T> linearized_view_mut()
961 requires(!is_lock_free)
962 {
963 const size_t cap = m_storage.capacity();
964 const size_t count = size();
965
966 if constexpr (AccessPattern::push_front) {
967 for (size_t i = 0; i < count; ++i) {
968 size_t idx = (m_state.write_index + cap - i) % cap;
969 m_linearized[i] = m_storage.buffer[idx];
970 }
971 } else {
972 for (size_t i = 0; i < count; ++i) {
973 size_t idx = (m_state.read_index + i) % cap;
974 m_linearized[i] = m_storage.buffer[idx];
975 }
976 }
977
978 return { m_linearized.data(), count };
979 }
980
981 /**
982 * @brief Thread-safe snapshot of buffer contents
983 * @return Vector copy ordered by AccessPattern
984 *
985 * Safe for lock-free contexts but allocates memory.
986 * For LockFreePolicy, provides consistent view despite concurrent access.
987 *
988 * **Not Real-time Safe**: Allocates std::vector.
989 *
990 * For MPSCPolicy, returns an empty vector. Use pop() to drain instead;
991 * a slot-by-slot index walk is not safe while producers are active.
992 *
993 * @code{.cpp}
994 * // Lock-free queue
995 * LockFreeQueue<Event, 1024> queue;
996 * auto events = queue.snapshot(); // Safe despite concurrent push
997 *
998 * for (const auto& event : events) {
999 * write_to_disk(event); // Can block safely
1000 * }
1001 * @endcode
1002 */
1003 [[nodiscard]] std::vector<T> snapshot() const
1004 {
1005 std::vector<T> result;
1006
1007 if constexpr (is_mpsc) {
1008 // snapshot() is not defined for MPSC - ready flags make a consistent
1009 // index-walk impossible without stopping producers. Use pop() to drain.
1010 return result;
1011 } else if constexpr (is_lock_free) {
1012 const size_t cap = m_storage.capacity();
1013 auto read = m_state.read_index.load(std::memory_order_acquire);
1014 auto write = m_state.write_index.load(std::memory_order_acquire);
1015
1016 result.reserve(cap);
1017 while (read != write) {
1018 result.push_back(m_storage.buffer[read]);
1019 read = State::increment(read, cap);
1020 }
1021 } else {
1022 auto view = linearized_view();
1023 result.assign(view.begin(), view.end());
1024 }
1025
1026 return result;
1027 }
1028
1029 /**
1030 * @brief Check if buffer is empty
1031 * @return true if no elements present
1032 */
1033 [[nodiscard]] bool empty() const noexcept
1034 {
1035 if constexpr (is_mpsc) {
1036 return m_state.read_index.load(std::memory_order_acquire)
1037 == m_state.claim_index.load(std::memory_order_acquire);
1038 } else if constexpr (is_lock_free) {
1039 return m_state.read_index.load(std::memory_order_acquire)
1040 == m_state.write_index.load(std::memory_order_acquire);
1041 } else {
1042 return m_state.read_index == m_state.write_index;
1043 }
1044 }
1045
1046 /**
1047 * @brief Get approximate element count
1048 * @return Number of elements in buffer
1049 *
1050 * For LockFreePolicy, value may be stale due to concurrent modification.
1051 * Use for debugging/monitoring only, not for critical logic.
1052 */
1053 [[nodiscard]] size_t size() const noexcept
1054 {
1055 const size_t cap = m_storage.capacity();
1056
1057 if constexpr (is_mpsc) {
1058 auto claim = m_state.claim_index.load(std::memory_order_acquire);
1059 auto read = m_state.read_index.load(std::memory_order_acquire);
1060 return (claim - read) & (cap - 1);
1061 } else if constexpr (is_lock_free) {
1062 auto write = m_state.write_index.load(std::memory_order_acquire);
1063 auto read = m_state.read_index.load(std::memory_order_acquire);
1064 return (write >= read) ? (write - read) : (cap - read + write);
1065 } else {
1066 return (m_state.write_index >= m_state.read_index)
1067 ? (m_state.write_index - m_state.read_index)
1068 : (cap - m_state.read_index + m_state.write_index);
1069 }
1070 }
1071
1072 /**
1073 * @brief Get buffer capacity
1074 * @return Maximum number of elements buffer can hold
1075 */
1076 [[nodiscard]] size_t capacity() const noexcept
1077 {
1078 return m_storage.capacity();
1079 }
1080
1081 /**
1082 * @brief Resize buffer capacity (DynamicStorage only)
1083 * @param new_capacity New maximum element count
1084 *
1085 * Preserves existing data ordered by AccessPattern.
1086 * **Not Real-time Safe**: May trigger heap allocation.
1087 */
1088 void resize(size_t new_capacity)
1089 requires(is_resizable)
1090 {
1091 if (new_capacity == m_storage.capacity())
1092 return;
1093
1094 auto current_data = snapshot();
1095
1096 m_storage.resize(new_capacity);
1097 m_linearized.resize(new_capacity);
1098
1099 m_state.write_index = 0;
1100 m_state.read_index = 0;
1101
1102 size_t to_copy = std::min(current_data.size(), new_capacity);
1103 for (size_t i = 0; i < to_copy; ++i) {
1104 m_storage.buffer[i] = current_data[i];
1105 }
1106
1107 if constexpr (AccessPattern::push_front) {
1108 m_state.write_index = (to_copy > 0) ? to_copy - 1 : 0;
1109 } else {
1110 m_state.write_index = to_copy;
1111 }
1112 }
1113
1114 /**
1115 * @brief Clear buffer contents and reset indices
1116 *
1117 * **Real-time Safe**: No allocations, just resets atomic indices.
1118 */
1119 void reset() noexcept
1120 {
1121 if constexpr (is_mpsc) {
1122 m_state.claim_index.store(0, std::memory_order_release);
1123 m_state.read_index.store(0, std::memory_order_release);
1124 for (auto& f : m_state.ready_flags)
1125 f.store(false, std::memory_order_release);
1126 } else if constexpr (is_lock_free) {
1127 m_state.write_index.store(0, std::memory_order_release);
1128 m_state.read_index.store(0, std::memory_order_release);
1129 } else {
1130 m_state.write_index = 0;
1131 m_state.read_index = 0;
1132 }
1133 }
1134
1135private:
1136 [[nodiscard]] bool push_lockfree(const T& value) noexcept
1137 {
1138 const size_t cap = m_storage.capacity();
1139 auto write = m_state.write_index.load(std::memory_order_relaxed);
1140 auto next_write = State::increment(write, cap);
1141
1142 if (next_write == m_state.read_index.load(std::memory_order_acquire)) {
1143 return false;
1144 }
1145
1146 m_storage.buffer[write] = value;
1147 m_state.write_index.store(next_write, std::memory_order_release);
1148
1149 return true;
1150 }
1151
1152 [[nodiscard]] std::optional<T> pop_lockfree() noexcept
1153 {
1154 auto read = m_state.read_index.load(std::memory_order_relaxed);
1155
1156 if (read == m_state.write_index.load(std::memory_order_acquire)) {
1157 return std::nullopt;
1158 }
1159
1160 T value = m_storage.buffer[read];
1161 m_state.read_index.store(
1162 State::increment(read, m_storage.capacity()),
1163 std::memory_order_release);
1164
1165 return value;
1166 }
1167
1168 [[nodiscard]] bool push_singlethread(const T& value) noexcept
1169 {
1170 const size_t cap = m_storage.capacity();
1171 auto next_write = State::increment(m_state.write_index, cap);
1172
1173 if (next_write == m_state.read_index) {
1174 return false;
1175 }
1176
1177 if constexpr (AccessPattern::push_front) {
1178 m_state.write_index = (m_state.write_index == 0)
1179 ? cap - 1
1180 : m_state.write_index - 1;
1181 m_storage.buffer[m_state.write_index] = value;
1182 } else {
1183 m_storage.buffer[m_state.write_index] = value;
1184 m_state.write_index = next_write;
1185 }
1186
1187 return true;
1188 }
1189
1190 [[nodiscard]] std::optional<T> pop_singlethread() noexcept
1191 {
1192 if (m_state.read_index == m_state.write_index) {
1193 return std::nullopt;
1194 }
1195
1196 T value = m_storage.buffer[m_state.read_index];
1197 m_state.read_index = State::increment(m_state.read_index, m_storage.capacity());
1198
1199 return value;
1200 }
1201
1202 [[nodiscard]] bool push_mpsc(const T& value) noexcept
1203 {
1204 const size_t cap = m_storage.capacity();
1205
1206 size_t slot = m_state.claim_index.fetch_add(1, std::memory_order_relaxed) & (cap - 1);
1207
1208 if (m_state.ready_flags[slot].load(std::memory_order_acquire))
1209 return false;
1210
1211 m_storage.buffer[slot] = value;
1212 m_state.ready_flags[slot].store(true, std::memory_order_release);
1213
1214 return true;
1215 }
1216
1217 [[nodiscard]] std::optional<T> pop_mpsc() noexcept
1218 {
1219 const size_t cap = m_storage.capacity();
1220 const size_t slot = m_state.read_index.load(std::memory_order_relaxed);
1221
1222 if (!m_state.ready_flags[slot].load(std::memory_order_acquire))
1223 return std::nullopt;
1224
1225 T value = m_storage.buffer[slot];
1226
1227 m_state.ready_flags[slot].store(false, std::memory_order_release);
1228 m_state.read_index.store(State::increment(slot, cap), std::memory_order_release);
1229
1230 return value;
1231 }
1232
1233 StoragePolicy m_storage;
1235 mutable std::vector<T> m_linearized;
1236};
1237
1238/**
1239 * @brief Type alias: Lock-free SPSC queue with fixed capacity
1240 * @tparam T Element type (must be TriviallyCopyable)
1241 * @tparam Capacity Buffer size (must be power of 2)
1242 *
1243 * Common configuration for real-time producer → non-realtime consumer.
1244 *
1245 * **Use Cases:**
1246 * - Input event queues (MIDI, OSC, HID)
1247 * - Real-time logging buffers
1248 * - Audio thread → worker thread communication
1249 *
1250 * **Example:**
1251 * @code{.cpp}
1252 * // Replace: Memory::LockFreeRingBuffer<InputValue, 4096>
1253 * LockFreeQueue<InputValue, 4096> input_queue;
1254 *
1255 * // Audio thread (realtime)
1256 * input_queue.push(event); // Wait-free
1257 *
1258 * // Worker thread (non-realtime)
1259 * while (auto event = input_queue.pop()) {
1260 * process_event(*event);
1261 * }
1262 * @endcode
1263 */
1264template <typename T, size_t Capacity>
1268 QueueAccess>;
1269
1270/**
1271 * @brief Type alias: Lock-free MPSC queue with fixed capacity.
1272 * @tparam T Element type.
1273 * @tparam Capacity Buffer size (must be power of 2).
1274 *
1275 * Multiple concurrent producers, single consumer. Each producer claims a
1276 * slot atomically; the consumer reads in claim order. Use for any context
1277 * where multiple threads or RT callers write to a single draining worker.
1278 */
1279template <typename T, size_t Capacity>
1282 MPSCPolicy,
1283 QueueAccess>;
1284
1285/**
1286 * @brief Type alias: Single-threaded FIFO queue with fixed capacity
1287 * @tparam T Element type
1288 * @tparam Capacity Buffer size (must be power of 2)
1289 *
1290 * For non-concurrent queue usage with compile-time capacity.
1291 */
1292template <typename T, size_t Capacity>
1296 QueueAccess>;
1297
1298/**
1299 * @brief Type alias: Resizable FIFO queue (non-concurrent)
1300 * @tparam T Element type
1301 *
1302 * For non-concurrent queue usage with runtime capacity.
1303 */
1304template <typename T>
1308 QueueAccess>;
1309
1310} // namespace MayaFlux::Memory
size_t count
float value
const_reference oldest() const
void resize(size_t new_capacity)
Resize buffer capacity.
void restore_state(const std::vector< T > &state)
Restore previously saved state.
std::span< T > linearized_view()
Get mutable linearized view of entire history.
reference operator[](size_t index)
Access element by temporal offset.
std::span< const T > linearized_view() const
Get const linearized view.
bool empty() const
Check if buffer is empty (always false for HistoryBuffer)
void overwrite_newest(const T &value)
Overwrite the newest element without advancing position.
void push(const T &value)
Push new value to front of history.
reference oldest()
Get oldest element (same as [capacity-1])
void reset()
Reset buffer to initial state (all zeros)
const_reference newest() const
HistoryBuffer(size_t capacity)
Construct history buffer with specified capacity.
void update(size_t index, const T &value)
Update element at specific index.
const_reference operator[](size_t index) const
size_t capacity() const
Get buffer capacity.
size_t size() const
Get current count (always equals capacity for HistoryBuffer)
std::vector< T > save_state() const
Save current state for later restoration.
void set_initial_conditions(const std::vector< T > &values)
Set initial conditions.
reference newest()
Get newest element (same as [0])
History buffer for difference equations and recursive relations.
RingBuffer()=default
Default construct ring buffer (FixedStorage only) Capacity determined by template parameter.
bool push_mpsc(const T &value) noexcept
const_reference peek_oldest() const
Peek at oldest element without removing.
void reset() noexcept
Clear buffer contents and reset indices.
bool push_lockfree(const T &value) noexcept
RingBuffer(size_t initial_capacity=64)
Construct ring buffer with runtime capacity (DynamicStorage only)
std::span< T > linearized_view() const
Get linearized view of buffer contents.
std::optional< T > pop_singlethread() noexcept
static constexpr bool is_resizable
bool push(const T &value) noexcept
Push element into buffer.
size_t size() const noexcept
Get approximate element count.
std::span< T > linearized_view_mut()
Get mutable linearized view for modification.
const_reference peek_newest() const
Peek at newest element without removing.
static constexpr bool is_lock_free
std::optional< T > pop_mpsc() noexcept
std::optional< T > pop_lockfree() noexcept
std::optional< T > pop() noexcept
Pop element from buffer.
static constexpr bool is_mpsc
bool empty() const noexcept
Check if buffer is empty.
bool push_singlethread(const T &value) noexcept
void resize(size_t new_capacity)
Resize buffer capacity (DynamicStorage only)
void overwrite_newest(const T &value)
Overwrite newest element without advancing write position.
std::vector< T > snapshot() const
Thread-safe snapshot of buffer contents.
typename ConcurrencyPolicy::template State< T, StoragePolicy > State
size_t capacity() const noexcept
Get buffer capacity.
static constexpr bool is_delay_line
const_reference operator[](size_t index) const
Access element by index (delay line style)
Policy-driven unified circular buffer implementation.
Constraint for types that can be safely copied bitwise.
static constexpr bool policy_is_mpsc< Policy >
Specialization for MPSCPolicy.
static constexpr bool policy_is_mpsc
Helper to detect if a policy is MPSC.
size_t capacity() const noexcept
void resize(size_t new_capacity)
static constexpr bool is_resizable
DynamicStorage(size_t initial_capacity=64)
Runtime-resizable storage using std::vector.
std::array< T, Capacity > storage_type
static constexpr size_t capacity_value
static constexpr bool is_resizable
constexpr size_t capacity() const noexcept
Compile-time fixed-capacity storage using std::array.
static constexpr const char * name
History buffer semantics (newest sample first)
static constexpr size_t increment(size_t index, size_t capacity) noexcept
static constexpr bool is_thread_safe
static constexpr bool requires_trivial_copyable
static constexpr bool requires_fixed_storage
Lock-free SPSC (Single Producer Single Consumer) concurrency.
std::array< std::atomic< bool >, N > ready_flags
std::atomic< size_t > claim_index
static constexpr size_t increment(size_t index, size_t capacity) noexcept
static constexpr bool is_thread_safe
static constexpr bool requires_fixed_storage
static constexpr bool requires_trivial_copyable
static constexpr bool is_mpsc
Lock-free MPSC (Multi-Producer Single-Consumer) concurrency.
static constexpr const char * name
static constexpr bool pop_front
static constexpr bool push_front
FIFO queue semantics (oldest data first)
static constexpr size_t increment(size_t index, size_t capacity) noexcept
static constexpr bool requires_trivial_copyable
static constexpr bool requires_fixed_storage
Non-atomic single-threaded operation.