Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
sample_stream_decode_pool.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <atomic>
8#include <condition_variable>
9#include <cstddef>
10#include <cstdint>
11#include <limits>
12#include <memory>
13#include <mutex>
14#include <optional>
15#include <thread>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
20namespace pulp::audio {
21
23 std::uint32_t worker_count = 0;
24 std::uint32_t source_capacity = 0;
25 std::uint32_t maximum_channels = 0;
26 std::uint64_t maximum_frames_per_job = 0;
31};
32
35 std::uint64_t reservation_serial = 0;
36 std::uint64_t start_frame = 0;
37 std::uint64_t frame_count = 0;
38};
39
47
63
73
75 const SampleStreamDecodeCompletion& completion,
76 SampleStreamSourceToken expected_source,
77 std::uint64_t expected_reservation_serial) noexcept {
78 if (completion.source.source_id != expected_source.source_id)
80 if (completion.source.source_generation != expected_source.source_generation)
82 if (completion.reservation_serial != expected_reservation_serial)
84 if (completion.status == SampleStreamDecodeCompletionStatus::Canceled)
86 if (completion.status == SampleStreamDecodeCompletionStatus::Stopped)
88 if (completion.status != SampleStreamDecodeCompletionStatus::Decoded)
91}
92
100
110
121
129
131 std::uint64_t jobs_queued = 0;
132 std::uint64_t completions_published = 0;
133 std::uint64_t job_queue_full = 0;
134 std::uint64_t completion_queue_full = 0;
135 std::uint64_t stale_generation = 0;
136 std::uint64_t stale_reservation = 0;
137 std::uint64_t decode_errors = 0;
138 std::uint64_t canceled_completions = 0;
139 std::uint64_t stopped_jobs = 0;
141 std::uint64_t completed_frames = 0;
143};
144
150
151static_assert(std::is_trivially_copyable_v<SampleStreamDecodeJob>);
152static_assert(std::is_trivially_copyable_v<SampleStreamDecodeCompletion>);
153
156template<std::size_t JobMailboxCapacity = 8,
157 std::size_t CompletionMailboxCapacity = 8>
159public:
160 static_assert(JobMailboxCapacity > 0);
161 static_assert(CompletionMailboxCapacity > 0);
162 static_assert(CompletionMailboxCapacity <=
163 std::numeric_limits<std::uint32_t>::max());
164
167
170
172 release();
173 if (config.worker_count == 0 || config.source_capacity == 0 ||
174 config.maximum_channels == 0 || config.maximum_frames_per_job == 0 ||
175 config.maximum_outstanding_jobs_per_source == 0 ||
176 config.maximum_outstanding_jobs_per_source > JobMailboxCapacity ||
177 config.maximum_outstanding_jobs_per_source > CompletionMailboxCapacity ||
178 config.maximum_frames_per_job >
179 static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
180 return false;
181 }
182
183 const auto frames_per_slot = checked_multiply(
184 static_cast<std::uint64_t>(config.maximum_channels),
185 config.maximum_frames_per_job);
186 const auto slots = checked_multiply(
187 static_cast<std::uint64_t>(config.worker_count),
188 config.maximum_outstanding_jobs_per_source);
189 const auto reserved_samples = frames_per_slot && slots
190 ? checked_multiply(*frames_per_slot, *slots)
191 : std::nullopt;
192 if (!frames_per_slot ||
193 *frames_per_slot > std::numeric_limits<std::size_t>::max() ||
194 !reserved_samples) {
195 return false;
196 }
197
198 if (next_prepare_epoch_ == 0) return false;
199 const auto prepare_epoch = next_prepare_epoch_++;
200 try {
201 workers_ = std::make_unique<Worker[]>(config.worker_count);
202 sources_ = std::make_unique<Source[]>(config.source_capacity);
203 config_ = config;
204 for (std::uint32_t index = 0; index < config.worker_count; ++index) {
205 auto& worker = workers_[index];
206 worker.scratch_slot_count =
207 config.maximum_outstanding_jobs_per_source;
208 worker.scratch_slots = std::make_unique<ScratchSlot[]>(
209 worker.scratch_slot_count);
210 for (std::uint32_t scratch_index = 0;
211 scratch_index < worker.scratch_slot_count;
212 ++scratch_index) {
213 auto& scratch = worker.scratch_slots[scratch_index];
214 scratch.audio.resize(config.maximum_channels,
215 static_cast<std::size_t>(
216 config.maximum_frames_per_job));
217 scratch.channel_ptrs.resize(config.maximum_channels);
218 scratch.const_channel_ptrs.resize(config.maximum_channels);
219 }
220 }
221 reset_telemetry();
222 reserved_scratch_samples_ = *reserved_samples;
223 active_prepare_epoch_ = prepare_epoch;
224 prepared_ = true;
225 for (std::uint32_t index = 0; index < config.worker_count; ++index) {
226 workers_[index].thread =
227 std::thread([this, index] { worker_loop(index); });
228 }
229 return true;
230 } catch (...) {
231 release();
232 return false;
233 }
234 }
235
236 void release() noexcept {
237 if (workers_) {
238 for (std::uint32_t index = 0; index < config_.worker_count; ++index) {
239 auto& worker = workers_[index];
240 {
241 std::lock_guard lock(worker.mutex);
242 worker.stopping.store(true, std::memory_order_release);
243 }
244 worker.wake.notify_all();
245 }
246 if (sources_) {
247 for (std::uint32_t index = 0; index < config_.source_capacity; ++index) {
248 sources_[index].active_stop_requested.store(
249 true, std::memory_order_release);
250 }
251 }
252 for (std::uint32_t index = 0; index < config_.worker_count; ++index) {
253 if (workers_[index].thread.joinable()) workers_[index].thread.join();
254 }
255 }
256 prepared_ = false;
257 sources_.reset();
258 workers_.reset();
259 config_ = {};
260 next_worker_ = 0;
261 reserved_scratch_samples_ = 0;
262 active_prepare_epoch_ = 0;
263 }
264
265 bool prepared() const noexcept { return prepared_; }
266 std::uint32_t worker_count() const noexcept { return config_.worker_count; }
267 std::uint32_t source_capacity() const noexcept { return config_.source_capacity; }
268
269 std::uint64_t reserved_scratch_samples() const noexcept {
270 return reserved_scratch_samples_;
271 }
272
274 std::uint32_t channels,
275 FrameReader reader) {
276 if (!reader)
278 return add_source(token,
279 channels,
281 .read = [reader = std::move(reader)](
282 std::uint64_t start,
283 BufferView<float> destination,
284 std::uint64_t frames,
286 return reader(start, destination, frames);
287 },
289 });
290 }
291
294 std::uint32_t channels,
295 FrameReaderBinding binding) {
296 if (!prepared_) return {SampleStreamDecodeSourceAddStatus::Stopped, 0};
297 if (token.source_id == 0 || token.source_generation == 0 || channels == 0 ||
298 channels > config_.maximum_channels || !binding.read)
300 for (std::uint32_t index = 0; index < config_.source_capacity; ++index) {
301 const auto& source = sources_[index];
302 if (source.registered && source.token.source_id == token.source_id)
304 source.worker_index};
305 }
306 for (std::uint32_t index = 0; index < config_.source_capacity; ++index) {
307 auto& source = sources_[index];
308 if (source.registered) continue;
309 source.token = token;
310 source.channels = channels;
311 source.binding = std::move(binding);
312 source.worker_index = next_worker_++ % config_.worker_count;
313 source.last_reservation_serial = 0;
314 source.outstanding_jobs = 0;
315 source.canceled.store(false, std::memory_order_relaxed);
316 source.active_stop_requested.store(false, std::memory_order_relaxed);
317 source.registered = true;
318 return {SampleStreamDecodeSourceAddStatus::Added, source.worker_index};
319 }
321 }
322
324 if (!prepared_) return SampleStreamDecodeSubmitStatus::Stopped;
325 if (job.source.source_id == 0 || job.source.source_generation == 0 ||
326 job.reservation_serial == 0 || job.frame_count == 0 ||
327 job.frame_count > config_.maximum_frames_per_job ||
328 job.start_frame >
329 std::numeric_limits<std::uint64_t>::max() - job.frame_count) {
331 }
332 auto* source = find_source_id(job.source.source_id);
333 if (source == nullptr) return SampleStreamDecodeSubmitStatus::StaleSource;
334 if (source->token.source_generation != job.source.source_generation) {
335 telemetry_.stale_generation.fetch_add(1, std::memory_order_relaxed);
337 }
338 if (job.reservation_serial <= source->last_reservation_serial) {
339 telemetry_.stale_reservation.fetch_add(1, std::memory_order_relaxed);
341 }
342 if (source->outstanding_jobs >=
343 config_.maximum_outstanding_jobs_per_source) {
345 }
346 if (source->canceled.load(std::memory_order_acquire))
348
349 auto& worker = workers_[source->worker_index];
350 InternalJob internal{job, static_cast<std::uint32_t>(source - sources_.get())};
351 {
352 std::lock_guard lock(worker.mutex);
353 if (worker.scratch_lease_exhausted.load(std::memory_order_acquire) ||
354 worker.stopping.load(std::memory_order_acquire)) {
356 }
357 ++source->outstanding_jobs;
358 if (!worker.jobs.try_push(internal)) {
359 --source->outstanding_jobs;
360 telemetry_.job_queue_full.fetch_add(1, std::memory_order_relaxed);
362 }
363 }
364 source->last_reservation_serial = job.reservation_serial;
365 update_high_water(telemetry_.source_outstanding_high_water,
366 source->outstanding_jobs);
367 telemetry_.jobs_queued.fetch_add(1, std::memory_order_relaxed);
368 worker.wake.notify_all();
370 }
371
373 SampleStreamSourceToken token) noexcept {
374 if (!prepared_) return SampleStreamDecodeCancelStatus::Stopped;
375 auto* source = find_source_id(token.source_id);
376 if (source == nullptr) return SampleStreamDecodeCancelStatus::StaleSource;
377 if (source->token.source_generation != token.source_generation) {
378 telemetry_.stale_generation.fetch_add(1, std::memory_order_relaxed);
380 }
381 if (source->canceled.exchange(true, std::memory_order_acq_rel))
383 source->active_stop_requested.store(true, std::memory_order_release);
384 if (source->outstanding_jobs == 0) clear_source(*source);
386 }
387
389 if (!prepared_) return false;
390 auto* source = find_source(token);
391 if (source == nullptr || source->outstanding_jobs != 0) return false;
392 clear_source(*source);
393 return true;
394 }
395
396 std::optional<SampleStreamDecodeCompletionView> try_pop_completion(
397 std::uint32_t worker_index) noexcept {
398 if (!prepared_ || worker_index >= config_.worker_count) return std::nullopt;
399 auto& worker = workers_[worker_index];
400 std::lock_guard lock(worker.mutex);
401 if (worker.scratch_lease_exhausted.load(std::memory_order_acquire))
402 return std::nullopt;
403 if (worker.completion_leased) return std::nullopt;
404 auto completion = worker.completions.try_pop();
405 if (!completion) return std::nullopt;
406 worker.completion_leased = true;
407 worker.leased_completion = *completion;
408 const auto publishable_frames =
410 ? completion->decoded_frames
411 : std::uint64_t{0};
412 if (completion->scratch_slot >= worker.scratch_slot_count) {
413 worker.completion_leased = false;
414 worker.wake.notify_all();
415 return std::nullopt;
416 }
417 auto& scratch = worker.scratch_slots[completion->scratch_slot];
419 .completion = *completion,
421 scratch.const_channel_ptrs.data(),
422 completion->channels,
423 static_cast<std::size_t>(publishable_frames)),
424 };
425 worker.wake.notify_all();
426 return result;
427 }
428
429 std::optional<SampleStreamDecodeCompletionView> wait_pop_completion(
430 std::uint32_t worker_index) noexcept {
431 if (!prepared_ || worker_index >= config_.worker_count) return std::nullopt;
432 auto& worker = workers_[worker_index];
433 {
434 std::unique_lock lock(worker.mutex);
435 worker.wake.wait(lock, [&] {
436 return worker.stopping.load(std::memory_order_acquire) ||
437 worker.scratch_lease_exhausted.load(
438 std::memory_order_acquire) ||
439 !worker.completions.empty();
440 });
441 }
442 if (worker.stopping.load(std::memory_order_acquire) ||
443 worker.scratch_lease_exhausted.load(std::memory_order_acquire)) {
444 return std::nullopt;
445 }
446 return try_pop_completion(worker_index);
447 }
448
449 bool release_completion(const SampleStreamDecodeCompletion& completion) noexcept {
450 if (!prepared_ || completion.worker_index >= config_.worker_count) return false;
451 auto& worker = workers_[completion.worker_index];
452 std::lock_guard lock(worker.mutex);
453 if (!worker.completion_leased ||
454 worker.leased_completion.source.source_id != completion.source.source_id ||
455 worker.leased_completion.source.source_generation !=
456 completion.source.source_generation ||
457 worker.leased_completion.reservation_serial !=
458 completion.reservation_serial ||
459 worker.leased_completion.scratch_slot != completion.scratch_slot ||
460 worker.leased_completion.scratch_lease_generation !=
461 completion.scratch_lease_generation) {
462 return false;
463 }
464 if (worker.leased_completion.pool_prepare_epoch !=
465 completion.pool_prepare_epoch ||
466 completion.pool_prepare_epoch != active_prepare_epoch_) {
467 return false;
468 }
469
470 auto* source = find_source(completion.source);
471 if (source != nullptr && source->outstanding_jobs != 0) {
472 --source->outstanding_jobs;
473 if (source->outstanding_jobs == 0 &&
474 source->canceled.load(std::memory_order_acquire)) {
475 clear_source(*source);
476 }
477 }
478 worker.completion_leased = false;
479 worker.scratch_slots[completion.scratch_slot].in_use.store(
480 false, std::memory_order_release);
481 worker.wake.notify_all();
482 return true;
483 }
484
486 return {
487 .jobs_queued = telemetry_.jobs_queued.load(std::memory_order_relaxed),
488 .completions_published =
489 telemetry_.completions_published.load(std::memory_order_relaxed),
490 .job_queue_full = telemetry_.job_queue_full.load(std::memory_order_relaxed),
491 .completion_queue_full =
492 telemetry_.completion_queue_full.load(std::memory_order_relaxed),
493 .stale_generation =
494 telemetry_.stale_generation.load(std::memory_order_relaxed),
495 .stale_reservation =
496 telemetry_.stale_reservation.load(std::memory_order_relaxed),
497 .decode_errors = telemetry_.decode_errors.load(std::memory_order_relaxed),
498 .canceled_completions =
499 telemetry_.canceled_completions.load(std::memory_order_relaxed),
500 .stopped_jobs = telemetry_.stopped_jobs.load(std::memory_order_relaxed),
501 .source_outstanding_high_water =
502 telemetry_.source_outstanding_high_water.load(std::memory_order_relaxed),
503 .completed_frames =
504 telemetry_.completed_frames.load(std::memory_order_relaxed),
505 .same_source_reader_concurrency_high_water =
506 telemetry_.same_source_reader_concurrency_high_water.load(
507 std::memory_order_relaxed),
508 };
509 }
510
511private:
512 struct InternalJob {
514 std::uint32_t source_index = 0;
515 };
516
517 struct Source {
518 SampleStreamSourceToken token{};
519 FrameReaderBinding binding;
520 std::uint64_t last_reservation_serial = 0;
521 std::uint32_t worker_index = 0;
522 std::uint32_t channels = 0;
523 std::atomic<bool> canceled{false};
524 std::atomic<std::uint32_t> active_reader_calls{0};
525 std::atomic<bool> active_stop_requested{false};
526 bool registered = false;
527 std::uint32_t outstanding_jobs = 0;
528 };
529
530 struct ScratchSlot {
531 Buffer<float> audio;
532 std::vector<float*> channel_ptrs;
533 std::vector<const float*> const_channel_ptrs;
534 std::atomic<bool> in_use{false};
535 std::uint64_t next_lease_generation = 1;
536 };
537
538 struct Worker {
539 runtime::SpscQueue<InternalJob, JobMailboxCapacity> jobs;
540 runtime::SpscQueue<SampleStreamDecodeCompletion,
541 CompletionMailboxCapacity> completions;
542 std::unique_ptr<ScratchSlot[]> scratch_slots;
543 std::uint32_t scratch_slot_count = 0;
544 std::thread thread;
545 std::mutex mutex;
546 std::condition_variable wake;
547 std::atomic<bool> stopping{false};
548 std::atomic<bool> scratch_lease_exhausted{false};
549 SampleStreamDecodeCompletion leased_completion{};
550 bool completion_leased = false;
551 };
552
553 struct AtomicTelemetry {
554 std::atomic<std::uint64_t> jobs_queued{0};
555 std::atomic<std::uint64_t> completions_published{0};
556 std::atomic<std::uint64_t> job_queue_full{0};
557 std::atomic<std::uint64_t> completion_queue_full{0};
558 std::atomic<std::uint64_t> stale_generation{0};
559 std::atomic<std::uint64_t> stale_reservation{0};
560 std::atomic<std::uint64_t> decode_errors{0};
561 std::atomic<std::uint64_t> canceled_completions{0};
562 std::atomic<std::uint64_t> stopped_jobs{0};
563 std::atomic<std::uint64_t> source_outstanding_high_water{0};
564 std::atomic<std::uint64_t> completed_frames{0};
565 std::atomic<std::uint64_t> same_source_reader_concurrency_high_water{0};
566 };
567
568 Source* find_source_id(std::uint64_t source_id) noexcept {
569 if (!sources_) return nullptr;
570 for (std::uint32_t index = 0; index < config_.source_capacity; ++index) {
571 auto& source = sources_[index];
572 if (source.registered && source.token.source_id == source_id) return &source;
573 }
574 return nullptr;
575 }
576
577 Source* find_source(SampleStreamSourceToken token) noexcept {
578 auto* source = find_source_id(token.source_id);
579 if (source == nullptr ||
580 source->token.source_generation != token.source_generation) {
581 return nullptr;
582 }
583 return source;
584 }
585
586 static void clear_source(Source& source) noexcept {
587 source.binding = {};
588 source.token = {};
589 source.last_reservation_serial = 0;
590 source.worker_index = 0;
591 source.channels = 0;
592 source.canceled.store(false, std::memory_order_relaxed);
593 source.active_reader_calls.store(0, std::memory_order_relaxed);
594 source.active_stop_requested.store(false, std::memory_order_relaxed);
595 source.outstanding_jobs = 0;
596 source.registered = false;
597 }
598
599 void worker_loop(std::uint32_t worker_index) noexcept {
600 auto& worker = workers_[worker_index];
601 for (;;) {
602 auto internal = worker.jobs.try_pop();
603 if (!internal) {
604 std::unique_lock lock(worker.mutex);
605 worker.wake.wait(lock, [&] {
606 return worker.stopping.load(std::memory_order_acquire) ||
607 !worker.jobs.empty();
608 });
609 if (worker.stopping.load(std::memory_order_acquire)) break;
610 continue;
611 }
612
613 auto scratch_lease = acquire_scratch_slot(worker);
614 if (!scratch_lease) break;
615 auto& scratch = worker.scratch_slots[scratch_lease->slot];
616 auto& source = sources_[internal->source_index];
617 for (std::uint32_t channel = 0; channel < source.channels; ++channel) {
618 scratch.channel_ptrs[channel] = scratch.audio.channel(channel).data();
619 scratch.const_channel_ptrs[channel] = scratch.channel_ptrs[channel];
620 }
621 BufferView<float> destination(
622 scratch.channel_ptrs.data(),
623 source.channels,
624 static_cast<std::size_t>(internal->job.frame_count));
625 std::uint64_t decoded = 0;
626 bool reader_error = false;
627 const auto canceled_before_read =
628 source.canceled.load(std::memory_order_acquire);
629 if (!canceled_before_read) {
630 const auto active_reader_calls =
631 source.active_reader_calls.fetch_add(1,
632 std::memory_order_acq_rel) + 1;
633 update_high_water(telemetry_.same_source_reader_concurrency_high_water,
634 active_reader_calls);
635 try {
636 decoded = source.binding.read(
637 internal->job.start_frame,
638 destination,
639 internal->job.frame_count,
640 FrameReaderStopToken{source.active_stop_requested});
641 if (decoded > internal->job.frame_count) {
642 decoded = 0;
643 reader_error = true;
644 }
645 } catch (...) {
646 reader_error = true;
647 decoded = 0;
648 }
649 source.active_reader_calls.fetch_sub(1, std::memory_order_acq_rel);
650 }
651
652 if (worker.stopping.load(std::memory_order_acquire)) {
653 {
654 std::lock_guard lock(worker.mutex);
655 scratch.in_use.store(false, std::memory_order_release);
656 }
657 worker.wake.notify_all();
658 telemetry_.stopped_jobs.fetch_add(1, std::memory_order_relaxed);
659 break;
660 }
661
663 if (reader_error) {
665 telemetry_.decode_errors.fetch_add(1, std::memory_order_relaxed);
666 } else if (source.canceled.load(std::memory_order_acquire)) {
668 telemetry_.canceled_completions.fetch_add(1,
669 std::memory_order_relaxed);
670 } else if (decoded != internal->job.frame_count) {
672 telemetry_.decode_errors.fetch_add(1, std::memory_order_relaxed);
673 }
674
675 const SampleStreamDecodeCompletion completion{
676 .source = internal->job.source,
677 .reservation_serial = internal->job.reservation_serial,
678 .requested_frames = internal->job.frame_count,
679 .decoded_frames = decoded,
680 .channels = source.channels,
681 .status = status,
682 .worker_index = worker_index,
683 .scratch_slot = scratch_lease->slot,
684 .scratch_lease_generation = scratch_lease->generation,
685 .pool_prepare_epoch = active_prepare_epoch_,
686 };
687 {
688 std::unique_lock lock(worker.mutex);
689 while (!worker.completions.try_push(completion)) {
690 telemetry_.completion_queue_full.fetch_add(1,
691 std::memory_order_relaxed);
692 worker.wake.wait(lock, [&] {
693 return worker.stopping.load(std::memory_order_acquire) ||
694 worker.completions.size_approx() <
695 CompletionMailboxCapacity;
696 });
697 if (worker.stopping.load(std::memory_order_acquire)) break;
698 }
699 }
700 if (worker.stopping.load(std::memory_order_acquire)) {
701 {
702 std::lock_guard lock(worker.mutex);
703 scratch.in_use.store(false, std::memory_order_release);
704 }
705 worker.wake.notify_all();
706 telemetry_.stopped_jobs.fetch_add(1, std::memory_order_relaxed);
707 break;
708 }
709 telemetry_.completions_published.fetch_add(1,
710 std::memory_order_relaxed);
712 telemetry_.completed_frames.fetch_add(decoded,
713 std::memory_order_relaxed);
714 }
715 worker.wake.notify_all();
716 }
717
718 while (worker.jobs.try_pop())
719 telemetry_.stopped_jobs.fetch_add(1, std::memory_order_relaxed);
720 }
721
722 SampleStreamDecodePoolConfig config_{};
723 std::unique_ptr<Worker[]> workers_;
724 std::unique_ptr<Source[]> sources_;
725 AtomicTelemetry telemetry_{};
726 std::uint32_t next_worker_ = 0;
727 bool prepared_ = false;
728 std::uint64_t reserved_scratch_samples_ = 0;
729 std::uint64_t active_prepare_epoch_ = 0;
730 std::uint64_t next_prepare_epoch_ = 1;
731
732 void reset_telemetry() noexcept {
733 telemetry_.jobs_queued.store(0, std::memory_order_relaxed);
734 telemetry_.completions_published.store(0, std::memory_order_relaxed);
735 telemetry_.job_queue_full.store(0, std::memory_order_relaxed);
736 telemetry_.completion_queue_full.store(0, std::memory_order_relaxed);
737 telemetry_.stale_generation.store(0, std::memory_order_relaxed);
738 telemetry_.stale_reservation.store(0, std::memory_order_relaxed);
739 telemetry_.decode_errors.store(0, std::memory_order_relaxed);
740 telemetry_.canceled_completions.store(0, std::memory_order_relaxed);
741 telemetry_.stopped_jobs.store(0, std::memory_order_relaxed);
742 telemetry_.source_outstanding_high_water.store(0, std::memory_order_relaxed);
743 telemetry_.completed_frames.store(0, std::memory_order_relaxed);
744 telemetry_.same_source_reader_concurrency_high_water.store(
745 0, std::memory_order_relaxed);
746 }
747
748 static void update_high_water(std::atomic<std::uint64_t>& high_water,
749 std::uint64_t candidate) noexcept {
750 auto observed = high_water.load(std::memory_order_relaxed);
751 while (observed < candidate &&
752 !high_water.compare_exchange_weak(observed,
753 candidate,
754 std::memory_order_relaxed)) {}
755 }
756
757 static std::optional<std::uint64_t> checked_multiply(
758 std::uint64_t lhs, std::uint64_t rhs) noexcept {
759 if (lhs != 0 && rhs > std::numeric_limits<std::uint64_t>::max() / lhs)
760 return std::nullopt;
761 return lhs * rhs;
762 }
763
764 struct ScratchLease {
765 std::uint32_t slot = 0;
766 std::uint64_t generation = 0;
767 };
768
769 static std::optional<ScratchLease> acquire_scratch_slot(Worker& worker) noexcept {
770 for (;;) {
771 for (std::uint32_t index = 0; index < worker.scratch_slot_count; ++index) {
772 bool available = false;
773 if (worker.scratch_slots[index].in_use.compare_exchange_strong(
774 available, true, std::memory_order_acq_rel)) {
775 auto& scratch = worker.scratch_slots[index];
776 const auto generation = scratch.next_lease_generation;
777 if (generation == 0) {
778 {
779 std::lock_guard lock(worker.mutex);
780 scratch.in_use.store(false, std::memory_order_release);
781 worker.scratch_lease_exhausted.store(
782 true, std::memory_order_release);
783 }
784 worker.wake.notify_all();
785 return std::nullopt;
786 }
787 ++scratch.next_lease_generation;
788 return ScratchLease{index, generation};
789 }
790 }
791 std::unique_lock lock(worker.mutex);
792 worker.wake.wait(lock, [&] {
793 if (worker.stopping.load(std::memory_order_acquire)) return true;
794 for (std::uint32_t index = 0;
795 index < worker.scratch_slot_count;
796 ++index) {
797 if (!worker.scratch_slots[index].in_use.load(
798 std::memory_order_acquire)) return true;
799 }
800 return false;
801 });
802 if (worker.stopping.load(std::memory_order_acquire)) return std::nullopt;
803 }
804 }
805};
806
807} // namespace pulp::audio
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Cooperative cancellation signal passed to a streaming frame reader.
Definition streaming_sample_source.hpp:45
std::uint32_t worker_count() const noexcept
Definition sample_stream_decode_pool.hpp:266
SampleStreamDecodePool & operator=(const SampleStreamDecodePool &)=delete
std::uint32_t source_capacity() const noexcept
Definition sample_stream_decode_pool.hpp:267
SampleStreamDecodePoolTelemetry telemetry() const noexcept
Definition sample_stream_decode_pool.hpp:485
bool remove_idle_source(SampleStreamSourceToken token) noexcept
Definition sample_stream_decode_pool.hpp:388
bool release_completion(const SampleStreamDecodeCompletion &completion) noexcept
Definition sample_stream_decode_pool.hpp:449
SampleStreamDecodeSubmitStatus submit(const SampleStreamDecodeJob &job) noexcept
Definition sample_stream_decode_pool.hpp:323
bool prepared() const noexcept
Definition sample_stream_decode_pool.hpp:265
SampleStreamDecodeSourceAddResult add_source(SampleStreamSourceToken token, std::uint32_t channels, FrameReader reader)
Definition sample_stream_decode_pool.hpp:273
SampleStreamDecodeCancelStatus cancel_source(SampleStreamSourceToken token) noexcept
Definition sample_stream_decode_pool.hpp:372
bool prepare(const SampleStreamDecodePoolConfig &config)
Definition sample_stream_decode_pool.hpp:171
SampleStreamDecodePool(const SampleStreamDecodePool &)=delete
SampleStreamDecodeSourceAddResult add_source(SampleStreamSourceToken token, std::uint32_t channels, FrameReaderBinding binding)
Definition sample_stream_decode_pool.hpp:292
std::optional< SampleStreamDecodeCompletionView > try_pop_completion(std::uint32_t worker_index) noexcept
Definition sample_stream_decode_pool.hpp:396
~SampleStreamDecodePool()
Definition sample_stream_decode_pool.hpp:166
std::uint64_t reserved_scratch_samples() const noexcept
Definition sample_stream_decode_pool.hpp:269
void release() noexcept
Definition sample_stream_decode_pool.hpp:236
std::optional< SampleStreamDecodeCompletionView > wait_pop_completion(std::uint32_t worker_index) noexcept
Definition sample_stream_decode_pool.hpp:429
Definition analyzer_provider.hpp:12
SampleStreamDecodeSourceAddStatus
Definition sample_stream_decode_pool.hpp:93
@ InvalidSource
Definition sample_stream_decode_pool.hpp:95
@ Stopped
Definition sample_stream_decode_pool.hpp:98
@ SourceCapacityFull
Definition sample_stream_decode_pool.hpp:97
@ DuplicateSource
Definition sample_stream_decode_pool.hpp:96
@ Added
Definition sample_stream_decode_pool.hpp:94
SampleStreamDecodeSubmitStatus
Definition sample_stream_decode_pool.hpp:111
@ SourceInFlight
Definition sample_stream_decode_pool.hpp:117
@ StaleGeneration
Definition sample_stream_decode_pool.hpp:115
@ StaleReservation
Definition sample_stream_decode_pool.hpp:116
@ Queued
Definition sample_stream_decode_pool.hpp:112
@ QueueFull
Definition sample_stream_decode_pool.hpp:118
@ InvalidJob
Definition sample_stream_decode_pool.hpp:113
@ Stopped
Definition sample_stream_decode_pool.hpp:119
@ StaleSource
Definition sample_stream_decode_pool.hpp:114
SampleStreamDecodeCompletionMatch match_sample_stream_decode_completion(const SampleStreamDecodeCompletion &completion, SampleStreamSourceToken expected_source, std::uint64_t expected_reservation_serial) noexcept
Definition sample_stream_decode_pool.hpp:74
std::function< std::uint64_t(std::uint64_t start_frame, BufferView< float > dest, std::uint64_t frames)> FrameReader
Caller-supplied random-access frame reader.
Definition streaming_sample_source.hpp:73
SampleStreamDecodeCompletionMatch
Definition sample_stream_decode_pool.hpp:64
@ Canceled
Definition sample_stream_decode_pool.hpp:70
@ Accepted
Definition sample_stream_decode_pool.hpp:65
@ StaleGeneration
Definition sample_stream_decode_pool.hpp:67
@ DecodeError
Definition sample_stream_decode_pool.hpp:69
@ StaleReservation
Definition sample_stream_decode_pool.hpp:68
@ Stopped
Definition sample_stream_decode_pool.hpp:71
@ StaleSource
Definition sample_stream_decode_pool.hpp:66
@ Queued
Definition sample_stream_async_service.hpp:37
SampleStreamDecodeCompletionStatus
Definition sample_stream_decode_pool.hpp:40
@ Canceled
Definition sample_stream_decode_pool.hpp:44
@ Decoded
Definition sample_stream_decode_pool.hpp:41
@ Stopped
Definition sample_stream_decode_pool.hpp:45
@ ReaderError
Definition sample_stream_decode_pool.hpp:43
@ ShortRead
Definition sample_stream_decode_pool.hpp:42
SampleStreamDecodeCancelStatus
Definition sample_stream_decode_pool.hpp:122
@ Canceled
Definition sample_stream_decode_pool.hpp:123
@ StaleGeneration
Definition sample_stream_decode_pool.hpp:126
@ Stopped
Definition sample_stream_decode_pool.hpp:127
@ AlreadyCanceled
Definition sample_stream_decode_pool.hpp:124
@ StaleSource
Definition sample_stream_decode_pool.hpp:125
@ InvalidSource
Definition finite_time_stretch.hpp:12
@ JoinOnly
Definition streaming_sample_source.hpp:79
@ index
Definition gpu_pass.hpp:140
@ internal
The module's own band-limited oscillator — the DVP-1 lesson.
Definition vocoder.hpp:142
@ available
A value was read. It may legitimately be zero.
Definition latency_evidence.hpp:57
Definition streaming_sample_source.hpp:89
StoppableFrameReader read
Definition streaming_sample_source.hpp:90
Definition sample_stream_decode_pool.hpp:145
BufferView< const float > audio
Valid until release_completion(); non-decoded outcomes expose no samples.
Definition sample_stream_decode_pool.hpp:148
SampleStreamDecodeCompletion completion
Definition sample_stream_decode_pool.hpp:146
Definition sample_stream_decode_pool.hpp:48
std::uint64_t pool_prepare_epoch
Qualifies scratch leases across release()/prepare() cycles.
Definition sample_stream_decode_pool.hpp:61
std::uint64_t scratch_lease_generation
Qualifies reuse of one scratch slot within a prepare epoch.
Definition sample_stream_decode_pool.hpp:59
std::uint32_t channels
Definition sample_stream_decode_pool.hpp:53
SampleStreamSourceToken source
Definition sample_stream_decode_pool.hpp:49
std::uint64_t reservation_serial
Definition sample_stream_decode_pool.hpp:50
std::uint64_t decoded_frames
Definition sample_stream_decode_pool.hpp:52
SampleStreamDecodeCompletionStatus status
Definition sample_stream_decode_pool.hpp:54
std::uint64_t requested_frames
Definition sample_stream_decode_pool.hpp:51
std::uint32_t scratch_slot
Definition sample_stream_decode_pool.hpp:57
std::uint32_t worker_index
Definition sample_stream_decode_pool.hpp:56
Definition sample_stream_decode_pool.hpp:33
SampleStreamSourceToken source
Definition sample_stream_decode_pool.hpp:34
std::uint64_t reservation_serial
Definition sample_stream_decode_pool.hpp:35
std::uint64_t frame_count
Definition sample_stream_decode_pool.hpp:37
std::uint64_t start_frame
Definition sample_stream_decode_pool.hpp:36
Definition sample_stream_decode_pool.hpp:22
std::uint32_t maximum_outstanding_jobs_per_source
Bounded serial pipeline depth for each source.
Definition sample_stream_decode_pool.hpp:30
std::uint32_t maximum_channels
Definition sample_stream_decode_pool.hpp:25
std::uint64_t maximum_frames_per_job
Definition sample_stream_decode_pool.hpp:26
std::uint32_t worker_count
Definition sample_stream_decode_pool.hpp:23
std::uint32_t source_capacity
Definition sample_stream_decode_pool.hpp:24
Definition sample_stream_decode_pool.hpp:130
std::uint64_t decode_errors
Definition sample_stream_decode_pool.hpp:137
std::uint64_t source_outstanding_high_water
Definition sample_stream_decode_pool.hpp:140
std::uint64_t same_source_reader_concurrency_high_water
Definition sample_stream_decode_pool.hpp:142
std::uint64_t stopped_jobs
Definition sample_stream_decode_pool.hpp:139
std::uint64_t completion_queue_full
Definition sample_stream_decode_pool.hpp:134
std::uint64_t completions_published
Definition sample_stream_decode_pool.hpp:132
std::uint64_t canceled_completions
Definition sample_stream_decode_pool.hpp:138
std::uint64_t stale_reservation
Definition sample_stream_decode_pool.hpp:136
std::uint64_t completed_frames
Definition sample_stream_decode_pool.hpp:141
std::uint64_t job_queue_full
Definition sample_stream_decode_pool.hpp:133
std::uint64_t jobs_queued
Definition sample_stream_decode_pool.hpp:131
std::uint64_t stale_generation
Definition sample_stream_decode_pool.hpp:135
Definition sample_stream_decode_pool.hpp:101
SampleStreamDecodeSourceAddStatus status
Definition sample_stream_decode_pool.hpp:102
bool added() const noexcept
Definition sample_stream_decode_pool.hpp:106
std::uint32_t worker_index
Definition sample_stream_decode_pool.hpp:104
Definition sample_stream_service.hpp:18
std::uint64_t source_generation
Definition sample_stream_service.hpp:20
std::uint64_t source_id
Definition sample_stream_service.hpp:19