Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
triple_buffer.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <atomic>
7#include <cassert>
8#include <array>
9#include <cstdint>
10#include <type_traits>
11#include <utility>
12
13namespace pulp::runtime {
14
44template <typename T>
46public:
47 static_assert(std::atomic<std::uint8_t>::is_always_lock_free,
48 "TripleBuffer publication flags must be lock-free");
49
50 TripleBuffer() = default;
51
53 explicit TripleBuffer(const T& initial) {
54 buffers_[0] = initial;
55 buffers_[1] = initial;
56 buffers_[2] = initial;
57 }
58
62 void write(const T& value) noexcept(std::is_nothrow_copy_assignable_v<T>) {
64 [&value](T& back) noexcept(std::is_nothrow_copy_assignable_v<T>) { back = value; });
65 }
66
72 template <typename Fill>
73 void write_with(Fill&& fill) noexcept(noexcept(std::declval<Fill>()(std::declval<T&>()))) {
74 [[maybe_unused]] const RoleGuard guard(active_roles_, kWriterRole);
75 std::forward<Fill>(fill)(buffers_[back_]);
76 // One exchange publishes the filled buffer as the middle slot and
77 // hands the previous middle slot back to the writer. Release makes the
78 // fill visible to the reader's acquire; acquire orders the reader's
79 // last use of the returned slot before the writer refills it.
80 const auto previous = middle_.exchange(static_cast<std::uint8_t>(back_ | kDirtyBit),
81 std::memory_order_acq_rel);
82 back_ = previous & kIndexMask;
83 }
84
88 const T& read() noexcept {
89 [[maybe_unused]] const RoleGuard guard(active_roles_, kReaderRole);
90 if (middle_.load(std::memory_order_relaxed) & kDirtyBit) {
91 const auto previous = middle_.exchange(front_, std::memory_order_acq_rel);
92 front_ = previous & kIndexMask;
93 }
94 return buffers_[front_];
95 }
96
97 private:
98 // middle_ holds the index of the slot between writer and reader, plus a
99 // dirty bit set by the writer and cleared by the reader's swap. back_ is
100 // owned by the writer thread and front_ by the reader thread; neither is
101 // shared, so neither needs to be atomic.
102 static constexpr std::uint8_t kDirtyBit = 0x80;
103 static constexpr std::uint8_t kIndexMask = 0x03;
104
105 // Debug-only overlap check for the one-writer / one-reader contract. The
106 // flag byte is present in every build so the layout does not depend on
107 // NDEBUG; release builds never touch it.
108 static constexpr std::uint8_t kWriterRole = 0x01;
109 static constexpr std::uint8_t kReaderRole = 0x02;
110 struct RoleGuard {
111#ifndef NDEBUG
112 RoleGuard(std::atomic<std::uint8_t>& roles, std::uint8_t role) noexcept
113 : roles_(roles), role_(role) {
114 const auto before = roles_.fetch_or(role_, std::memory_order_acquire);
115 assert((before & role_) == 0 && "TripleBuffer: two overlapping calls in the same role");
116 (void)before;
117 }
118 ~RoleGuard() {
119 roles_.fetch_and(static_cast<std::uint8_t>(~role_), std::memory_order_release);
120 }
121 std::atomic<std::uint8_t>& roles_;
122 std::uint8_t role_;
123#else
124 RoleGuard(std::atomic<std::uint8_t>&, std::uint8_t) noexcept {}
125#endif
126 };
127
128 std::array<T, 3> buffers_{};
129 std::atomic<std::uint8_t> middle_{1};
130 std::uint8_t back_ = 0;
131 std::uint8_t front_ = 2;
132 std::atomic<std::uint8_t> active_roles_{0};
133};
134
135} // namespace pulp::runtime
TripleBuffer(const T &initial)
Initialize all three internal buffers with the same value.
Definition triple_buffer.hpp:53
const T & read() noexcept
Read the latest published value from the reader thread.
Definition triple_buffer.hpp:88
void write_with(Fill &&fill) noexcept(noexcept(std::declval< Fill >()(std::declval< T & >())))
Fill and publish the writer-owned back buffer in place.
Definition triple_buffer.hpp:73
void write(const T &value) noexcept(std::is_nothrow_copy_assignable_v< T >)
Publish a new value from the writer thread.
Definition triple_buffer.hpp:62
Definition generated_event_source.hpp:33