MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
BufferProcessingChain.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "BufferProcessor.hpp"
4
5namespace MayaFlux::Buffers {
6
7/**
8 * @class BufferProcessingChain
9 * @brief Advanced pipeline manager for multi-stage buffer transformations with backend optimization
10 *
11 * BufferProcessingChain organizes multiple BufferProcessor objects into sophisticated transformation
12 * pipelines for one or more buffers. This system enables complex multi-stage computational processes
13 * to be applied to data in a controlled, deterministic order while leveraging the expanded capabilities
14 * of modern BufferProcessors for optimal performance and backend utilization.
15 *
16 * The chain implements an intelligent directed acyclic graph (DAG) of transformations, maintaining
17 * separate processor sequences for each buffer while enabling advanced features:
18 *
19 * **Backend-Aware Processing:**
20 * - Automatic backend optimization based on processor recommendations
21 * - Intelligent batching of compatible processors for parallel execution
22 * - Dynamic backend switching to optimize processing pipelines
23 * - Resource-aware scheduling to prevent backend conflicts
24 *
25 * **Multi-Modal Data Support:**
26 * - Seamless processing of different data types (audio, video, texture) within unified chains
27 * - Type-safe processor assignment and validation
28 * - Cross-domain transformations between different buffer types
29 * - Unified interface for heterogeneous data processing
30 *
31 * **Performance Optimization:**
32 * - Processor compatibility validation and automatic optimization
33 * - Complexity-based scheduling for optimal resource utilization
34 * - Parallel execution of independent processing stages
35 * - Memory layout optimization for improved cache performance
36 *
37 * Key features:
38 * - Enables construction of complex computational pipelines with backend optimization
39 * - Supports both parallel and sequential transformation patterns with automatic selection
40 * - Preserves transformation order while optimizing execution strategy
41 * - Provides special "final" processors for guaranteed post-processing operations
42 * - Allows dynamic reconfiguration of transformation pipelines at runtime
43 * - Leverages processor agency for optimal backend selection and resource utilization
44 */
45class MAYAFLUX_API BufferProcessingChain {
46public:
47 friend class BufferProcessor;
48
49 /**
50 * @brief Adds a processor to the transformation pipeline for a specific buffer
51 * @param processor Processor to add
52 * @param buffer Buffer to associate with this processor
53 * @param rejection_reason Optional reason for rejection if the processor cannot be added
54 * @return true if the processor was successfully added, false if it was rejected
55 *
56 * The processor is added to the end of the transformation sequence for the specified buffer.
57 * If this is the first processor for the buffer, a new sequence is created. The chain
58 * performs intelligent validation and optimization:
59 *
60 * - **Compatibility Validation**: Ensures the processor can handle the buffer's data type
61 * - **Backend Analysis**: Analyzes processor backend preferences for optimization opportunities
62 * - **Pipeline Optimization**: May reorder or batch processors for improved performance
63 * - **Resource Planning**: Allocates necessary resources for the processor's execution
64 */
65 bool add_processor(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer, std::string* rejection_reason = nullptr);
66
67 /**
68 * @brief Removes a processor from the pipeline for a specific buffer
69 * @param processor Processor to remove
70 * @param buffer Buffer to remove the processor from
71 *
72 * If the processor is found in the buffer's transformation sequence, it is removed
73 * and its on_detach method is called. The chain also performs cleanup optimization:
74 *
75 * - **Resource Cleanup**: Ensures all processor resources are properly released
76 * - **Pipeline Reoptimization**: Rebuilds optimization plans without the removed processor
77 * - **Backend Restoration**: Restores default backends if the processor was overriding them
78 */
79 void remove_processor(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
80
81 /**
82 * @brief Removes a processor from any slot associated with a buffer.
83 * @param processor Processor to remove
84 * @param buffer Buffer to remove the processor from
85 *
86 * Searches the regular, preprocessor, postprocessor, and final slots.
87 */
88 void remove_processor_anywhere(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
89
90 /**
91 * @brief Applies the transformation pipeline to a buffer with intelligent execution
92 * @param buffer Buffer to transform
93 *
94 * Applies each processor in the buffer's sequence using an optimized execution strategy.
95 * The chain leverages processor capabilities for maximum performance:
96 *
97 * - **Backend Optimization**: Uses processor-recommended backends when beneficial
98 * - **Parallel Execution**: Executes compatible processors in parallel when possible
99 * - **Resource Management**: Optimally allocates CPU, GPU, and memory resources
100 * - **Error Handling**: Provides robust error recovery and fallback mechanisms
101 *
102 * This does not include the final processor, which must be applied separately with
103 * process_final() to ensure proper pipeline completion.
104 */
105 void process(const std::shared_ptr<Buffer>& buffer);
106
107 /**
108 * @brief Applies preprocessors, processing chain, post processors and final processors sequentially to a buffer
109 * @param buffer Buffer to transform
110 *
111 * Use this when explicit control of order is not needed, and you want to ensure that all stages
112 * of the processing pipeline are applied in a strict sequence. This method guarantees that
113 * preprocessors, main processors, postprocessors, and final processors are executed one after
114 * the other, maintaining the exact order of operations as defined in the chain.
115 */
116 void process_complete(const std::shared_ptr<Buffer>& buffer);
117
118 /**
119 * @brief Sets a preprocessor to be applied before the main pipeline
120 * @param processor Preprocessor to add
121 * @param buffer Buffer to associate with this preprocessor
122 *
123 * The preprocessor is applied before all regular processors when process() is called.
124 * This is useful for initial data preparation steps that must occur prior to the
125 * main transformation sequence, such as format conversion, normalization, or validation.
126 * NOTE: This runs after the buffer's own default processor. If you wish this to be the
127 * preprocessor, remove the default processor first.
128 * This is done to allow buffers to configure their own default processing behavior.
129 * i.e NodeBuffer WILL acquire node data using its default processor before any processing chain preprocessor.
130 */
131 void add_preprocessor(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
132
133 /**
134 * @brief Sets a postprocessor to be applied after the main pipeline
135 * @param processor Postprocessor to add
136 * @param buffer Buffer to associate with this postprocessor
137 *
138 * The postprocessor is applied after all regular processors when process() is called.
139 * This is useful for final data adjustments that must occur immediately after the
140 * main transformation sequence, such as clamping values, applying effects, or cleanup.
141 * NOTE: This is different from the final processor, and runs before it.
142 */
143 void add_postprocessor(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
144
145 /**
146 * @brief Sets a special processor to be applied after the main pipeline
147 * @param processor Final processor to add
148 * @param buffer Buffer to associate with this final processor
149 *
150 * The final processor is applied after all regular processors when process_final() is called.
151 * This is essential for operations like normalization, boundary enforcement, format conversion,
152 * or validation that must be applied as the last step in a transformation pipeline, regardless
153 * of the optimization strategies used for the main processing sequence.
154 */
155 void add_final_processor(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
156
157 /**
158 * @brief Checks if a buffer has any processors in its pipeline
159 * @param buffer Buffer to check
160 * @return true if the buffer has processors, false otherwise
161 *
162 * This method enables dynamic pipeline management and optimization decisions
163 * based on the presence of processing stages for specific buffers.
164 */
165 bool has_processors(const std::shared_ptr<Buffer>& buffer) const;
166
167 /**
168 * @brief Gets all processors in a buffer's transformation pipeline
169 * @param buffer Buffer to get processors for
170 * @return Constant reference to the vector of processors
171 *
172 * Returns an empty vector if the buffer has no processors. This provides
173 * access to the processor sequence for analysis, optimization, or debugging
174 * purposes while maintaining the integrity of the processing pipeline.
175 */
176 const std::vector<std::shared_ptr<BufferProcessor>>& get_processors(const std::shared_ptr<Buffer>& buffer) const;
177
178 /**
179 * @brief Returns the preprocessor for a buffer, or nullptr if none is set.
180 * @param buffer Buffer to query
181 */
182 [[nodiscard]] std::shared_ptr<BufferProcessor>
183 get_preprocessor(const std::shared_ptr<Buffer>& buffer) const;
184
185 /**
186 * @brief Returns the postprocessor for a buffer, or nullptr if none is set.
187 * @param buffer Buffer to query
188 */
189 [[nodiscard]] std::shared_ptr<BufferProcessor>
190 get_postprocessor(const std::shared_ptr<Buffer>& buffer) const;
191
192 /**
193 * @brief Returns the final processor for a buffer, or nullptr if none is set.
194 * @param buffer Buffer to query
195 */
196 [[nodiscard]] std::shared_ptr<BufferProcessor>
197 get_final_processor(const std::shared_ptr<Buffer>& buffer) const;
198
199 /**
200 * @brief Gets the entire transformation pipeline structure
201 * @return Map of buffers to their processor sequences
202 *
203 * This provides access to the internal structure of the pipeline, mapping each
204 * buffer to its sequence of transformation processors. Essential for pipeline
205 * analysis, optimization planning, and system introspection capabilities.
206 */
207 inline std::unordered_map<std::shared_ptr<Buffer>, std::vector<std::shared_ptr<BufferProcessor>>> get_chain() const { return m_buffer_processors; }
208
209 /**
210 * @brief Combines another processing pipeline into this one with optimization
211 * @param other Chain to merge into this one
212 *
213 * Adds all processors from the other chain to this one, preserving their buffer
214 * associations and order while performing intelligent optimization. This enables
215 * the composition of complex transformation pipelines from simpler, reusable components:
216 *
217 * - **Compatibility Analysis**: Validates that merged processors are compatible
218 * - **Optimization Opportunities**: Identifies potential performance improvements in the combined pipeline
219 * - **Resource Consolidation**: Optimizes resource usage across the merged processors
220 * - **Backend Harmonization**: Resolves any backend conflicts between the chains
221 */
222 void merge_chain(const std::shared_ptr<BufferProcessingChain>& other);
223
224 /**
225 * @brief Applies the preprocessor to a buffer
226 * @param buffer Buffer to preprocess
227 *
228 * If the buffer has a preprocessor, it is applied before the main processing sequence.
229 * This is useful for initial data preparation steps that must occur prior to the
230 * main transformation sequence, such as format conversion, normalization, or validation.
231 */
232 void preprocess(const std::shared_ptr<Buffer>& buffer);
233
234 /**
235 * @brief Applies the postprocessor to a buffer
236 * @param buffer Buffer to postprocess
237 *
238 * If the buffer has a postprocessor, it is applied after the main processing sequence.
239 * This is useful for final data adjustments that must occur immediately after the
240 * main transformation sequence, such as clamping values, applying effects, or cleanup.
241 */
242 void postprocess(const std::shared_ptr<Buffer>& buffer);
243
244 /**
245 * @brief Applies the final processor to a buffer with guaranteed execution
246 * @param buffer Buffer to process
247 *
248 * If the buffer has a final processor, it is applied with guaranteed execution regardless
249 * of any optimization strategies or backend considerations. This is typically called after
250 * process() to apply final-stage transformations like normalization, boundary enforcement,
251 * or format validation that must complete successfully for pipeline integrity.
252 */
253 void process_final(const std::shared_ptr<Buffer>& buffer);
254
255 /**
256 * @brief Sets the preferred processing token for this chain
257 * @param token Preferred processing token to set
258 *
259 * This method allows the chain to specify a preferred processing domain that influences
260 * how processors are executed, including backend selection and execution strategy.
261 * The token can be used to optimize the entire pipeline based on the expected data type,
262 * processing requirements, and available hardware resources.
263 */
264 inline void set_preferred_token(ProcessingToken token) { m_token_filter_mask = token; }
265
266 /** * @brief Gets the preferred processing token for this chain
267 * @return Current preferred processing token
268 *
269 * Returns the currently set preferred processing token, which can be used by processors
270 * to optimize their execution strategies and backend selections. This token represents the
271 * overall processing domain that the chain aims to optimize for.
272 */
274 {
275 return m_token_filter_mask;
276 }
277
278 /**
279 * @brief Sets the token enforcement strategy for this chain
280 * @param strategy Token enforcement strategy to set
281 *
282 * This method allows the chain to specify how the processing token is enforced across the pipeline,
283 * including whether to filter processors based on their compatibility with the token. The default
284 * strategy is FILTERED, which applies the token only to compatible processors.
285 */
287 {
288 m_enforcement_strategy = strategy;
289 }
290
291 /**
292 * @brief Gets the current token enforcement strategy for this chain
293 * @return Current token enforcement strategy
294 *
295 * Returns the currently set token enforcement strategy, which determines how the processing token
296 * is applied to processors in the pipeline. This can influence whether processors are filtered
297 * based on their compatibility with the preferred processing token.
298 */
300 {
301 return m_enforcement_strategy;
302 }
303
304 /**
305 * @brief Optimizes the processing pipeline for improved performance
306 * @param buffer Buffer to optimize the pipeline for
307 *
308 * Analyzes the current processor sequence and applies various optimization strategies:
309 *
310 * - **Backend Consolidation**: Groups processors by preferred backend for batched execution
311 * - **Parallel Execution Planning**: Identifies processors that can run concurrently
312 * - **Memory Layout Optimization**: Optimizes data access patterns for cache efficiency
313 * - **Resource Balancing**: Balances processor load across available hardware resources
314 */
315 void optimize_for_tokens(const std::shared_ptr<Buffer>& buffer);
316
317 /**
318 * @brief Analyzes token compatibility across all processors in the chain
319 * @return Vector of TokenCompatibilityReport objects summarizing processor compatibility
320 *
321 * This method generates a detailed report on how each processor in the chain
322 * aligns with the preferred processing token, including compatibility status,
323 * enforcement strategy, and any processors that will be skipped or pending removal.
324 * Useful for debugging, optimization planning, and ensuring pipeline integrity.
325 */
326 std::vector<TokenCompatibilityReport> analyze_token_compatibility() const;
327
328 /** * @brief Validates all processors in the chain against the preferred processing token
329 * @param incompatibility_reasons Optional vector to store reasons for any incompatibilities
330 * @return true if all processors are compatible with the preferred token, false otherwise
331 *
332 * This method checks each processor in the chain against the preferred processing token,
333 * ensuring that all processors can execute under the current backend and execution strategy.
334 * If any incompatibilities are found, they can be reported through the provided vector.
335 */
336 bool validate_all_processors(std::vector<std::string>* incompatibility_reasons = nullptr) const;
337
338 /**
339 * @brief Enforces the chain's preferred processing token on all processors
340 *
341 * This method ensures that all processors in the chain are compatible with the
342 * preferred processing token, applying any necessary optimizations or removals
343 * of incompatible processors. It is typically called after setting a new preferred
344 * token or changing the enforcement strategy to ensure the pipeline remains valid.
345 */
346 void enforce_chain_token_on_processors();
347
348 inline bool has_pending_operations() const
349 {
350 return m_pending_count.load(std::memory_order_relaxed) > 0;
351 }
352
353 /**
354 * @brief Gets a processor of a specific type from the buffer's processing pipeline
355 * @tparam T Type of the processor to retrieve
356 * @param buffer Buffer to get the processor from
357 * @return Shared pointer to the processor if found, nullptr otherwise
358 *
359 * This method searches for a processor of the specified type in the buffer's
360 * transformation sequence. If found, it returns a shared pointer to the processor,
361 * allowing type-safe access to specialized functionality.
362 */
363 template <typename T>
364 std::shared_ptr<T> get_processor(const std::shared_ptr<Buffer>& buffer) const
365 {
366 auto processors = get_processors(buffer);
367
368 for (auto& processor : processors) {
369 if (auto t_processor = std::dynamic_pointer_cast<T>(processor)) {
370 return t_processor;
371 }
372 }
373 return nullptr;
374 }
375
376protected:
377 bool add_processor_direct(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer, std::string* rejection_reason = nullptr);
378 void remove_processor_direct(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
379 void remove_processor_anywhere_direct(const std::shared_ptr<BufferProcessor>& processor, const std::shared_ptr<Buffer>& buffer);
380
381private:
382 void remove_processor_internal(
383 const std::shared_ptr<BufferProcessor>& processor,
384 const std::shared_ptr<Buffer>& buffer,
385 bool search_all_slots);
386
387 /**
388 * @brief Internal processing method for non-owning buffer contexts
389 * @param buffer Buffer to process
390 *
391 * This method is used internally by BufferProcessingChain to process buffers
392 * that are not owned by the chain itself. It ensures that the processor's
393 * processing function is called in a thread-safe manner, managing the
394 * active processing state to prevent concurrent access issues.
395 */
396 void process_non_owning(const std::shared_ptr<Buffer>& buffer);
397
398 /**
399 * @brief Validates the processing token against the chain's preferred token
400 */
401 void cleanup_rejected_processors(const std::shared_ptr<Buffer>& buffer);
402
403 /**
404 * @brief Process pending processor operations
405 */
406 void process_pending_processor_operations();
407
408 bool queue_pending_processor_op(
409 const std::shared_ptr<BufferProcessor>& processor,
410 const std::shared_ptr<Buffer>& buffer,
411 bool is_addition,
412 bool search_all_slots = false,
413 std::string* rejection_reason = nullptr);
414
415 /**
416 * @brief Map of buffers to their processor sequences
417 *
418 * Each buffer has its own vector of processors that are applied in order when
419 * the buffer is processed. The sequence may be optimized for performance while
420 * maintaining the logical order of transformations.
421 */
422 std::unordered_map<std::shared_ptr<Buffer>, std::vector<std::shared_ptr<BufferProcessor>>> m_buffer_processors;
423
424 /**
425 * @brief Map of buffers to processors that are conditionally applied
426 */
427 std::unordered_map<std::shared_ptr<Buffer>, std::unordered_set<std::shared_ptr<BufferProcessor>>> m_conditional_processors;
428
429 /**
430 * @brief Map of buffers to processors pending removal
431 */
432 std::unordered_map<std::shared_ptr<Buffer>, std::unordered_set<std::shared_ptr<BufferProcessor>>> m_pending_removal;
433
434 /**
435 * @brief Map of buffers to their preprocessors
436 *
437 * Each buffer can have one preprocessor that is applied before the main
438 * processing sequence to prepare the data.
439 */
440 std::unordered_map<std::shared_ptr<Buffer>, std::shared_ptr<BufferProcessor>> m_preprocessors;
441
442 /**
443 * @brief Map of buffers to their postprocessors
444 *
445 * Each buffer can have one postprocessor that is applied after the main
446 * processing sequence to finalize the data.
447 */
448 std::unordered_map<std::shared_ptr<Buffer>, std::shared_ptr<BufferProcessor>> m_postprocessors;
449
450 /**
451 * @brief Map of buffers to their final processors
452 *
453 * Each buffer can have one final processor that is applied after the main
454 * processing sequence with guaranteed execution, regardless of optimization strategies.
455 */
456 std::unordered_map<std::shared_ptr<Buffer>, std::shared_ptr<BufferProcessor>> m_final_processors;
457
458 /**
459 * @brief Preferred processing token for this chain
460 *
461 * This token represents the preferred processing domain that influences how processors
462 * are executed, including backend selection and execution strategy. It can be used to
463 * optimize the entire pipeline based on the expected data type, processing requirements,
464 * and available hardware resources.
465 */
466 mutable ProcessingToken m_token_filter_mask { ProcessingToken::AUDIO_BACKEND };
467
468 /**
469 * @brief Token enforcement strategy for this chain
470 *
471 * This strategy determines how the processing token is enforced across the pipeline,
472 * including whether to filter processors based on their compatibility with the token.
473 * The default strategy is FILTERED, which applies the token only to compatible processors.
474 */
475 TokenEnforcementStrategy m_enforcement_strategy { TokenEnforcementStrategy::FILTERED };
476
477 static constexpr size_t MAX_PENDING_PROCESSORS = 32;
478
480 std::atomic<bool> active { false };
481 std::shared_ptr<BufferProcessor> processor;
482 std::shared_ptr<Buffer> buffer;
483 bool is_addition { true }; // true = add, false = remove
484 bool search_all_slots { false };
485 };
486
487 std::atomic<bool> m_is_processing;
488
489 PendingProcessorOp m_pending_ops[MAX_PENDING_PROCESSORS];
490
491 std::atomic<uint32_t> m_pending_count { 0 };
492};
493}
std::shared_ptr< NetworkGeometryBuffer > buffer
void set_preferred_token(ProcessingToken token)
Sets the preferred processing token for this chain.
std::unordered_map< std::shared_ptr< Buffer >, std::shared_ptr< BufferProcessor > > m_final_processors
Map of buffers to their final processors.
std::unordered_map< std::shared_ptr< Buffer >, std::unordered_set< std::shared_ptr< BufferProcessor > > > m_pending_removal
Map of buffers to processors pending removal.
std::unordered_map< std::shared_ptr< Buffer >, std::vector< std::shared_ptr< BufferProcessor > > > m_buffer_processors
Map of buffers to their processor sequences.
std::unordered_map< std::shared_ptr< Buffer >, std::vector< std::shared_ptr< BufferProcessor > > > get_chain() const
Gets the entire transformation pipeline structure.
std::unordered_map< std::shared_ptr< Buffer >, std::shared_ptr< BufferProcessor > > m_preprocessors
Map of buffers to their preprocessors.
std::shared_ptr< T > get_processor(const std::shared_ptr< Buffer > &buffer) const
Gets a processor of a specific type from the buffer's processing pipeline.
ProcessingToken get_preferred_token() const
Gets the preferred processing token for this chain.
TokenEnforcementStrategy get_enforcement_strategy() const
Gets the current token enforcement strategy for this chain.
void set_enforcement_strategy(TokenEnforcementStrategy strategy)
Sets the token enforcement strategy for this chain.
std::unordered_map< std::shared_ptr< Buffer >, std::unordered_set< std::shared_ptr< BufferProcessor > > > m_conditional_processors
Map of buffers to processors that are conditionally applied.
std::unordered_map< std::shared_ptr< Buffer >, std::shared_ptr< BufferProcessor > > m_postprocessors
Map of buffers to their postprocessors.
Advanced pipeline manager for multi-stage buffer transformations with backend optimization.
Central computational transformation interface for continuous buffer processing.
ProcessingToken
Bitfield enum defining processing characteristics and backend requirements for buffer operations.
TokenEnforcementStrategy
Defines how strictly processing token requirements are enforced in buffer processing chains.
void add_processor(const std::shared_ptr< Buffers::BufferProcessor > &processor, const std::shared_ptr< Buffers::Buffer > &buffer, Buffers::ProcessingToken token)
Adds a processor to a specific buffer.
Definition Graph.cpp:153
void remove_processor(const std::shared_ptr< Buffers::BufferProcessor > &processor, const std::shared_ptr< Buffers::Buffer > &buffer)
Removes a processor from a specific buffer.
Definition Graph.cpp:168