160 static_assert(JobMailboxCapacity > 0);
161 static_assert(CompletionMailboxCapacity > 0);
162 static_assert(CompletionMailboxCapacity <=
163 std::numeric_limits<std::uint32_t>::max());
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())) {
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)
192 if (!frames_per_slot ||
193 *frames_per_slot > std::numeric_limits<std::size_t>::max() ||
198 if (next_prepare_epoch_ == 0)
return false;
199 const auto prepare_epoch = next_prepare_epoch_++;
201 workers_ = std::make_unique<Worker[]>(config.worker_count);
202 sources_ = std::make_unique<Source[]>(config.source_capacity);
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;
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);
222 reserved_scratch_samples_ = *reserved_samples;
223 active_prepare_epoch_ = prepare_epoch;
225 for (std::uint32_t index = 0; index < config.worker_count; ++index) {
226 workers_[index].thread =
227 std::thread([
this, index] { worker_loop(index); });
238 for (std::uint32_t index = 0; index < config_.worker_count; ++index) {
239 auto& worker = workers_[index];
241 std::lock_guard lock(worker.mutex);
242 worker.stopping.store(
true, std::memory_order_release);
244 worker.wake.notify_all();
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);
252 for (std::uint32_t index = 0; index < config_.worker_count; ++index) {
253 if (workers_[index].thread.joinable()) workers_[index].thread.join();
261 reserved_scratch_samples_ = 0;
262 active_prepare_epoch_ = 0;
265 bool prepared() const noexcept {
return prepared_; }
266 std::uint32_t
worker_count() const noexcept {
return config_.worker_count; }
270 return reserved_scratch_samples_;
274 std::uint32_t channels,
281 .read = [reader = std::move(reader)](
284 std::uint64_t frames,
286 return reader(start, destination, frames);
294 std::uint32_t channels,
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};
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;
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 ||
329 std::numeric_limits<std::uint64_t>::max() - job.frame_count) {
332 auto* source = find_source_id(job.source.source_id);
334 if (source->token.source_generation != job.source.source_generation) {
335 telemetry_.stale_generation.fetch_add(1, std::memory_order_relaxed);
338 if (job.reservation_serial <= source->last_reservation_serial) {
339 telemetry_.stale_reservation.fetch_add(1, std::memory_order_relaxed);
342 if (source->outstanding_jobs >=
343 config_.maximum_outstanding_jobs_per_source) {
346 if (source->canceled.load(std::memory_order_acquire))
349 auto& worker = workers_[source->worker_index];
350 InternalJob internal{job,
static_cast<std::uint32_t
>(source - sources_.get())};
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)) {
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);
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();
375 auto* source = find_source_id(token.source_id);
377 if (source->token.source_generation != token.source_generation) {
378 telemetry_.stale_generation.fetch_add(1, std::memory_order_relaxed);
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);
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);
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))
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
412 if (completion->scratch_slot >= worker.scratch_slot_count) {
413 worker.completion_leased =
false;
414 worker.wake.notify_all();
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)),
425 worker.wake.notify_all();
430 std::uint32_t worker_index)
noexcept {
431 if (!prepared_ || worker_index >= config_.worker_count)
return std::nullopt;
432 auto& worker = workers_[worker_index];
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();
442 if (worker.stopping.load(std::memory_order_acquire) ||
443 worker.scratch_lease_exhausted.load(std::memory_order_acquire)) {
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) {
464 if (worker.leased_completion.pool_prepare_epoch !=
465 completion.pool_prepare_epoch ||
466 completion.pool_prepare_epoch != active_prepare_epoch_) {
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);
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();
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),
494 telemetry_.stale_generation.load(std::memory_order_relaxed),
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),
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),
514 std::uint32_t source_index = 0;
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;
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;
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;
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;
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};
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;
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) {
586 static void clear_source(Source& source)
noexcept {
589 source.last_reservation_serial = 0;
590 source.worker_index = 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;
599 void worker_loop(std::uint32_t worker_index)
noexcept {
600 auto& worker = workers_[worker_index];
602 auto internal = worker.jobs.try_pop();
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();
609 if (worker.stopping.load(std::memory_order_acquire))
break;
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];
621 BufferView<float> destination(
622 scratch.channel_ptrs.data(),
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);
636 decoded = source.binding.read(
640 FrameReaderStopToken{source.active_stop_requested});
641 if (decoded >
internal->job.frame_count) {
649 source.active_reader_calls.fetch_sub(1, std::memory_order_acq_rel);
652 if (worker.stopping.load(std::memory_order_acquire)) {
654 std::lock_guard lock(worker.mutex);
655 scratch.in_use.store(
false, std::memory_order_release);
657 worker.wake.notify_all();
658 telemetry_.stopped_jobs.fetch_add(1, std::memory_order_relaxed);
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);
675 const SampleStreamDecodeCompletion completion{
677 .reservation_serial =
internal->job.reservation_serial,
678 .requested_frames =
internal->job.frame_count,
679 .decoded_frames = decoded,
680 .channels = source.channels,
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_,
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;
697 if (worker.stopping.load(std::memory_order_acquire))
break;
700 if (worker.stopping.load(std::memory_order_acquire)) {
702 std::lock_guard lock(worker.mutex);
703 scratch.in_use.store(
false, std::memory_order_release);
705 worker.wake.notify_all();
706 telemetry_.stopped_jobs.fetch_add(1, std::memory_order_relaxed);
709 telemetry_.completions_published.fetch_add(1,
710 std::memory_order_relaxed);
712 telemetry_.completed_frames.fetch_add(decoded,
713 std::memory_order_relaxed);
715 worker.wake.notify_all();
718 while (worker.jobs.try_pop())
719 telemetry_.stopped_jobs.fetch_add(1, std::memory_order_relaxed);
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;
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);
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,
754 std::memory_order_relaxed)) {}
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)
764 struct ScratchLease {
765 std::uint32_t slot = 0;
766 std::uint64_t generation = 0;
769 static std::optional<ScratchLease> acquire_scratch_slot(Worker& worker)
noexcept {
771 for (std::uint32_t index = 0;
index < worker.scratch_slot_count; ++
index) {
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) {
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);
784 worker.wake.notify_all();
787 ++scratch.next_lease_generation;
788 return ScratchLease{
index, generation};
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;
797 if (!worker.scratch_slots[index].in_use.load(
798 std::memory_order_acquire))
return true;
802 if (worker.stopping.load(std::memory_order_acquire))
return std::nullopt;