Core Architecture Design Principles¶
The CaptureMoment core library is designed with modularity, high performance, and future extensibility in mind, which is essential for a modern, tile-based image processing engine. This document outlines the key architectural decisions, focusing on how we separate data from behavior and use established design patterns.
1. Data-Centric Design: Plain Old Data (POD) / Value Types¶
A fundamental principle of this architecture is the segregation of data storage from processing logic. Core data structures are kept simple and easy to handle:
-
CaptureMoment::Core::Common::ImageRegion: A Plain Old Data (POD) struct representing a rectangular region of an image. It holds raw pixel data (std::vector<float>), dimensions (m_width,m_height), channel count (m_channels), and coordinates (m_x,m_y). Designed as a value type for efficient copying/moving by value. IncludesisValid()for overflow-safe integrity checks and safe accessors (width(),height(),channels()). ProvidesgetBuffer()returningstd::span<float>for zero-copy algorithm access. Includes a dedicated constructorImageRegion(std::span<const float>, ImageDim, ImageDim, ImageChan)for efficient creation from non-owning views. -
CaptureMoment::Core::Operations::OperationDescriptor: A POD struct holding parameters for a single operation instance (e.g.,OperationType,OperationParams,uint64_t id). It acts as a configuration snapshot passed toIOperationimplementations. Designed as a value type for easy storage and transmission. Theidfield provides a stable, unique key for pipeline caching and structural change detection. -
CaptureMoment::Core::Common::OperationParams: A POD struct containing the specific parameters for an operation (e.g.,float brightness,float contrast).
2. Manager and Engine Separation¶
The core functionality is split into specialized components: Managers handle resources and external concerns, while the Engine handles orchestration.
CaptureMoment::Core::Managers::ISourceManager & CaptureMoment::Core::Managers::SourceManager (Strategy Pattern)¶
- Problem: Direct dependency on OpenImageIO (OIIO) throughout the core would make testing difficult and tightly couple the core logic to OIIO's API.
- Solution: Introduce
ISourceManager. The concrete implementationSourceManageruses OIIO internally. - Impact:
- Loose Coupling: Higher-level components like
StateImageManagerdepend only on theISourceManagerinterface. - Testability: Mock implementations of
ISourceManagercan be used for unit tests. -
Abstraction: The rest of the application is insulated from OIIO's specific complexities.
-
Implementation (
SourceManager): The concrete implementation uses industry-standard tools like OpenImageIO (OIIO) to handle diverse file formats and efficient caching viaOIIO::ImageBufandOIIO::ImageCache, insulating the rest of the application from OIIO's specific complexities.
3. Hardware Abstraction Layer (IWorkingImageHardware)¶
- Problem: Implementing image processing logic separately for CPU and multiple GPU APIs (CUDA, OpenCL, Metal, DX12, Vulkan) leads to massive code duplication and maintenance overhead.
- Solution: Introduce the
IWorkingImageHardwareinterface. Concrete implementations (WorkingImageCPU_Halide,WorkingImageGPU_Halideand its descendants likeWorkingImageCUDA_Halide,WorkingImageVulkan_Halide) handle platform-specific details. - Impact:
- Unified Processing Logic: Algorithms (like the fused Halide pipeline execution and downsampling) operate on the
IWorkingImageHardwareinterface, oblivious to the underlying hardware. - Modularity: Adding a new backend involves implementing the interface, without touching the core processing logic.
- Flexibility: The system can dynamically choose the best backend (
AppConfig, benchmarking) at runtime or initialization. - Performance: The
downsamplemethod allows efficient generation of display-sized images, potentially leveraging GPU acceleration.
CaptureMoment::Core::ImageProcessing::WorkingImageHalide (Base Class)¶
- Shared Infrastructure: Base class providing common Halide buffer functionality for both CPU and GPU implementations.
- Memory Management: Uses
std::unique_ptr<float[]>(allocated viastd::make_unique_for_overwrite) as backing store for Halide buffers, enabling in-place modifications without unnecessary copies and avoiding zero-initialization overhead during allocation. - Pattern Explanation (Template Method): Defines common steps for buffer creation and management, allowing subclasses to customize specific parts (e.g., GPU buffer allocation/deallocation).
CaptureMoment::Core::ImageProcessing::WorkingImageData (Central Data Owner)¶
- Raw Data Storage: Provides the underlying
std::vector<float>(m_datafor working buffer,m_original_datafor source cache) and metadata (m_width,m_height,m_channels,m_valid) for CPU and GPU implementations. - Common Types: Uses
Common::ImageDim,Common::ImageChan,Common::ImageSizefor its members. - Initialization Logic: Contains the
initializeData(Common::ImageRegion&&)method, which handles the allocation and copying of pixel data from anImageRegionusing move semantics, and sets the metadata. - State Management: The
m_validflag is managed here and used by derived classes to determine the overall validity state. ProvidesrestoreOriginalData()to copym_original_databack tom_datausingstd::ranges::copy.
CaptureMoment::Core::ImageProcessing::ImageView (Zero-Copy View)¶
- Role: Lightweight, non-owning structure containing
std::span<float>for working data,std::span<const float>for original data, and geometry metadata. - Purpose: Enables efficient binding of external memory (from
WorkingImageContext) to hardware workers without transferring ownership or copying data.
WorkingImage Refactoring¶
- New Base Classes & Architecture: Introduced
WorkingImageData(for raw data and metadata) andWorkingImageHalide(for shared Halide logic).IWorkingImageHardwarenow exposesbindView(const ImageView&)to attach data views instead of passingImageRegionat construction. - Hierarchical Structure: Concrete implementations like
WorkingImageCPU_HalideandWorkingImageGPU_Halidenow inherit from specific base classes (WorkingImageCPU/WorkingImageGPU) which themselves inherit fromIWorkingImageHardware,WorkingImageData, andWorkingImageHalide. - Unified Buffer Initialization:
WorkingImageHalide::initializeHalidenow accepts astd::span<float>for safer and more flexible buffer view creation. - Method Updates:
exportToCPUCopy()has been renamed togetFullResImage()for semantic clarity.updateFromCPU()has been replaced bytransferToVRAM()for GPU and removed for CPU backends (data is now bound directly via views).
CaptureMoment::Core::ImageProcessing::WorkingImageFactory (Factory & Registry Pattern)¶
This factory encapsulates the logic for creating the appropriate IWorkingImageHardware implementation based on the configured backend.
- Responsibility: Centralize creation logic to avoid duplication in
StateImageManagerandPhotoTask. - Signature: Creator functions now return empty hardware objects (
std::function<std::unique_ptr<IWorkingImageHardware>()>). Data binding is deferred toWorkingImageContext::prepare()which callsbindView()after instantiation.
4. Pipeline Fusion with Halide¶
- Problem: Applying multiple adjustments sequentially (e.g., brightness -> contrast -> saturation) involves multiple passes over the image buffer, leading to poor performance and potential rounding errors.
- Solution: Use Halide to fuse multiple operations into a single computational pipeline executed in one pass, with automatic conversion to/from Oklab perceptual color space.
- Impact:
- Performance: Dramatically reduces execution time by minimizing memory bandwidth usage and loop overhead.
- Quality: Reduces cumulative floating-point errors by performing all calculations in a single pass; Oklab ensures perceptually uniform adjustments without hue shifts.
- Hardware Agnostic: The fused pipeline system works seamlessly across different hardware backends with backend-specific memory layout optimizations.
Pipeline Flow with Oklab Integration¶
Input RGB (Chunky, linear)
↓
[CPU] Transpose: Chunky(x,y,c) → Planar(c,y,x) → Chunky(x,y,c) // For SIMD vectorization
[GPU] Direct Chunky(x,y,c) // For GPU memory coalescing
↓
rgb_to_oklab() → [L, a, b, Alpha] // Alpha preserved (c==3 passthrough)
↓
applyOperations() → Fused ops on L channel only (a, b, Alpha unchanged)
↓
oklab_to_rgb() → Output RGB (Chunky)
↓
Final clamp [0,1] applied post-conversion for display/export
Operation Fusion Logic Update¶
- Oklab-First Design: All basic tone adjustments (Brightness, Contrast, Highlights, Shadows, Whites, Blacks) now operate exclusively on the Oklab
Lchannel (c == 0). Thea,b, and Alpha channels are preserved unchanged to prevent hue shifts. - Smoothstep Masks: Regional adjustments use the smoothstep polynomial
3t² - 2t³for mask generation, eliminating banding artifacts in tonal roll-offs. - Deferred Clamping: No intermediate
clamp()on theLchannel during operations; clamping is applied only afteroklabToRgb()conversion to avoid color distortion.
Dynamic Input/Output Binding Correction¶
- Problem: An earlier version of
OperationPipelineExecutorattempted to statically compile the entire Halide pipeline, including the input node, leading to incorrect execution where the input buffer was not properly linked during runtime. - Solution: The pipeline execution was corrected to bind the input
IWorkingImageHardwarebuffer dynamically at runtime before running the compiled pipeline.IHalidePipelineExecutor::executeOnHalideBuffernow accepts separateinput_bufferandoutput_bufferparameters. - Impact:
- Correctness: Ensures the pipeline operates on the intended image data.
- Flexibility: Allows the same compiled pipeline to process different image instances or perform non-destructive transformations with different output dimensions.
Executor Refactoring: Generic applyOperations() + Backend-Specific buildOperationChain()¶
applyOperations(const Halide::Func& input, x, y, c): Protected generic method inOperationPipelineExecutorthat chains operations viaappendToFusedPipeline(). Backend-agnostic; works on any input Func.buildOperationChain(): Pure virtual protected method implemented byOperationPipelineExecutorCPUandOperationPipelineExecutorGPU. Each backend:- Wraps
m_inputinto a chunky Func. - Applies backend-specific layout transformations (CPU: transpose for planar logic; GPU: direct chunky).
- Calls
ColorSpace::rgbToOklab()→applyOperations()→ColorSpace::oklabToRgb(). - Sets output strides (
dim(0).set_stride(4),dim(2).set_stride(1)) and applies backend-specific scheduling. - Compiles JIT and stores result in
m_pipeline.
Backend-Specific Layout Strategies¶
| Backend | Memory Layout | Rationale |
|---|---|---|
| CPU | Chunky → Planar(c,y,x) → Chunky | Enables reorder(c, x, y).unroll(c) for contiguous SIMD loads on 4 channels |
| GPU | Direct Chunky(x,y,c) | Preserves memory coalescing for GPU threads; gpu_tile(x, y, ...) handles parallelism |
- Stride Constraints: Both backends enforce
dim(0).set_stride(4)anddim(2).set_stride(1)on output buffers to match the expected interleaved RGBA layout.
Pipeline Executor Interaction¶
- **
OperationPipelineExecutorreceives theIWorkingImageHardwareobject via theexecutemethod parameter, which originates fromWorkingImageContext. It then interacts with this object as described in Section 3 and Section 9.
5. Operation Management¶
- Problem: Hard-coding specific operations into the engine makes the system inflexible and difficult to extend.
- Solution: Use
OperationDescriptor(POD) andIOperation(interface/factory pattern). - Impact:
- Extensibility: New operations can be added by implementing
IOperationand registering them. - Decoupling: The
PhotoEngineandStateImageManagerdon't need to know about specific operation types, only theOperationDescriptorand theIOperationinterface. -
Persistence:
OperationDescriptoris easily serializable. -
Why Value Type? It acts as a configuration snapshot. The
OperationPipelineuses this descriptor to instruct theOperationFactorywhat to create and what parameters to apply, keeping the engine itself free from hard-coded operation logic. -
Sequential vs Fused: Operations now rely exclusively on
appendToFusedPipeline()for fused execution.ISingleOperationprovidesexecute()as a fallback for debugging or standalone execution when fusion is not applicable.
6. Asynchronous Workers¶
- Problem: Long-running tasks (like heavy image processing or I/O) can block the UI thread.
- Solution: Implement the Worker Pattern using
std::asyncor similar for background execution. - Impact:
- Responsiveness: Keeps the UI thread free.
-
Scalability: Allows parallel execution of independent tasks.
-
Pattern Explanation (Worker/Active Object):
IWorkerRequestdefines the work contract.WorkerBuildercreates specific worker instances (e.g.,HalideOperationWorker).WorkerContextprovides access to shared resources without tight coupling.
7. Error Handling and Logging¶
- Problem: Propagating errors through a complex, layered architecture without throwing exceptions everywhere.
- Solution: Use
std::expected<T, Error>for functions that can fail, andspdlogfor structured logging. - Impact:
- Explicit Error Handling: Forces callers to deal with potential failures.
- Debugging: Comprehensive logs help diagnose issues.
8. Serialization System¶
- Problem: Saving and restoring the state of image adjustments in a standard, portable way.
- Solution: Use Exiv2 to embed operation descriptors into image file XMP metadata.
- Impact:
- Non-Destructive Editing: Preserves original image data.
-
Portability: XMP is a widely supported standard.
-
Pattern Explanation (Facade):
FileSerializerManagerprovides a simplified interface to the complex serialization subsystem (IXmpProvider,IXmpPathStrategy,IFileSerializerWriter/Reader). -
Usage: Relies on
CaptureMoment::Core::Serializer::OperationSerializationfor parameter conversion, which in turn uses generic conversion utilities fromCaptureMoment::Core::utils.
9. Memory Management¶
- Problem: Efficiently handling large image buffers without excessive copying or unnecessary initialization.
- Solution:
- Use
std::span<float>for non-owning views of data, minimizing copies when passing buffers to algorithms (like Halide). - Use
std::vector<float>for owning allocations inWorkingImageData, leveraging RAII and automatic memory management. - Use move semantics (
std::move) for transferring ownership of large objects likestd::vectororImageRegionbetween functions/components. - Impact:
- Performance: Reduces allocation time and memory bandwidth usage.
- Safety: RAII and smart pointers prevent leaks.
10. Configuration Management¶
- Problem: Managing settings like selected GPU backend, performance preferences, UI language, etc.
- Solution: Centralized
AppConfigobject holding these parameters, potentially loaded from a file. - Impact:
- Maintainability: Single source of truth for configuration.
- Flexibility: Easy to modify behavior based on settings.
11. Performance Optimization Techniques¶
- Zero-Copy Processing:
WorkingImageHalidebase class eliminates unnecessary data copying by sharing memory betweenstd::vector<float>andHalide::Buffer. - In-Place Processing: Halide buffers operate directly on shared data vectors, eliminating redundant copies.
- Optimized Scheduling: Pipeline fusion creates single computational passes instead of multiple sequential operations.
- Backend Selection: Runtime benchmarking automatically determines optimal CPU/GPU usage.
- Pipeline Fusion Optimization: Fused Execution: Operations support only fused (
appendToFusedPipeline) execution patterns. - Hardware-Accelerated Downsampling: The
IWorkingImageHardware::downsamplemethod allows generating display-sized images efficiently, potentially on the GPU. CPU implementation usesOIIO::ImageBufAlgo::resizewith optimized tiling (split(y, yo, yi, 32).parallel(yo).vectorize(x, 8)).
12. Data Flow and Immutability Considerations¶
- Problem: Managing state changes and ensuring data consistency in a pipeline with potential asynchronous operations.
- Solution:
StateImageManageracts as the single authority for the current "working image".- Operations modify the image in-place on the
IWorkingImageHardwaremanaged byWorkingImageContext. - Persistent Worker Model: A single background thread processes operations sequentially; rapid UI updates (e.g., slider drag) are coalesced so only the latest state is executed.
- Explicit Synchronization: Callers wait on
std::future<bool>fromapplyOperations();waitForPendingProcessing()uses atomic idle checks +yield()for efficient blocking. WorkingImageDatamaintains a cached original buffer (m_original_data) enablingrestoreOriginalData()without re-reading from disk.
13. Testing Strategy¶
- Unit Tests: Individual components (Operations, Factories, Utils) are tested in isolation using mocks for dependencies.
- Integration Tests: Higher-level workflows (e.g.,
StateImageManager::applyOperations) are tested with real or near-real dependencies. - Performance Tests: Benchmark critical paths (pipeline execution, loading times, downsampling) to monitor regressions.
14. Future Extensibility¶
- Plugin Architecture: The Factory and Strategy patterns lay groundwork for potentially loading operations or backends dynamically as plugins.
- New Backends: The
IWorkingImageHardwareinterface allows adding new hardware targets (e.g., future GPU APIs, specialized accelerators). - Advanced Pipelines: The
IPipelineManagerstrategy could be extended to manage different types of processing (e.g., AI inference, advanced filters).
15. Namespace Organization¶
The codebase is structured using a clear namespace hierarchy to improve modularity and maintainability:
CaptureMoment::Core::Common: Contains fundamental data structures likeImageRegionandPixelFormat, and common types (ImageTypes).CaptureMoment::Core::Engine: Contains the main orchestratorPhotoEngine.CaptureMoment::Core::Managers: Contains resource managers likeStateImageManager,SourceManager.CaptureMoment::Core::Operations: Contains operation logic and descriptors.CaptureMoment::Core::Factories: Contains factory classes likeOperationFactory.CaptureMoment::Core::ImageProcessing: Contains image buffer abstractions (IWorkingImageHardware,ImageView,WorkingImageData,WorkingImageHalide) and processing helpers.CaptureMoment::Core::Pipeline: Contains pipeline fusion and execution logic (IPipelineExecutor,OperationPipelineExecutor,OperationPipelineExecutorCPU,OperationPipelineExecutorGPU,PipelineContext,PipelineRegistry).CaptureMoment::Core::Strategies: Contains high-level processing strategies (IPipelineManager).CaptureMoment::Core::Workers: Contains asynchronous processing workers (IWorkerRequest).CaptureMoment::Core::Serialization: Contains serialization/deserialization logic (FileSerializerManager,OperationSerialization).CaptureMoment::Core::utils: Contains generic utility functions (toString,ImageConversion).
StateImageManager as Central Coordinator¶
- Threading Model: Uses a single persistent background thread (
m_worker_thread) for all image processing, avoiding thread creation/destruction overhead during rapid UI interactions. - Coalescing Strategy: When
applyOperations()is called while the worker is busy, new operations overwrite the pending queue. Supersededstd::futureobjects are resolved immediately to prevent UI blocking. - Synchronization: Uses
std::condition_variable+std::mutexfor efficient wait/notify;std::atomic<bool>for lock-free idle state checks.
Asynchronous Processing & Coalescing¶
- Persistent Worker:
StateImageManagerowns a singlestd::threadrunningworkerLoop(), which waits on astd::condition_variablefor new work. - Coalescing Logic:
- New
applyOperations()calls overwritem_pending_workif the worker is busy. - Superseded
std::promise<bool>objects are resolved immediately withset_value(true)to unblock callers. - Only the latest operation list is executed, ensuring UI responsiveness during rapid interactions.
- Shutdown: Clean termination via
m_stop_requestedatomic flag +join()in destructor. - Thread Safety: All shared state (
m_pending_work,m_active_promise,m_is_idle) protected bym_work_mutex; atomics used for idle/stop flags.
Simplified PhotoEngine Architecture¶
- Reduced Coupling:
PhotoEngineconstructor now has zero parameters, with internal managers handling their own dependencies. It no longer directly ownsSourceManager. - Centralized Management:
StateImageManagernow ownsm_source_manager,m_pipeline_builder, andm_operation_factoryfor better encapsulation and clearer responsibilities.
Pipeline Management Refactoring¶
- Global Registry Pattern: Introduced
PipelineBuilder(global registry) andPipelineRegistryfor flexible executor creation.PipelineRegistry::registerHalideExecutors()queriesAppConfig::getProcessingBackend()and registers eitherOperationPipelineExecutorCPUorOperationPipelineExecutorGPU. - Strategy Pattern: Introduced
IPipelineManagerandPipelineHalideOperationManagerto control the execution strategy of operation pipelines, allowing for different approaches (e.g., fused vs. sequential) based on requirements. -
PipelineExecutor Split:
OperationPipelineExecutoris now an abstract base class.applyScheduling()and backend-specific execution logic are delegated toOperationPipelineExecutorCPUandOperationPipelineExecutorGPU. Scheduling usestarget.has_gpu_feature()for adaptive compilation. -
Global Builder Usage:
PipelineContextusesPipelineBuilder::build()to create the appropriateIPipelineExecutor.
Operation Descriptor Enhancement & Pipeline Optimization¶
- Unique Identifier:
OperationDescriptornow includes auint64_t idfield, generated atomically usingOperationDescriptor::generateId(). This provides a stable, unique key for each operation instance. - Impact:
- Cache Key: The
OperationPipelineExecutornow uses thisidas the key for its cache (replacing the previous use ofname). - Structure Detection:
PipelineHalideOperationManagernow uses theidfor precise structural change detection in itsinitmethod, comparingoperations[i].idwithm_last_operations[i].id. This makes the pipeline update logic more robust by relying on a truly unique and stable identifier instead of potentially changing names or types.
Synchronized Operation Execution¶
- Problem: Potential race conditions or stale display updates if UI tried to refresh before the core processing was complete.
- Solution:
PhotoEngine::applyOperationsnow returns astd::future<bool>. The caller (e.g.,ImageControllerBase) is responsible for waiting on this future (.get()) before proceeding with UI updates. -
Impact: Ensures visual consistency between the UI controls and the rendered image by guaranteeing the core image processing finishes before the display updates.
-
Method Change:
ImageControllerBase::doApplyOperationsnow callsm_engine->applyOperations(...)and waits for the returned future using.get(). - Responsibility: Ensures that the display update (
DisplayManager) only occurs after the core image processing (StateImageManager) has fully finished and its state is updated. - Benefit: Guarantees visual consistency between the UI controls and the rendered image.
Hardware-Agnostic Downsampling Abstraction¶
- Problem: Displaying high-resolution images efficiently required CPU-side downsampling, which could be a bottleneck. The UI layer (
DisplayManager) previously handled this itself, tightly coupling display logic with image processing details. - Solution: The
IWorkingImageHardwareinterface (and its implementations likeWorkingImageCPU_Halide,WorkingImageGPU_Halide) now exposes a dedicateddownsample(target_width, target_height)method. This abstracts the downsampling operation behind the hardware abstraction layer. - Impact:
- Core Integration: The Core's
StateImageManagerandPhotoEnginenow expose a new methodgetDownsampledDisplayImage(width, height).StateImageManagerdelegates this call toWorkingImageContext::getDownsampled(renamed fromdownsamplefor clarity), which in turn invokes thedownsamplemethod on the activeIWorkingImageHardwareimplementation. - UI Decoupling: The
PhotoEngineserves as the primary entry point for the UI layer (ImageControllerBase) to request a display-ready, downsampled image.ImageControllerBasecallsm_engine->getDownsampledDisplayImage(...)and receives the result (asstd::unique_ptr<Common::ImageRegion>), which it then passes to theDisplayManager. This cleanly separates the Core's processing responsibilities from the UI's display management. - Performance: For GPU implementations (
WorkingImageGPU_Halide), this means the downsampling operation can be performed directly on the GPU using a specialized Halide pipeline before the smaller result buffer is transferred back to the CPU. This drastically reduces the amount of data transferred over the PCIe bus compared to transferring the full-resolution image and downsampling it on the CPU, leading to smoother UI interactions. - Quality: The CPU-side implementation (
WorkingImageCPU) was refined to useOIIO::ImageBufAlgo::resamplewith interpolate = false.
Introduction of Common Image Types¶
- Problem: Inconsistent types (
int,size_t,unsigned int, etc.) were used throughout the codebase for representing image dimensions (width, height), channel counts, pixel coordinates (x, y), and buffer sizes. This led to potential confusion, implicit narrowing conversions, casting errors, and reduced type safety. - Solution: A new header
Core::Common::ImageTypeswas introduced to define specific, semantically clear type aliases: ImageDimfor dimensions (width, height) - aliased tostd::size_tto support very large images (e.g., 64MP+).ImageChanfor channel count - aliased tostd::uint8_t(sufficient for up to 255 channels, typically 3 or 4).ImageCoordfor pixel coordinates (x, y) - aliased tostd::int32_tto allow for negative offsets in region calculations.ImageSizefor data size (element count) - aliased tostd::size_t.- Impact:
- Type Safety: Using these dedicated types makes the code more robust by preventing accidental mixing of dimensions with coordinates or channel counts.
- Code Clarity: Function signatures and member variable declarations are now more self-documenting, clearly indicating the purpose of each numeric value.
- Consistency: Across the Core library, classes like
ImageRegion,IWorkingImageHardware,StateImageManager,WorkingImageContext,OperationDescriptor, and many others have been systematically updated to use these new types for member variables, constructor parameters, method arguments, and return values. For example,WorkingImageGPU_Halide::downsamplenow explicitly expectsCommon::ImageDimfor its target dimensions.
Refinement of ImageRegion Structure¶
- Problem: The
ImageRegionstructure, being a core data type, needed to be safer, more efficient, and easier to use for common operations like accessing dimensions, validating data integrity, and interfacing with external libraries like Halide. - Solution: Several targeted improvements were made to
Core::Common::ImageRegion: - Safe Accessors: Added
constexprandnoexceptgetter methods (width(),height(),channels(),format(),x(),y()) for retrieving image properties. These are marked[[nodiscard]]and can be evaluated at compile-time if possible, providing safe and efficient access without exposing internal member names. - Robust Validation: The
isValid()method was significantly enhanced. It now includes checks for potential integer overflow during the calculation of the expected total element count (width * height * channels) and verifies that this calculated size matches the actualm_data.size(). This prevents errors caused by inconsistent or corrupted image region data. - Efficient Data Access: The
getBuffer()method was added, returningstd::span<float>(both mutable and const versions). This provides a non-owning, efficient view into the pixel data, which is ideal for passing to external libraries like Halide or for internal algorithms without copying the underlyingstd::vector<float>. - Direct Indexing: An
operator()(int y, int x, int c)was introduced for convenient direct access to individual pixel values. It includesassertstatements in Debug builds to catch out-of-bounds access early during development, while remaining fast in Release builds. - Generic Programming: C++23
ImageLikeandMutableImageLikeconcepts were defined. These allow generic algorithms to be written that can operate on any type conforming to theImageRegioninterface (havingwidth,height,channels,getBuffer, etc.), increasing flexibility and reducing code duplication. - Impact:
ImageRegionis now a more robust, safer, and developer-friendly cornerstone of the image processing pipeline, facilitating better performance and easier maintenance.
CaptureMoment::Core::Managers::StateImageManager (Centralized Management)¶
- Ownership of SourceManager: Now owns
m_source_managerexclusively, decouplingPhotoEnginefrom direct I/O concerns. - Delegated Responsibilities: Acts as a coordinator between
SourceManager,PipelineContext, andWorkerContext, preparing data and delegating execution. - Enhanced Interface: Provides methods like
loadImage,commitWorkingImageToSource, and getters for source image properties (width,height,channels).launchProcessing()no longer manually resets or updates the context; data management is fully encapsulated withinWorkingImageContext.
Independent Serialization Layer¶
The core library includes a flexible system for saving and loading the state of image operations using XMP metadata. This system is designed as an independent layer, separate from the core image processing engine (PhotoEngine), to maximize modularity and flexibility.
Components¶
-
CaptureMoment::Core::Serializer::IXmpProvider: An interface abstracting the low-level XMP packet read/write operations. This allows switching between different XMP libraries (e.g., Exiv2, Adobe XMP Toolkit) without changing dependent code. -
CaptureMoment::Core::Serializer::FileSerializerManager: The main manager coordinating the serialization process, using strategies for path determination and actual read/write operations. -
CaptureMoment::Core::Serializer::IXmpPathStrategy: Defines how the XMP file path is determined (e.g., sidecar file, embedded in image file). -
CaptureMoment::Core::Serializer::IFileSerializerReader/Writer: Interfaces for reading/writing the list ofOperationDescriptors from/to the XMP structure. -
CaptureMoment::Core::Serializer::OperationSerialization: A namespace containing utility functions (serializeParameter,deserializeParameter) for convertingstd::any(orstd::variant) parameter values withinOperationDescriptorto/from string representations suitable for storage in XMP metadata, preserving type information. This module relies on generic conversion utilities from theCaptureMoment::Core::utilsnamespace.
Benefits¶
- Flexibility: The serialization layer (
FileSerializerManager) can be managed and invoked independently by the UI layer (e.g., viaUISerializerManagerin the Qt module) without requiringPhotoEngineto hold a reference to it. - Maintainability: Changes to serialization mechanisms or strategies do not impact the core processing engine.
- Clear Responsibility:
PhotoEnginehandles image processing state and pipeline execution. A separate service handles persistence. - 🟦 SEE SERIALIZER.md.
Dynamic Binding Correction¶
- Dynamic Binding Correction: Fixed pipeline execution to correctly bind input buffers at runtime, resolving issues with static compilation.
IHalidePipelineExecutor::executeOnHalideBuffernow accepts distinctinput_bufferandoutput_bufferparameters, enabling non-destructive processing and dimension-changing operations.
Simplified PhotoEngine Architecture¶
- Reduced Coupling:
PhotoEngineconstructor now has zero parameters, with internal managers handling their own dependencies. It no longer directly ownsSourceManager. - Centralized Management:
StateImageManagernow ownsm_source_manager,m_pipeline_builder, andm_operation_factoryfor better encapsulation and clearer responsibilities.
The most significant recent evolution is the introduction of a hardware-agnostic processing layer that abstracts CPU and GPU execution behind a unified interface.¶
CaptureMoment::Core::ImageProcessing::IWorkingImageHardware (Abstract Interface)¶
This interface represents an image used as a working buffer, abstracting its hardware location (CPU RAM or GPU memory).
- Key Methods:
getFullResImage(): Creates a CPU copy of the full-resolution image data for display or saving.bindView(const ImageView&): Attaches a non-owning data view to the hardware worker for zero-copy processing.downsample(target_width, target_height): Generates a downsampled version of the image, potentially on the GPU.-
isValid(): Checks if the buffer is valid. -
WorkingImageGPU_Halide: Concrete implementation inheriting fromWorkingImageGPUandWorkingImageHalide. Combines GPU-specific logic (device transfers), raw data management (WorkingImageData), and Halide buffer logic (WorkingImageHalide). ExposesgetOriginalHalideBuffer(),getExecutionBuffer(),resetExecutionBuffer(), andsyncToHostRAM()for explicit pipeline control.
- Hierarchical Structure: Concrete implementations like
WorkingImageCPU_HalideandWorkingImageGPU_Halidenow inherit from specific base classes (WorkingImageCPU/WorkingImageGPU) which themselves inherit fromIWorkingImageHardware,WorkingImageData, andWorkingImageHalide. - Unified Buffer Initialization:
WorkingImageHalide::initializeHalidenow accepts astd::span<float>for safer and more flexible buffer view creation.
14. Utility Modules and Generic Conversion¶
-
CaptureMoment::Core::Utils::ToStringConverter: A utility class using templates andstd::formatto provide a generictoStringfunction for various types (e.g., enums likeCoreError, floats, doubles, ints). This replaces multiple hand-written serialization functions within the core, promoting code reuse and consistency. -
CaptureMoment::Core::utils::toString: A generic template function using C++20 Concepts (ToStringablePrimitive) for converting primitive types (int, float, double, bool) and strings to their string representation. -
Usage: Replaces legacy specific functions like
serializeFloat,serializeDouble, etc., within the serialization module and other parts of the core requiring type-to-string conversion.
15. Halide Pipeline Architecture: Chunky-First Paradigm¶
- Problem: Traditional Halide pipelines assume a Planar memory layout (Stride X=1), which requires expensive Chunky↔Planar conversions when interfacing with display APIs and OIIO-decoded images.
- Solution: Enforce a strict Interleaved (Chunky) memory layout [R0,G0,B0,A0, R1,G1,B1,A1...] from decode to display, eliminating conversion overhead.
- 🟦 SEE IMAGE PIPELINE.md.md.