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