Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
graph_runtime_worker_pool.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <cassert>
5#include <cstdint>
6#include <memory>
7#include <thread>
8#include <vector>
9
10namespace pulp::format {
11
33public:
34 using TaskFn = void (*)(void* context, std::uint32_t task_index) noexcept;
35
36 // Private diagnostic seam. The hook is inert unless a focused test or
37 // diagnostic campaign installs it before start(); it is never part of the
38 // graph executor's scheduling contract. Timestamps use the worker's local
39 // monotonic clock and preserve the pool epoch that published the batch.
47 std::uint64_t generation = 0;
48 std::uint32_t worker_index = 0;
49 std::uint32_t task_count = 0;
50 std::uint64_t timestamp_ns = 0;
51 };
52 using SchedulingReceiptHook = void (*)(const SchedulingReceipt&, void*) noexcept;
53
56
59
60 // Off-RT: spawn `worker_count` total participants (worker_count - 1 threads;
61 // the caller of run() is the worker_count-th). worker_count <= 1 means run()
62 // executes everything inline on the caller with no threads. Re-start() after
63 // stop(). Returns false on an invalid count or thread-spawn failure.
64 // PULP_WASM refuses counts above one without stopping an existing pool.
65 bool start(std::uint32_t worker_count);
66
67 // Off-RT: signal and join all worker threads. Idempotent.
68 void stop() noexcept;
69
70 // RT-safe: publish an optional platform audio workgroup handle. May be
71 // called before start() or when the render context changes. Each worker
72 // leaves its old group and joins the new one on its own thread before it
73 // participates in another batch. A null device handle preserves the
74 // best-effort realtime-priority fallback used when no OS workgroup exists.
75 void set_audio_workgroup(void* workgroup) noexcept;
76
77 // AU observer path: coherent O(1) publication. The raw workgroup is borrowed
78 // and is adopted by every worker in the synchronous triggering-render
79 // barrier before the host may retire the preceding context.
80 void set_audio_workgroup_from_render_context(void* workgroup) noexcept;
81
82 // RT-safe full-participant transition barrier. Every worker leaves its old
83 // context and acknowledges the new publication before this returns, even
84 // when it was cold. False means at least one new join failed and subsequent
85 // graph work must remain inline for this publication.
87
88 // Off-RT: wait until every worker has left/joined the last publication.
89 // Used before an owner invalidates a borrowed workgroup handle.
91
92 void* configured_audio_workgroup() const noexcept {
93 return current_audio_workgroup_publication().workgroup;
94 }
96
97 std::uint32_t worker_count() const noexcept { return worker_count_; }
98 bool running() const noexcept { return running_.load(std::memory_order_acquire); }
99 std::uint64_t worker_backoff_count() const noexcept {
100 return worker_backoff_count_.load(std::memory_order_relaxed);
101 }
102 std::uint64_t worker_idle_sleep_count() const noexcept {
103 return worker_idle_sleep_count_.load(std::memory_order_relaxed);
104 }
105 std::uint64_t worker_park_count() const noexcept {
107 }
108
109#ifdef PULP_GRAPH_RUNTIME_WORKER_POOL_TEST_HOOKS
110 // Install before start(). The hook receives publication, worker-admission,
111 // and inline-fallback events from the existing pool seam. The callback must
112 // be allocation-free and safe for concurrent worker/audio invocations.
113 void set_scheduling_receipt_hook_for_test(SchedulingReceiptHook hook, void* context) noexcept {
114 assert(!running_.load(std::memory_order_acquire));
115 scheduling_receipt_hook_for_test_ = hook;
116 scheduling_receipt_context_for_test_ = context;
117 }
118 // Install before start(). This intercepts only the no-handle fallback join,
119 // allowing tests to prove that device-null attempts fallback while AU-null
120 // removal does not. Production workgroup joins are never redirected.
121 void set_fallback_join_hook_for_test(bool (*hook)(void*) noexcept,
122 void* context) noexcept {
123 assert(!running_.load(std::memory_order_acquire));
124 fallback_join_hook_for_test_ = hook;
125 fallback_join_context_for_test_ = context;
126 }
127 bool configured_audio_workgroup_uses_fallback_for_test() const noexcept {
128 return current_audio_workgroup_publication().fallback_when_null;
129 }
130 // Install before start(). Invoked on the worker thread once per cold-idle
131 // episode, immediately after that worker has published the park, so an
132 // observer can block on the transition itself instead of polling for it.
133 // Polling competes for the cores the workers need to reach the transition.
134 void set_worker_park_hook_for_test(void (*hook)(void*) noexcept,
135 void* context) noexcept {
136 assert(!running_.load(std::memory_order_acquire));
137 worker_park_hook_ = hook;
138 worker_park_context_ = context;
139 }
140 // Install before start(). Workers never take the cold-idle path, so run()
141 // always finds every worker hot and dispatches instead of running inline.
142 // A test that asserts worker admission needs this: without it, a caller
143 // descheduled for longer than the hot idle window lets a worker park, and
144 // run() then correctly falls back to inline execution.
145 void keep_workers_hot_for_test() noexcept {
146 assert(!running_.load(std::memory_order_acquire));
147 keep_workers_hot_for_test_ = true;
148 }
149 // Claim the cold-transition gate from the control thread. While it is held
150 // no worker can park (each park CAS fails and the worker keeps spinning),
151 // which is how a test builds a pool that provably never idles. run() and
152 // prepare_audio_workgroup_for_render() must not be called while it is held.
153 bool try_hold_cold_transition_gate_for_test() noexcept {
154 bool expected = false;
155 return cold_transition_gate_.compare_exchange_strong(
156 expected, true, std::memory_order_acq_rel, std::memory_order_acquire);
157 }
158 void release_cold_transition_gate_for_test() noexcept {
159 cold_transition_gate_.store(false, std::memory_order_release);
160 }
161#ifndef NDEBUG
162 void pause_workgroup_transition_for_test() noexcept {
163 transition_pause_released_for_test_.store(false, std::memory_order_release);
164 transition_pause_reached_for_test_.store(false, std::memory_order_release);
165 transition_pause_enabled_for_test_.store(true, std::memory_order_release);
166 }
167 bool workgroup_transition_pause_reached_for_test() const noexcept {
168 return transition_pause_reached_for_test_.load(std::memory_order_acquire);
169 }
170 void release_workgroup_transition_for_test() noexcept {
171 transition_pause_released_for_test_.store(true, std::memory_order_release);
172 }
173 void clear_workgroup_transition_pause_for_test() noexcept {
174 transition_pause_enabled_for_test_.store(false, std::memory_order_release);
175 }
176#endif
177 void seed_reheat_state_for_test(std::uint32_t worker_count,
178 std::uint32_t active_worker_threads,
179 bool reheat_requested) noexcept {
180 worker_count_ = worker_count;
181 active_worker_threads_.store(active_worker_threads, std::memory_order_release);
182 reheat_requested_.store(reheat_requested, std::memory_order_release);
183 }
184 void clear_transient_reheat_if_no_worker_cold_for_test() noexcept {
185 clear_transient_reheat_if_no_worker_cold();
186 }
187 bool reheat_requested_for_test() const noexcept {
188 return reheat_requested_.load(std::memory_order_acquire);
189 }
190#endif
191
192 // RT-safe: run `fn(context, i)` for every i in [0, task_count) exactly once,
193 // distributed across the pool threads and the calling thread, and return only
194 // after all have completed. Allocation-free and lock-free. With no worker
195 // threads (worker_count <= 1) it simply runs them inline on the caller.
196 void run(std::uint32_t task_count, TaskFn fn, void* context) noexcept;
197
198private:
199 struct AudioWorkgroupPublication {
200 void* workgroup = nullptr;
201 std::uint64_t generation = 0;
202 bool fallback_when_null = true;
203 };
204
205 AudioWorkgroupPublication current_audio_workgroup_publication() const noexcept;
206
207 public:
208#if defined(PULP_GRAPH_RUNTIME_WORKER_POOL_TEST_HOOKS)
209 // One coherent read of the workgroup publication (pointer, generation and
210 // null policy), so a test can check that no reader ever combines fields
211 // from two different publications.
212 AudioWorkgroupPublication audio_workgroup_publication_for_test() const noexcept {
213 return current_audio_workgroup_publication();
214 }
215#endif
216
217 private:
218 bool workers_acknowledged_current_audio_workgroup() const noexcept;
219 void publish_audio_workgroup(void* workgroup,
220 bool fallback_when_null) noexcept;
221 void worker_loop(std::uint32_t worker_index) noexcept;
222 void clear_transient_reheat_if_no_worker_cold() noexcept;
223 void publish_scheduling_receipt(SchedulingReceiptPhase phase, std::uint64_t generation,
224 std::uint32_t worker_index, std::uint32_t task_count) noexcept;
225 // Run this worker's STATIC contiguous task range for the current batch and
226 // add its size to the completion counter. Static partitioning (no shared
227 // claim cursor) means a lagging worker can never re-claim tasks after run()
228 // publishes the next batch — run() returns only once completed_ == task_count
229 // (every worker's add is done), so the next batch's counter reset is safe.
230 void run_range(std::uint32_t worker_index) noexcept;
231
232 std::vector<std::thread> threads_;
233 std::uint32_t worker_count_ = 0;
234 // Single-writer seqlock publication. AU's render-context observer is called
235 // serially on the render thread; standalone device changes are serialized
236 // by CoreAudioDevice::switch_mutex_. An odd sequence means the pointer is
237 // being changed, and an even sequence identifies one immutable snapshot.
238 // Workers never combine a pointer or null policy from publication N with
239 // N +/- 1's generation, including under rapid A -> B -> null -> A changes.
240 std::atomic<std::uint64_t> audio_workgroup_sequence_{2};
241 std::atomic<void*> audio_workgroup_{nullptr};
242 std::atomic<bool> audio_workgroup_fallback_when_null_{true};
243 std::unique_ptr<std::atomic<std::uint64_t>[]> worker_workgroup_generation_;
244 std::unique_ptr<std::atomic<bool>[]> worker_workgroup_join_succeeded_;
245
246 // Immutable while workers are running. The setter is exposed only to the
247 // focused test target, but these fields remain in every build so the class
248 // layout cannot differ between the library and its consumer.
249 bool (*fallback_join_hook_for_test_)(void*) noexcept = nullptr;
250 void* fallback_join_context_for_test_ = nullptr;
251 void (*worker_park_hook_)(void*) noexcept = nullptr;
252 void* worker_park_context_ = nullptr;
253 bool keep_workers_hot_for_test_ = false;
254 // Kept in every build so test-hook and production class layouts remain
255 // identical. The pointer is null in every shipping path.
256 SchedulingReceiptHook scheduling_receipt_hook_for_test_ = nullptr;
257 void* scheduling_receipt_context_for_test_ = nullptr;
258
259 // Published batch (valid for the current epoch). Written by run() before the
260 // epoch bump (release), read by workers after observing the new epoch
261 // (acquire). run() must not notify parked OS waiters. If not every worker is
262 // hot, run() executes inline and lets sleepers reheat on their next cold-idle
263 // tick.
264 TaskFn fn_ = nullptr;
265 void* context_ = nullptr;
266 std::uint32_t task_count_ = 0;
267
268 // Epoch a worker polls; bumped by run() to publish a batch. stop() bumps it
269 // with `stopping_` set so workers exit.
270 alignas(64) std::atomic<std::uint64_t> epoch_{0};
271 alignas(64) std::atomic<bool> stopping_{false};
272 alignas(64) std::atomic<bool> running_{false};
273 alignas(64) std::atomic<bool> cold_transition_gate_{false};
274 alignas(64) std::atomic<bool> reheat_requested_{false};
275 // A workgroup transition uses a tagged epoch distinct from task epochs.
276 // Every worker, including a cold sleeper, must observe and acknowledge it;
277 // this is what lets an AU host retire the preceding borrowed context when
278 // the triggering render returns.
279 alignas(64) std::atomic<std::uint64_t> workgroup_prepare_epoch_{0};
280 alignas(64) std::atomic<std::uint32_t> active_worker_threads_{0};
281
282 // MONOTONIC count of tasks completed across all batches (each worker adds its
283 // range size with release; the caller acquire-waits until it reaches the
284 // batch target). Never reset — a reset would race a lagging worker's add from
285 // the previous batch. 64-bit so it never wraps in a long session.
286 alignas(64) std::atomic<std::uint64_t> completed_{0};
287 // The value of completed_ before the current batch (caller-only scratch, so
288 // the per-batch target is completed_base_ + task_count_).
289 std::uint64_t completed_base_ = 0;
290
291 // Debug-only guard asserting run() is never called re-entrantly / from two
292 // threads at once — the load-bearing invariant the lock-free barrier relies
293 // on. Compiled out (and never touched) in release builds.
294 alignas(64) std::atomic<bool> in_run_{false};
295 alignas(64) std::atomic<std::uint64_t> worker_backoff_count_{0};
296 alignas(64) std::atomic<std::uint64_t> worker_idle_sleep_count_{0};
297#ifndef NDEBUG
298 // Deterministic race-test seam, absent from Release builds and checked only
299 // on the rare workgroup-transition path.
300 std::atomic<bool> transition_pause_enabled_for_test_{false};
301 std::atomic<bool> transition_pause_reached_for_test_{false};
302 std::atomic<bool> transition_pause_released_for_test_{true};
303#endif
304};
305
306} // namespace pulp::format
std::uint64_t worker_backoff_count() const noexcept
Definition graph_runtime_worker_pool.hpp:99
void run(std::uint32_t task_count, TaskFn fn, void *context) noexcept
std::uint64_t worker_park_count() const noexcept
Definition graph_runtime_worker_pool.hpp:105
std::uint64_t worker_idle_sleep_count() const noexcept
Definition graph_runtime_worker_pool.hpp:102
bool workers_use_current_audio_workgroup() const noexcept
void(*)(const SchedulingReceipt &, void *) noexcept SchedulingReceiptHook
Definition graph_runtime_worker_pool.hpp:52
std::uint32_t worker_count() const noexcept
Definition graph_runtime_worker_pool.hpp:97
void(*)(void *context, std::uint32_t task_index) noexcept TaskFn
Definition graph_runtime_worker_pool.hpp:34
void * configured_audio_workgroup() const noexcept
Definition graph_runtime_worker_pool.hpp:92
void wait_for_audio_workgroup_update() noexcept
bool running() const noexcept
Definition graph_runtime_worker_pool.hpp:98
GraphRuntimeWorkerPool(const GraphRuntimeWorkerPool &)=delete
bool start(std::uint32_t worker_count)
void set_audio_workgroup_from_render_context(void *workgroup) noexcept
void set_audio_workgroup(void *workgroup) noexcept
SchedulingReceiptPhase
Definition graph_runtime_worker_pool.hpp:40
@ InlineFallback
Definition graph_runtime_worker_pool.hpp:43
@ WorkerAdmission
Definition graph_runtime_worker_pool.hpp:42
@ Published
Definition graph_runtime_worker_pool.hpp:41
GraphRuntimeWorkerPool & operator=(const GraphRuntimeWorkerPool &)=delete
bool prepare_audio_workgroup_for_render() noexcept
What the product grants a device: the quota table indexed by capability tier and live thermal state.
Definition aax_editor.hpp:31
@ vector
a block of samples for a scope-style display
Definition editor_host.hpp:237
STL namespace.
Definition graph_runtime_worker_pool.hpp:45
std::uint64_t generation
Definition graph_runtime_worker_pool.hpp:47
std::uint32_t worker_index
Definition graph_runtime_worker_pool.hpp:48
std::uint32_t task_count
Definition graph_runtime_worker_pool.hpp:49
SchedulingReceiptPhase phase
Definition graph_runtime_worker_pool.hpp:46
std::uint64_t timestamp_ns
Definition graph_runtime_worker_pool.hpp:50