MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Promise.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <coroutine>
4
6
7namespace MayaFlux::Vruta {
8
9class SoundRoutine;
10class GraphicsRoutine;
11class CrossRoutine;
12class FreeRoutine;
13class Event;
14class NetworkSource;
15
16MAYAFLUX_API void invoke_routine_completion(std::function<void()> callback) noexcept;
17
18/**
19 * @struct routine_promise
20 * @brief Base coroutine promise type for audio processing tasks
21 *
22 * This promise_type serves as the base class for all coroutine promises in the
23 * MayaFlux engine. It defines the common behavior and interface for
24 * coroutines, including lifecycle management, state storage, and execution flags.
25 *
26 * The promise_type is a crucial component of C++20 coroutines that defines the
27 * behavior of SoundRoutine coroutines. It serves as the control interface between
28 * the coroutine machinery and the audio engine, managing:
29 *
30 * In the coroutine model, the promise object is created first when a coroutine
31 * function is called. It then creates and returns the SoundRoutine object that
32 * the caller receives. The promise remains associated with the coroutine frame
33 * throughout its lifetime, while the RoutineType provides the external interface
34 * to manipulate the coroutine.
35 */
36template <typename RoutineType>
37struct MAYAFLUX_API routine_promise {
38
39 void on_complete(std::function<void()> callback)
40 {
41 completion_callback = std::move(callback);
42 completion_registered.store(static_cast<bool>(completion_callback));
43 if (completion_signaled.load())
44 notify_complete();
45 }
46
47 void set_should_terminate(bool value) noexcept
48 {
49 should_terminate.store(value, std::memory_order_release);
50 if (value)
51 notify_complete();
52 }
53
54 void notify_complete() noexcept
55 {
56 completion_signaled.store(true);
57 if (!completion_registered.load() || completion_notified.exchange(true))
58 return;
59
60 invoke_routine_completion(std::move(completion_callback));
61 }
62
65
66 bool await_ready() const noexcept { return false; }
67 void await_suspend(std::coroutine_handle<>) const noexcept
68 {
69 promise->notify_complete();
70 }
71 void await_resume() const noexcept { }
72 };
73
74 RoutineType get_return_object()
75 {
76 return RoutineType(std::coroutine_handle<routine_promise>::from_promise(*this));
77 }
78
79 /**
80 * @brief Determines whether the coroutine suspends immediately upon creation
81 * @return A suspend never awaitable, meaning the coroutine begins execution immediately
82 *
83 * By returning std::suspend_never, this method indicates that the coroutine
84 * should start executing as soon as it's created, rather than waiting for
85 * an explicit resume call.
86 */
87 std::suspend_never initial_suspend() { return {}; }
88
89 /**
90 * @brief Determines whether the coroutine suspends before destruction
91 * @return A suspend always awaitable, meaning the coroutine suspends before completing
92 *
93 * By returning std::suspend_always, this method ensures that the coroutine
94 * frame isn't destroyed immediately when the coroutine completes. This gives
95 * the scheduler a chance to observe the completion and perform cleanup.
96 */
97 CompletionAwaiter final_suspend() noexcept { return { this }; }
98
99 /**
100 * @brief Handles the coroutine's void return
101 *
102 * This method is called when the coroutine executes a co_return statement
103 * without a value, or reaches the end of its function body.
104 */
105 void return_void() { }
106
107 /**
108 * @brief Handles exceptions thrown from within the coroutine
109 *
110 * This method is called if an unhandled exception escapes from the coroutine.
111 * The current implementation terminates the program, as exceptions in audio
112 * processing code are generally considered fatal errors.
113 */
114 void unhandled_exception() { std::terminate(); }
115
116 /**
117 * @brief Token indicating how this coroutine should be processed
118 */
119 const ProcessingToken processing_token { ProcessingToken::ON_DEMAND };
120
121 /**
122 * @brief Flag indicating whether the coroutine should be automatically resumed
123 *
124 * When true, the scheduler will automatically resume the coroutine when
125 * the current sample position reaches next_sample. When false, the coroutine
126 * must be manually resumed by calling ::try_resume.
127 */
128 bool auto_resume = true;
129
130 /**
131 * @brief Flag indicating whether the coroutine should be terminated
132 *
133 * When set to true, the scheduler will destroy the coroutine rather than
134 * resuming it, even if it hasn't completed naturally. This allows for
135 * early termination of long-running coroutines.
136 */
137 std::atomic<bool> should_terminate { false };
138
139 std::atomic<bool> completion_signaled { false };
140 std::atomic<bool> completion_registered { false };
141 std::atomic<bool> completion_notified { false };
142 std::function<void()> completion_callback;
143
144 /**
145 * @brief Dictionary for storing arbitrary state data
146 *
147 * This map allows the coroutine to store and retrieve named values of any type.
148 * It serves multiple purposes:
149 * 1. Persistent storage between suspensions
150 * 2. Communication channel between the coroutine and external code
151 * 3. Parameter storage for configurable behaviors
152 *
153 * The use of std::any allows for type-safe heterogeneous storage without
154 * requiring the promise type to be templated.
155 */
156 std::unordered_map<std::string, std::any> state;
157
158 /**
159 * @brief Flag indicating whether the coroutine should synchronize with the audio clock
160 *
161 * When true, the coroutine will be scheduled to run in sync with the specificed clock,
162 * via tokens ensuring sample-accurate timing. When false, it has to be "proceeded" manually
163 */
164 const bool sync_to_clock = false;
165
166 /**
167 * @brief Amount of delay requested by the coroutine
168 *
169 * This value is set when the coroutine co_awaits a delay awaiter (e.g., SampleDelay).
170 * It indicates how many time units the coroutine wishes to wait before resuming.
171 */
172 uint64_t delay_amount = 0;
173
174 /**
175 * @brief Stores a value in the state dictionary
176 * @param key Name of the state value
177 * @param value Value to store
178 *
179 * This method provides a type-safe way to store values of any type in the
180 * state dictionary. The value is wrapped in std::any for type erasure.
181 */
182 template <typename T>
183 inline void set_state(const std::string& key, T value)
184 {
185 state[key] = std::make_any<T>(std::move(value));
186 }
187
188 /**
189 * @brief Retrieves a value from the state dictionary
190 * @param key Name of the state value to retrieve
191 * @return Pointer to the stored value, or nullptr if not found or type mismatch
192 *
193 * This method provides a type-safe way to retrieve values from the state
194 * dictionary. It returns a pointer to the stored value if it exists and
195 * has the requested type, or nullptr otherwise.
196 */
197 template <typename T>
198 inline T* get_state(const std::string& key)
199 {
200 auto it = state.find(key);
201 if (it != state.end()) {
202 try {
203 return std::any_cast<T>(&it->second);
204 } catch (const std::bad_any_cast&) {
205 return nullptr;
206 }
207 }
208 return nullptr;
209 }
210
211 void domain_mismatch_error(const std::string& awaiter_name, const std::string& suggestion)
212 {
213 set_state("domain_error", awaiter_name + ": " + suggestion);
214 should_terminate = true;
215 }
216};
217
218/**
219 * @struct audio_promise
220 * @brief Coroutine promise type for audio processing tasks with sample-accurate timing
221 *
222 * The promise_type is a crucial component of C++20 coroutines that defines the
223 * behavior of SoundRoutine coroutines. It serves as the control interface between
224 * the coroutine machinery and the audio engine, managing:
225 *
226 * 1. Coroutine lifecycle (creation, suspension, resumption, and destruction)
227 * 2. Timing information for sample-accurate scheduling
228 * 3. State storage for persistent data between suspensions
229 * 4. Control flags for execution behavior
230 *
231 * In the coroutine model, the promise object is created first when a coroutine
232 * function is called. It then creates and returns the SoundRoutine object that
233 * the caller receives. The promise remains associated with the coroutine frame
234 * throughout its lifetime, while the SoundRoutine provides the external interface
235 * to manipulate the coroutine.
236 *
237 * This separation of concerns allows the audio engine to schedule and manage
238 * coroutines efficiently while providing a clean API for audio processing code.
239 */
240struct MAYAFLUX_API audio_promise : public routine_promise<SoundRoutine> {
241 /**
242 * @brief Creates the SoundRoutine object returned to the caller
243 * @return A new SoundRoutine that wraps this promise
244 *
245 * This method is called by the compiler-generated code when a coroutine
246 * function is invoked. It creates the SoundRoutine object that will be
247 * returned to the caller and associates it with this promise.
248 */
249 SoundRoutine get_return_object();
250
251 ProcessingToken processing_token { ProcessingToken::SAMPLE_ACCURATE };
252
253 bool sync_to_clock = true;
254
255 /**
256 * @brief The sample position when this coroutine should next execute
257 *
258 * This is the core timing mechanism for sample-accurate scheduling.
259 * When a coroutine co_awaits a SampleDelay, this value is updated to
260 * indicate when the coroutine should be resumed next.
261 */
262 uint64_t next_sample = 0;
263
264 /**
265 * @brief The buffer cycle when this coroutine should next execute
266 * Managed by BufferDelay awaiter. Incremented on each co_await BufferDelay{}.
267 * Starts at 0, incremented to 1 on first await.
268 */
269 uint64_t next_buffer_cycle = 0;
270
271 /**
272 * @brief The active delay context for this coroutine
273 *
274 * This value indicates which type of delay (sample, buffer, event)
275 * is currently being awaited by the coroutine. It helps the scheduler
276 * determine how to manage the coroutine's timing.
277 */
278 DelayContext active_delay_context = DelayContext::NONE;
279};
280
281/**
282 * @struct graphics_promise
283 * @brief Coroutine promise type for graphics processing tasks with frame-accurate timing
284 *
285 * graphics_promise is the frame-domain equivalent of audio_promise. It manages the
286 * state and lifecycle of GraphicsRoutine coroutines, providing frame-accurate timing
287 * and scheduling for visual processing tasks.
288 *
289 * Key Architectural Notes:
290 * - Frame timing is managed by FrameClock (self-driven wall-clock based)
291 * - Unlike audio_promise (driven by audio backend callbacks), graphics_promise observes
292 * the FrameClock which ticks independently in the graphics thread loop
293 * - The promise doesn't care HOW timing advances, only that it receives tick updates
294 * - Mirrors audio_promise architecture but for the visual/frame domain
295 *
296 * Timing Flow:
297 * 1. Graphics thread loop: m_frame_clock->tick() (self-driven)
298 * 2. GraphicsSubsystem::process() called
299 * 3. Scheduler::process_frame_coroutines_impl() checks routines
300 * 4. If current_frame >= next_frame, routine->try_resume(current_frame)
301 * 5. Routine updates next_frame based on FrameDelay amount
302 */
303struct MAYAFLUX_API graphics_promise : public routine_promise<GraphicsRoutine> {
304 /**
305 * @brief Creates the GraphicsRoutine object returned to the caller
306 * @return A new GraphicsRoutine that wraps this promise
307 *
308 * This method is called by the compiler-generated code when a coroutine
309 * function is invoked. It creates the GraphicsRoutine object that will be
310 * returned to the caller and associates it with this promise.
311 */
312 GraphicsRoutine get_return_object();
313
314 /**
315 * @brief Processing token indicating frame-accurate scheduling
316 */
317 ProcessingToken processing_token { ProcessingToken::FRAME_ACCURATE };
318
319 /**
320 * @brief Whether this routine should synchronize with FrameClock
321 */
322 bool sync_to_clock = true;
323
324 /**
325 * @brief The frame position when this coroutine should next execute
326 *
327 * This is the core timing mechanism for frame-accurate scheduling.
328 * When a coroutine co_awaits a FrameDelay, this value is updated to
329 * indicate when the coroutine should be resumed next.
330 *
331 * Example:
332 * - Current frame: 1000
333 * - co_await FrameDelay{5}
334 * - next_frame becomes: 1005
335 * - Routine resumes when FrameClock reaches frame 1005
336 */
337 uint64_t next_frame = 0;
338
339 /**
340 * @brief The active delay context for this coroutine
341 *
342 * This value indicates which type of delay (frame, event, etc.)
343 * is currently being awaited by the coroutine. It helps the scheduler
344 * determine how to manage the coroutine's timing and prevents
345 * cross-domain contamination (e.g., audio delays don't affect graphics routines).
346 *
347 * Valid states for graphics routines:
348 * - NONE: No active delay, can resume immediately
349 * - FRAME_BASED: Waiting for a specific frame (FrameDelay)
350 * - EVENT_BASED: Waiting for a window/input event (EventAwaiter)
351 * - AWAIT: Temporary state during GetPromise awaiter
352 */
353 DelayContext active_delay_context = DelayContext::NONE;
354
355 /**
356 * @brief The amount of delay units for incremental delays
357 *
358 * When using delays that accumulate (like BufferDelay for audio),
359 * this stores the increment amount. For graphics, this would be
360 * the frame increment for continuous animations.
361 *
362 * Example:
363 * ```cpp
364 * auto animation = []() -> GraphicsRoutine {
365 * while (true) {
366 * render_frame();
367 * co_await FrameDelay{1}; // delay_amount = 1
368 * }
369 * };
370 * ```
371 */
372 uint64_t delay_amount = 0;
373};
374
375/**
376 * @struct cross_promise
377 * @brief Coroutine promise for routines resumed by more than one clock.
378 *
379 * A cross routine reports ProcessingToken::MULTI_RATE and lives in the single
380 * MULTI_RATE task list, which both the sample-clock pump (audio thread) and the
381 * frame-clock pump (graphics thread) scan. When suspended on a single-clock
382 * awaiter only the matching thread resumes it. When suspended on MultiRateDelay
383 * the context is MULTIPLE: both threads contribute, and the gate uses a
384 * compare-exchange on active_delay_context so exactly one thread fires the
385 * resume after all required clocks are satisfied.
386 *
387 * active_delay_context, next_sample, next_frame, sample_satisfied, and
388 * frame_satisfied are read by both pumps concurrently with the await_suspend
389 * write, so they are atomic. The two delay-amount fields are written only in
390 * await_suspend and read only after the gate has claimed exclusivity, so they
391 * stay non-atomic.
392 */
393struct MAYAFLUX_API cross_promise : public routine_promise<CrossRoutine> {
394 CrossRoutine get_return_object();
395
396 /**
397 * @brief Processing token identifying this coroutine as multi-rate.
398 */
399 ProcessingToken processing_token { ProcessingToken::MULTI_RATE };
400
401 /**
402 * @brief Whether this routine should synchronize with a clock on initialization.
403 */
404 bool sync_to_clock = true;
405
406 /**
407 * @brief Sample position at which the sample-clock pump should next resume this routine.
408 *
409 * Written by MultiRateDelay::await_suspend via fetch_add and by
410 * initialize_state via store. Read concurrently by both pumps.
411 */
412 std::atomic<uint64_t> next_sample { 0 };
413
414 /**
415 * @brief Frame position at which the frame-clock pump should next resume this routine.
416 *
417 * Written by MultiRateDelay::await_suspend via fetch_add and by
418 * initialize_state via store. Read concurrently by both pumps.
419 */
420 std::atomic<uint64_t> next_frame { 0 };
421
422 /**
423 * @brief Active delay context controlling which pump(s) may resume this routine.
424 *
425 * Valid states for cross routines:
426 * - NONE: no suspension active, routine is running or uninitialized.
427 * - AWAIT: suspended in GetCrossPromise awaiter during initialization.
428 * - MULTIPLE: suspended on MultiRateDelay; one or both clocks are armed.
429 *
430 * The gate in try_resume_with_context CAS-es MULTIPLE -> NONE to claim
431 * the resume exclusively once all required clocks are satisfied.
432 */
433 std::atomic<DelayContext> active_delay_context { DelayContext::NONE };
434
435 /**
436 * @brief Number of samples requested by the current MultiRateDelay suspension.
437 *
438 * Zero means the sample clock is not required for this suspension.
439 * Written only in await_suspend; read only after the gate CAS succeeds.
440 */
441 uint64_t sample_delay_amount { 0 };
442
443 /**
444 * @brief Number of frames requested by the current MultiRateDelay suspension.
445 *
446 * Zero means the frame clock is not required for this suspension.
447 * Written only in await_suspend; read only after the gate CAS succeeds.
448 */
449 uint64_t frame_delay_amount { 0 };
450
451 /**
452 * @brief Set by the sample-clock pump when next_sample has been reached.
453 *
454 * Cleared before resume() fires so the flag is clean for the next
455 * suspension before any concurrent await_suspend can re-arm it.
456 */
457 std::atomic<bool> sample_satisfied { false };
458
459 /**
460 * @brief Set by the frame-clock pump when next_frame has been reached.
461 *
462 * Cleared before resume() fires so the flag is clean for the next
463 * suspension before any concurrent await_suspend can re-arm it.
464 */
465 std::atomic<bool> frame_satisfied { false };
466};
467
468/**
469 * @struct conditional_promise
470 * @brief Coroutine promise for routines suspended on an arbitrary boolean condition.
471 *
472 * FreeRoutine coroutines carry no clock. The scheduler's dedicated CONDITIONAL
473 * thread evaluates the stored condition on every iteration; when it returns true
474 * the handle is resumed. DelayContext stays NONE throughout because no delay is
475 * being modelled - the coroutine is simply waiting for a predicate to become
476 * satisfied.
477 *
478 * The condition is written by ConditionAwaiter::await_suspend and read by the
479 * scheduler thread. Both accesses are on different threads, so the condition
480 * field is protected by the same atomic flag pattern used in BroadcastSource:
481 * the awaiter stores condition + handle atomically before setting armed, and
482 * the scheduler thread reads only after observing armed == true.
483 */
484struct MAYAFLUX_API conditional_promise : public routine_promise<FreeRoutine> {
485 FreeRoutine get_return_object();
486
487 ProcessingToken processing_token { ProcessingToken::CONDITIONAL };
488
489 /**
490 * @brief Condition evaluated by the scheduler thread on each iteration.
491 *
492 * Written once per suspension by ConditionAwaiter::await_suspend.
493 * Read repeatedly by the scheduler thread until it returns true.
494 * Null when the coroutine is not suspended on a condition.
495 */
496 std::function<bool()> condition;
497
498 /**
499 * @brief True while the coroutine is suspended on a ConditionAwaiter.
500 *
501 * Set to true in await_suspend, cleared to false before handle.resume().
502 * The scheduler thread reads this to distinguish an active suspension
503 * from a running or completed coroutine.
504 */
505 std::atomic<bool> armed { false };
506};
507
508/**
509 * @struct EventPromise
510 * @brief Promise type for event-driven coroutines
511 *
512 * Unlike time-based promises (SampleClockPromise, FrameClockPromise),
513 * EventPromise has no clock. Coroutines suspend/resume based on
514 * discrete event signals, not periodic ticks.
515 */
516struct MAYAFLUX_API event_promise : public routine_promise<Event> {
517 Event get_return_object();
518
519 ProcessingToken processing_token { ProcessingToken::EVENT_DRIVEN };
520
521 DelayContext active_delay_context { DelayContext::EVENT_BASED };
522
523 /**
524 * @brief Transfer ownership of a NetworkSource to this coroutine frame.
525 * @param source Source to keep alive for the coroutine's lifetime.
526 */
527 void own(std::shared_ptr<Vruta::NetworkSource> source)
528 {
529 owned_sources.push_back(std::move(source));
530 }
531
532 /**
533 * @brief Coroutine-owned NetworkSource instances.
534 *
535 * Sources deposited here via GetEventPromise live for exactly the
536 * lifetime of the coroutine frame. Populated through co_await
537 * GetEventPromise{ source } at coroutine startup.
538 */
539 std::vector<std::shared_ptr<Vruta::NetworkSource>> owned_sources;
540};
541
542}
PickCallback callback
Definition Picker.cpp:22
glm::vec3 value
Coroutine resumed by more than one clock.
Definition Routine.hpp:663
Coroutine type for event-driven suspension.
Definition Event.hpp:26
Coroutine resumed when a caller-supplied condition becomes true.
Definition Routine.hpp:779
A C++20 coroutine-based graphics processing task with frame-accurate timing.
Definition Routine.hpp:512
A C++20 coroutine-based audio processing task with sample-accurate timing.
Definition Routine.hpp:327
MAYAFLUX_API void invoke_routine_completion(std::function< void()> callback) noexcept
Definition Routine.cpp:6
DelayContext
Discriminator for different temporal delay mechanisms.
Coroutine promise type for audio processing tasks with sample-accurate timing.
Definition Promise.hpp:240
std::function< bool()> condition
Condition evaluated by the scheduler thread on each iteration.
Definition Promise.hpp:496
Coroutine promise for routines suspended on an arbitrary boolean condition.
Definition Promise.hpp:484
Coroutine promise for routines resumed by more than one clock.
Definition Promise.hpp:393
void own(std::shared_ptr< Vruta::NetworkSource > source)
Transfer ownership of a NetworkSource to this coroutine frame.
Definition Promise.hpp:527
std::vector< std::shared_ptr< Vruta::NetworkSource > > owned_sources
Coroutine-owned NetworkSource instances.
Definition Promise.hpp:539
Coroutine promise type for graphics processing tasks with frame-accurate timing.
Definition Promise.hpp:303
void await_suspend(std::coroutine_handle<>) const noexcept
Definition Promise.hpp:67
void unhandled_exception()
Handles exceptions thrown from within the coroutine.
Definition Promise.hpp:114
void domain_mismatch_error(const std::string &awaiter_name, const std::string &suggestion)
Definition Promise.hpp:211
void on_complete(std::function< void()> callback)
Definition Promise.hpp:39
void notify_complete() noexcept
Definition Promise.hpp:54
void set_state(const std::string &key, T value)
Stores a value in the state dictionary.
Definition Promise.hpp:183
T * get_state(const std::string &key)
Retrieves a value from the state dictionary.
Definition Promise.hpp:198
void set_should_terminate(bool value) noexcept
Definition Promise.hpp:47
void return_void()
Handles the coroutine's void return.
Definition Promise.hpp:105
std::unordered_map< std::string, std::any > state
Dictionary for storing arbitrary state data.
Definition Promise.hpp:156
std::suspend_never initial_suspend()
Determines whether the coroutine suspends immediately upon creation.
Definition Promise.hpp:87
std::function< void()> completion_callback
Definition Promise.hpp:142
CompletionAwaiter final_suspend() noexcept
Determines whether the coroutine suspends before destruction.
Definition Promise.hpp:97
Base coroutine promise type for audio processing tasks.
Definition Promise.hpp:37