Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
gpu_audio_transport.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <cstdint>
6#include <memory>
7#include <mutex>
8#include <semaphore>
9#include <thread>
10#include <vector>
11
12#include <pulp/audio/buffer.hpp>
18
19namespace pulp::gpu_audio {
20
21namespace detail {
22struct RealtimeGpuHealth;
23struct GpuAudioTransportDiagnostics;
24struct SharedIoCompletionNotifier;
25class WorkerWake;
26} // namespace detail
27
42 public:
43 // The node's descriptor is the single source of truth for channels, block
44 // size, and latency. Config only carries transport knobs.
45 struct Config {
46 uint32_t ring_blocks = 4; // ring capacity, in blocks (>= latency+2)
47 // Spawn an internal non-RT worker thread that drives pump(). By default
48 // the worker polls the input ring, so the RT path stays fully decoupled
49 // and lock-free. When false, the caller drives pump() (deterministic
50 // tests / custom worker integration).
51 bool run_worker_thread = false;
52 // Opt-in: the RT process() posts a semaphore after each input write,
53 // and a provider that installs a completion notifier posts it when a
54 // completion may be ready. The worker waits on it, with the poll
55 // interval as a fallback timeout only, instead of sleeping a full
56 // quantum. Posting never allocates or blocks, but while the worker is
57 // parked it is a kernel call (a Mach semaphore signal on macOS, the
58 // platform's semaphore wake elsewhere) on the audio thread every
59 // block, so it is off by default and the default RT path makes no
60 // syscall. No shipped provider
61 // installs a completion notifier yet, so today the worker still finds
62 // completions by polling. Only meaningful when run_worker_thread is true.
63 bool wake_on_write = false;
64 // Optional macOS Audio Workgroup for the owned submission worker. The
65 // handle is borrowed by the transport and must remain valid until
66 // release(). This is an experiment switch: Dawn encode/submit still
67 // runs on the worker and is not claimed to be realtime-safe.
68 void* audio_workgroup = nullptr;
81 std::uint64_t reacquire_release_budget_ns = 250'000'000;
85 std::uint32_t reacquire_max_attempts = 3;
91 std::uint64_t reacquire_backoff_ns = 100'000'000;
92 };
93
94 struct Stats {
95 std::uint64_t produced_blocks = 0; // blocks the worker completed
96 std::uint64_t miss_blocks = 0; // RT reads with no ready output
97 std::uint64_t input_dropped_frames = 0; // RT writes lost to a full input ring
98 // Wet blocks the worker dropped to realign the stream after a miss. Each
99 // miss already emitted a substitute (dry/fallback) block for that timeline
100 // slot, so its late-arriving wet counterpart is redundant; dropping it
101 // keeps effective latency pinned at latency_blocks instead of creeping one
102 // block per miss (which would comb-filter dry against wet).
103 std::uint64_t resynced_blocks = 0;
104 // Worker-observed wall time. The staged path measures one process_block
105 // call, including any blocking readback. The shared path measures one
106 // service call that reports progress; it may cover several completions.
107 // Neither interval is GPU execution time, CPU consumption, or an audio
108 // deadline measurement. last is the latest recorded interval; avg is an
109 // EWMA. Both remain zero until the worker first reports progress.
110 double last_block_us = 0.0;
111 double avg_block_us = 0.0;
114 // Appended: the owned worker runs at QOS_CLASS_USER_INTERACTIVE
115 // (macOS; false when it joined a host workgroup instead, or off Apple).
117 };
118
124 std::uint64_t gpu_blocks = 0; // callback-side GPU path selected ready output
125 // A generic node may implement process_gpu() using CPU work or internal
126 // fallback. A ring delivery alone cannot establish GPU execution.
127 std::uint64_t worker_output_blocks = 0;
128 std::uint64_t cpu_fallback_blocks = 0;
129 std::uint64_t silence_blocks = 0;
130 std::uint64_t passthrough_blocks = 0;
131 std::uint64_t priming_blocks = 0;
132 std::uint64_t invalid_blocks = 0;
133 };
134
137
140
152 bool prepare(GpuAudioNode* node, const Config& config);
153 void release() noexcept;
154
155 bool is_prepared() const noexcept {
156 return prepared_;
157 }
158 uint32_t latency_samples() const noexcept {
159 return latency_blocks_ * block_size_;
160 }
161
170 uint32_t n) noexcept;
171
187 audio::BufferView<float>& output, uint32_t n) noexcept;
188
191 void pump(uint32_t max_blocks = 0) noexcept;
192
193 Stats stats() const noexcept;
194
209
216
223
224 private:
225 // Undo the worker-pipelining opt-in prepare() bound, if any. Host thread,
226 // no worker running.
227 void release_pipeline_hosting() noexcept;
228 using TrialDeliveryFn = void (*)(void*, std::uint64_t, std::uint8_t, std::uint64_t,
229 std::uint64_t, std::uint64_t) noexcept;
231 TrialDeliveryFn) noexcept;
232
233 GpuAudioNode* node_ = nullptr;
234 // Derived from the node descriptor + config at prepare(); authoritative for
235 // the RT path so it never calls the (allocating) descriptor().
236 uint32_t channels_ = 0;
237 uint32_t block_size_ = 0;
238 uint32_t sample_rate_ = 0;
239 uint32_t latency_blocks_ = 0;
240 uint32_t ring_blocks_ = 0;
241 // Fails closed until prepare() copies the node's declared policy in.
242 MissPolicy miss_policy_ = MissPolicy::Silence;
243 bool prepared_ = false;
244
245 void reset_staged_transport_state() noexcept;
246 // Callback timeline only. Records whether `sequence`'s input block was
247 // dropped and returns whether the block due now (`latency_blocks` earlier)
248 // was.
249 bool record_dropped_slot(std::uint64_t sequence, bool dropped) noexcept;
250 void process_shared(const audio::BufferView<const float>&, audio::BufferView<float>&,
251 std::uint32_t, std::uint64_t, bool input_valid,
252 std::uint64_t callback_start_ns, bool count_delivery) noexcept;
253 void process_realtime_position(const audio::BufferView<const float>&, audio::BufferView<float>&,
254 std::uint32_t, std::uint64_t, bool input_valid,
255 std::uint64_t callback_start_ns) noexcept;
256 void process_offline_position(const audio::BufferView<const float>&, audio::BufferView<float>&,
257 std::uint32_t, std::uint64_t, bool input_valid) noexcept;
258 void process_invalid_position(audio::BufferView<float>&, std::uint64_t, bool offline) noexcept;
259 void publish_trial_delivery(std::uint64_t sequence, std::uint8_t disposition,
260 std::uint64_t callback_start_ns, std::uint64_t callback_end_ns,
261 std::uint64_t result_visible_ns) noexcept;
262 void record_delivery(std::uint8_t disposition, bool worker_output) noexcept;
263
264 audio::PlanarAudioRingBuffer input_ring_;
265 audio::PlanarAudioRingBuffer output_ring_;
266
267 audio::Buffer<float> worker_in_;
268 audio::Buffer<float> worker_out_;
269 // Callback-owned scratch for rejected views. Zero input advances every
270 // stateful delay/history position without reading the malformed input.
271 audio::Buffer<float> rejected_input_;
272 audio::Buffer<float> rejected_output_;
273 std::vector<const float*> rejected_input_ptrs_;
274 std::vector<float*> rejected_output_ptrs_;
275 // Stable channel-pointer arrays for the worker views (BufferView holds the
276 // array by reference, so it must outlive the views).
277 std::vector<float*> in_fptrs_;
278 std::vector<const float*> in_cptrs_;
279 std::vector<float*> out_fptrs_;
280
281 std::atomic<std::uint64_t> produced_blocks_{0};
282 std::atomic<std::uint64_t> miss_blocks_{0};
283 std::atomic<std::uint64_t> input_dropped_blocks_{0}; // whole-block input drops
284 std::atomic<std::uint64_t> resynced_blocks_{0}; // late wet blocks dropped to realign
285 std::atomic<std::uint64_t> delivery_gpu_{0}, delivery_worker_{0}, delivery_fallback_{0},
286 delivery_silence_{0}, delivery_passthrough_{0}, delivery_priming_{0}, delivery_invalid_{0};
287 // Resync debt: output slots a miss already substituted for, whose late wet
288 // counterparts must still be dropped to realign the stream. Incremented on a
289 // miss, decremented as those blocks are drained. Touched ONLY by process() on
290 // the audio thread, so it needs no atomicity — counting misses explicitly
291 // (rather than inferring "excess ring depth") keeps the drain immune to a
292 // worker that races a fresh block in before the RT read of the same call.
293 std::uint64_t blocks_owed_ = 0;
294 // One flag per callback slot over a window of latency_blocks + 1, sized at
295 // prepare(): set when that slot's input block was dropped on a full input
296 // ring. Its wet block will never exist, so the callback `latency_blocks`
297 // later substitutes without reading the ring and without adding resync
298 // debt; reading there would return the next slot's block early and shift
299 // the stream ahead of the declared latency. Written and read only on the
300 // callback timeline.
301 std::vector<std::uint8_t> dropped_slots_;
302 // Per-block worker timing, published as integer nanoseconds for lock-free
303 // reads by the UI thread (double isn't reliably lock-free).
304 std::atomic<std::uint64_t> last_block_ns_{0};
305 std::atomic<std::uint64_t> avg_block_ns_{0};
306
307 // Optional internal worker. The worker polls the input ring; the RT path
308 // never touches these.
309 void worker_loop() noexcept;
310 std::thread worker_;
311 std::atomic<bool> worker_running_{false};
312 std::chrono::microseconds poll_interval_{200};
313 void* audio_workgroup_ = nullptr;
314 bool join_audio_workgroup_ = false;
315 std::atomic<bool> worker_workgroup_joined_{false};
316 std::atomic<std::uint64_t> worker_workgroup_join_failures_{0};
317 std::atomic<bool> worker_interactive_qos_{false};
318
319 // Wake-on-write (Config::wake_on_write, opt-in). The RT process() posts
320 // `wake_` after each input write, and an installed completion notifier
321 // posts it from the provider's callback thread; the worker waits on it
322 // (bounded by poll_interval_) instead of always sleeping the full quantum.
323 // A post never allocates or blocks, but signals the kernel while the
324 // worker is parked. When off, worker_loop() sleeps the poll interval.
325 bool wake_on_write_ = false;
326 std::unique_ptr<detail::WorkerWake> wake_; // allocated at construction
327 // Provider completion events, routed to `wake_` while the owned worker
328 // runs. Installed at prepare(), cleared in release() after the join.
329 bool (*realtime_gpu_set_notifier_)(void*,
330 detail::SharedIoCompletionNotifier) noexcept = nullptr;
331 bool completion_notifier_installed_ = false;
332 static void notify_worker(void* self) noexcept;
333
334 // Private callback-side GPU path captured at prepare() from the concrete
335 // node. These are plain function pointers so the RT process path does not
336 // allocate, lock, or touch a public base-class extension.
337 void* realtime_gpu_context_ = nullptr;
338 std::uint8_t (*realtime_gpu_process_)(void*, const audio::BufferView<const float>&,
339 audio::BufferView<float>&, std::uint32_t, std::uint64_t,
340 bool, std::uint64_t) noexcept = nullptr;
341 std::uint32_t (*realtime_gpu_service_)(void*, std::uint64_t) noexcept = nullptr;
342 bool (*realtime_gpu_fence_)(void*) noexcept = nullptr;
343 void (*realtime_gpu_delivered_)(void*, std::uint64_t, std::uint8_t, std::uint64_t,
344 std::uint64_t) noexcept = nullptr;
345 // Serialized non-RT health read for the same context. The worker
346 // republishes every node health sample, and prepare()/release() publish
347 // the prepared configuration, into `diagnostics_`; status_snapshot() and
348 // capability_report() read only those copies. Allocated at construction.
349 bool (*realtime_gpu_health_)(const void*, detail::RealtimeGpuHealth&) noexcept = nullptr;
350 bool sample_staged_health_ = false;
351 std::unique_ptr<detail::GpuAudioTransportDiagnostics> diagnostics_;
352 void refresh_node_health() noexcept;
353 void publish_prepared_configuration() noexcept;
354
355 // Worker-only re-acquire steward (Config::recovery).
356 void steward_reacquire() noexcept;
357 bool (*realtime_gpu_reprepare_)(void*, std::uint64_t) noexcept = nullptr;
359 std::uint64_t reacquire_budget_ns_ = 0;
360 std::uint32_t reacquire_max_attempts_ = 0;
361 std::uint64_t reacquire_backoff_ns_ = 0;
362 std::uint32_t reacquire_round_ = 0; // failed attempts in this outage
363 std::uint64_t outage_started_ns_ = 0; // worker's first sight of the fence
364 std::uint64_t next_reacquire_ns_ = 0;
365 // A success armed resume_pending_from_ns_; once the callback clears it the
366 // re-acquired epoch has delivered GPU audio, even if a new fault latched
367 // before the worker saw the healthy state.
368 bool resume_armed_ = false;
369 std::atomic<std::uint64_t> reacquire_attempts_{0};
370 std::atomic<std::uint64_t> reacquire_successes_{0};
371 std::atomic<std::uint64_t> last_reacquire_to_gpu_ns_{0};
372 // Outage start of a successful re-acquire whose first GPU block the
373 // callback has not delivered yet; the callback measures and clears it.
374 std::atomic<std::uint64_t> resume_pending_from_ns_{0};
375 std::uint64_t callback_sequence_ = 0; // one monotonic RT/offline callback timeline
376 bool realtime_gpu_fenced_for_offline_ = false;
377 // Optional node hook bracketing each offline run; `realtime_gpu_offline_`
378 // is set by its first block and cleared by the next realtime callback.
379 void (*realtime_gpu_offline_render_)(void*, bool) noexcept = nullptr;
380 bool realtime_gpu_offline_ = false;
381
382 // Synchronous (offline) drive. When `synchronous_` is set, the background
383 // worker yields and process_offline() pumps the node inline under
384 // `pump_mutex_`; the realtime process() clears the flag so the worker
385 // resumes. `pump_mutex_` only ever contends between the offline path and the
386 // worker (both off the audio thread) — process() never takes it, so the
387 // realtime path stays lock-free.
388 std::atomic<bool> synchronous_{false};
389 std::mutex pump_mutex_;
390
391 // Host-only P4 trial observer. The observer is installed before the
392 // callback starts and must itself be realtime-safe. The default runtime
393 // path leaves it null, so this adds no hot-path work for normal users.
394 void* trial_observer_context_ = nullptr;
395 TrialDeliveryFn trial_observer_ = nullptr;
396
397 // True only while the hosted node pipelines its worker
398 // (worker_pipeline_blocks > 0 after the opt-in at prepare()). release()
399 // opts such a node back out; any other node is never touched there, so a
400 // host may destroy it before release().
401 bool pipeline_hosted_ = false;
402};
403
404} // namespace pulp::gpu_audio
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
A unit of audio work the transport schedules onto a non-real-time worker.
Definition gpu_audio_node.hpp:92
GpuAudioCapabilityReport capability_report() const noexcept
Host/UI-only snapshot of the prepared integration path.
bool is_prepared() const noexcept
Definition gpu_audio_transport.hpp:155
uint32_t latency_samples() const noexcept
Definition gpu_audio_transport.hpp:158
GpuAudioStatus status_snapshot() const noexcept
Allocation-free host/UI snapshot with stable, versioned field semantics.
void process(const audio::BufferView< const float > &input, audio::BufferView< float > &output, uint32_t n) noexcept
Real-time-safe.
GpuAudioTransport(const GpuAudioTransport &)=delete
GpuAudioTransport & operator=(const GpuAudioTransport &)=delete
void process_offline(const audio::BufferView< const float > &input, audio::BufferView< float > &output, uint32_t n) noexcept
Offline / faster-than-real-time render path.
friend bool configure_gpu_audio_transport_trial_observer(GpuAudioTransport &, void *, TrialDeliveryFn) noexcept
DeliverySnapshot delivery_snapshot() const noexcept
Allocation-free independent atomic loads: approximate while process() runs, exact once its caller has...
void pump(uint32_t max_blocks=0) noexcept
Non-RT worker step: drain ready input blocks, run the node, fill output.
Stats stats() const noexcept
bool prepare(GpuAudioNode *node, const Config &config)
Non-RT.
Definition analyzer_provider.hpp:12
Definition gpu_audio_node.hpp:78
Definition flow_pans.hpp:19
GpuAudioRecoveryPolicy
Whether a node may re-acquire a lost provider on its own.
Definition gpu_audio_capability.hpp:39
@ Manual
Definition gpu_audio_capability.hpp:40
MissPolicy
What the transport does for a block when the GPU result is not ready in time (the worker fell behind,...
Definition gpu_audio_node.hpp:12
@ Silence
Output silence for the missed block (+ telemetry).
Definition gpu_audio_node.hpp:15
Definition host_transport_projector.hpp:8
STL namespace.
Backend-neutral, allocation-free capability snapshot for installed SDK consumers.
Definition gpu_audio_capability.hpp:46
Versioned, allocation-free status shared by native hosts and UI adapters.
Definition gpu_audio_status.hpp:80
Definition gpu_audio_transport.hpp:45
GpuAudioRecoveryPolicy recovery_policy
AutoReacquire lets the worker replace a fenced node's provider while the callback stays on its prepar...
Definition gpu_audio_transport.hpp:77
std::uint32_t reacquire_max_attempts
Attempts per outage.
Definition gpu_audio_transport.hpp:85
bool wake_on_write
Definition gpu_audio_transport.hpp:63
std::uint64_t reacquire_release_budget_ns
Budget for releasing the old provider.
Definition gpu_audio_transport.hpp:81
uint32_t ring_blocks
Definition gpu_audio_transport.hpp:46
bool join_audio_workgroup
Definition gpu_audio_transport.hpp:69
void * audio_workgroup
Definition gpu_audio_transport.hpp:68
std::uint64_t reacquire_backoff_ns
The first attempt is immediate.
Definition gpu_audio_transport.hpp:91
bool run_worker_thread
Definition gpu_audio_transport.hpp:51
Selected output for prepared process() calls, not worker completions or GPU admissions.
Definition gpu_audio_transport.hpp:123
std::uint64_t invalid_blocks
Definition gpu_audio_transport.hpp:132
std::uint64_t priming_blocks
Definition gpu_audio_transport.hpp:131
std::uint64_t worker_output_blocks
Definition gpu_audio_transport.hpp:127
std::uint64_t cpu_fallback_blocks
Definition gpu_audio_transport.hpp:128
std::uint64_t silence_blocks
Definition gpu_audio_transport.hpp:129
std::uint64_t passthrough_blocks
Definition gpu_audio_transport.hpp:130
std::uint64_t gpu_blocks
Definition gpu_audio_transport.hpp:124
Definition gpu_audio_transport.hpp:94
bool worker_workgroup_joined
Definition gpu_audio_transport.hpp:112
std::uint64_t miss_blocks
Definition gpu_audio_transport.hpp:96
std::uint64_t resynced_blocks
Definition gpu_audio_transport.hpp:103
bool worker_interactive_qos
Definition gpu_audio_transport.hpp:116
std::uint64_t worker_workgroup_join_failures
Definition gpu_audio_transport.hpp:113
std::uint64_t input_dropped_frames
Definition gpu_audio_transport.hpp:97
double avg_block_us
Definition gpu_audio_transport.hpp:111
std::uint64_t produced_blocks
Definition gpu_audio_transport.hpp:95
double last_block_us
Definition gpu_audio_transport.hpp:110
Real-time thread priority management.