CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
state_image_manager.cpp
Go to the documentation of this file.
1
9
15
16#include <spdlog/spdlog.h>
17#include <utility>
18
20
22 : m_pipeline_context(std::make_unique<Pipeline::PipelineContext>())
23 , m_worker_context(std::make_unique<Workers::WorkerContext>())
24 , m_working_image_context(std::make_unique<ImageProcessing::WorkingImageContext>())
25 , m_source_manager(std::make_unique<Managers::SourceManager>())
26{
27 if (!m_source_manager) {
28 spdlog::critical("[StateImageManager::StateImageManager]: Null dependency provided during construction.");
29 throw std::invalid_argument("[StateImageManager::StateImageManager]: Null dependency provided.");
30 }
31
32 // Start the worker thread ONLY AFTER all members are constructed and validation passes.
33 m_worker_thread = std::thread(&StateImageManager::workerLoop, this);
34}
35
37{
38 // Signal the worker thread to stop and wake it up safely (prevents lost wakeup)
39 {
40 std::lock_guard<std::mutex> lock(m_work_mutex);
41 m_stop_requested.store(true, std::memory_order_release);
42 }
43 m_work_cv.notify_one();
44
45 // Wait for the thread to finish its current task and exit
46 if (m_worker_thread.joinable()) {
47 m_worker_thread.join();
48 }
49
50 spdlog::debug("[StateImageManager::~StateImageManager]: Destroyed.");
51}
52
53// ============================================================
54// State Management
55// ============================================================
56
57bool StateImageManager::loadImage(std::string_view path)
58{
59 // 1. Wait for any pending processing to complete
60 waitForPendingProcessing();
61
62 // 2. Load the file into the INTERNAL SourceManager
63 auto load_result { m_source_manager->loadFile(path) };
64
65 if (!load_result) {
66 spdlog::error("[StateImageManager::loadImage]: Failed to load file '{}': {}", path, static_cast<int>(load_result.error()));
67 return false;
68 }
69
70 const Common::ImageDim w { m_source_manager->width() };
71 const Common::ImageDim h { m_source_manager->height() };
72 auto tile { m_source_manager->getTile(0, 0, w, h) };
73
74 if (!tile) {
75 spdlog::error("[StateImageManager::loadImage]: Failed to get tile for loaded image '{}': {}", path, static_cast<int>(tile.error()));
76 return false;
77 }
78
79 if (!m_working_image_context->prepare(std::move(tile.value()))) {
80 spdlog::error("[StateImageManager::loadImage]: Failed to prepare working image context for '{}'.", path);
81 return false;
82 }
83
84 // 3. Update State Metadata
85 {
86 std::lock_guard lock(m_state_mutex);
87 m_original_image_path = std::string(path);
88 }
89
90 // 4. Clear any pending operations from previous image
91 {
92 std::lock_guard lock(m_work_mutex);
93 m_pending_work.reset();
94 }
95
96 spdlog::info("[StateImageManager::loadImage]: Image '{}' loaded successfully ({}x{}).",
97 path, m_source_manager->width(), m_source_manager->height());
98
99 return true;
100}
101
102
103void StateImageManager::workerLoop()
104{
105 spdlog::trace("[StateImageManager::workerLoop]: Thread started.");
106
107
108 while (!m_stop_requested.load(std::memory_order_acquire))
109 {
110 std::optional<std::vector<Operations::OperationDescriptor>> work_to_do;
111 std::shared_ptr<std::promise<bool>> work_promise;
112
113 // --------------------------------------------------------
114 // PHASE 1: Wait for and acquire work
115 // --------------------------------------------------------
116 {
117 std::unique_lock<std::mutex> lock(m_work_mutex);
118
119 // Wait until: stop requested OR work available
120 m_work_cv.wait(lock, [this] {
121 return m_stop_requested.load(std::memory_order_acquire)
122 || m_pending_work.has_value();
123 });
124
125 if (m_stop_requested.load(std::memory_order_acquire))
126 {
127 // Resolve any pending promise before exit
128 if (m_active_promise) {
129 try { m_active_promise->set_value(false); }
130 catch (const std::future_error&) {}
131 }
132
133 // CRITICAL: Clear pending state and signal idle to prevent waiters from hanging
134 m_pending_work.reset();
135 m_is_idle.store(true, std::memory_order_release);
136 m_idle_cv.notify_all();
137
138 break;
139 }
140
141 // Acquire work (move out of pending)
142 work_to_do = std::move(m_pending_work);
143 m_pending_work.reset();
144 work_promise = m_active_promise;
145 }
146
147 // --------------------------------------------------------
148 // PHASE 2: Execute processing
149 // --------------------------------------------------------
150 const bool success { executeProcessing(std::move(*work_to_do)) };
151
152 // --------------------------------------------------------
153 // PHASE 3: Check for coalesced work
154 // --------------------------------------------------------
155 bool has_more_work { false };
156 {
157 std::lock_guard<std::mutex> lock(m_work_mutex);
158 has_more_work = m_pending_work.has_value();
159
160 if (!has_more_work)
161 {
162 // No more work - this is the final operation in the chain
163 // Set idle to true UNDER the mutex to prevent race conditions
164 m_is_idle.store(true, std::memory_order_release);
165
166 // Resolve the promise
167 if (work_promise) {
168 try { work_promise->set_value(success); }
169 catch (const std::future_error&) {}
170 }
171 }
172 // If has_more_work: keep m_is_idle = false, loop back immediately
173 }
174 }
175
176 spdlog::trace("[StateImageManager::workerLoop]: Thread exiting.");
177}
178
179bool StateImageManager::executeProcessing(std::vector<Operations::OperationDescriptor> ops)
180{
181 try
182 {
183 spdlog::trace("[StateImageManager::executeProcessing]: Executing {} operations.", ops.size());
184
185 auto& halide_manager { m_pipeline_context->getHalideManager() };
186 halide_manager.init(std::move(ops));
187
188 auto worker { m_worker_context->getHalideOperationWorker() };
189
190 std::shared_ptr<ImageProcessing::IWorkingImageHardware> working_image;
191 {
192 std::lock_guard<std::mutex> lock(m_state_mutex);
193 working_image = m_working_image_context->getWorkingImage();
194 }
195
196 if (!working_image) {
197 spdlog::error("[StateImageManager::executeProcessing]: No working image available.");
198 return false;
199 }
200
201 auto future { worker.execute(*m_pipeline_context, *working_image) };
202 return future.get();
203 }
204 catch (const std::exception& e)
205 {
206 spdlog::error("[StateImageManager::executeProcessing]: Exception during processing: {}", e.what());
207 return false;
208 }
209}
210
211std::expected<void, ErrorHandling::CoreError> StateImageManager::commitWorkingImageToSource()
212{
213 // Ensure no processing is running before committing
214 waitForPendingProcessing();
215
216 std::shared_ptr<ImageProcessing::IWorkingImageHardware> working_image_hw;
217 {
218 std::lock_guard lock(m_state_mutex);
219 working_image_hw = m_working_image_context->getWorkingImage();
220 }
221
222 if (!working_image_hw) {
223 spdlog::error("[StateImageManager::commitWorkingImageToSource]: No working image available.");
224 return std::unexpected(ErrorHandling::CoreError::InvalidWorkingImage);
225 }
226
227 auto cpu_copy_result { working_image_hw->getFullResImage() };
228 if (!cpu_copy_result) {
229 spdlog::error("[StateImageManager::commitWorkingImageToSource]: CPU export failed: {}",
230 ErrorHandling::to_string(cpu_copy_result.error()));
231 return std::unexpected(cpu_copy_result.error());
232 }
233
234 std::unique_ptr<Common::ImageRegion> cpu_copy { std::move(cpu_copy_result.value()) };
235
236 if (!m_source_manager->setTile(*cpu_copy)) {
237 spdlog::error("[StateImageManager::commitWorkingImageToSource]: Write to source failed.");
238 return std::unexpected(ErrorHandling::CoreError::IOError);
239 }
240
241 spdlog::info("[StateImageManager::commitWorkingImageToSource]: Changes committed to source.");
242 return {};
243}
244
245std::expected<void, ErrorHandling::CoreError> StateImageManager::resetToOriginal()
246{
247 // 1. Trigger the async processing with an empty list of operations
248 auto future { applyOperations(std::vector<Operations::OperationDescriptor>{}) };
249
250 // 2. Block and wait for the operation to complete
251 // This allows the caller (PhotoEngine) to assume the image is ready when this returns.
252 bool success { future.get() };
253
254 // 3. Convert boolean result to std::expected
255 if (!success) {
256 spdlog::error("[StateImageManager::resetToOriginal]: Processing failed.");
257 return std::unexpected(ErrorHandling::CoreError::AllocationFailed);
258 }
259
260 return {};
261}
262
264{
265 return m_source_manager->getImageSourcePath();
266}
267
268std::future<bool> StateImageManager::applyOperations(std::vector<Operations::OperationDescriptor>&& ops)
269{
270 spdlog::trace("[StateImageManager::applyOperations]: Received {} ops.", ops.size());
271
272 auto promise { std::make_shared<std::promise<bool>>() };
273 auto future { promise->get_future() };
274
275 {
276 std::lock_guard<std::mutex> lock(m_work_mutex);
277
278 if (m_active_promise) {
279 try { m_active_promise->set_value(true); } // Superseded = success (not an error)
280 catch (const std::future_error&) {}
281 }
282
283 // Store new promise and work
284 m_active_promise = promise;
285 m_pending_work = std::move(ops);
286
287 // CRITICAL: Set idle to false UNDER the mutex
288 m_is_idle.store(false, std::memory_order_release);
289 }
290
291 // Wake the persistent worker thread
292 m_work_cv.notify_one();
293
294 return future;
295}
296
297void StateImageManager::waitForPendingProcessing()
298{
299 std::unique_lock<std::mutex> lock(m_idle_mutex);
300 m_idle_cv.wait(lock, [this] {
301 return m_is_idle.load(std::memory_order_acquire);
302 });
303}
304
306{
307 std::lock_guard lock(m_state_mutex);
308 return m_source_manager ? m_source_manager->width() : 0;
309}
310
312{
313 std::lock_guard lock(m_state_mutex);
314 return m_source_manager ? m_source_manager->height() : 0;
315}
316
318{
319 std::lock_guard lock(m_state_mutex);
320 return m_source_manager ? m_source_manager->channels() : 0;
321}
322
323std::expected<std::unique_ptr<Common::ImageRegion>, ErrorHandling::CoreError> StateImageManager::getWorkingImageAsRegion() const
324{
325 std::lock_guard lock(m_state_mutex);
326
327 if (!m_working_image_context) {
328 return std::unexpected(ErrorHandling::CoreError::InvalidWorkingImage);
329 }
330
331 return m_working_image_context->getWorkingImageAsRegion();
332}
333
334std::expected<std::unique_ptr<Common::ImageRegion>, ErrorHandling::CoreError>
336{
337 std::lock_guard lock(m_state_mutex);
338
339 if (!m_working_image_context) {
340 spdlog::error("[StateImageManager::getDownsampledDisplayImage]: No WorkingImageContext");
341 return std::unexpected(ErrorHandling::CoreError::InvalidWorkingImage);
342 }
343
344 return m_working_image_context->getDownsampled(target_width, target_height);
345}
346
347} // namespace CaptureMoment::Core::Managers
Concrete implementation of ISourceManager using OIIO.
bool loadImage(std::string_view path)
Loads an image file into the internal SourceManager.
Common::ImageChan getSourceChannels() const
Gets the number of channels of the original source image.
std::expected< void, ErrorHandling::CoreError > commitWorkingImageToSource()
Commits the current working image (with applied operations) back to the SourceManager.
Common::ImageDim getSourceWidth() const
Gets the width of the original source image.
Common::ImageDim getSourceHeight() const
Gets the height of the original source image.
std::expected< void, ErrorHandling::CoreError > resetToOriginal()
Resets the image to its original state (synchronous).
std::expected< std::unique_ptr< Common::ImageRegion >, ErrorHandling::CoreError > getDownsampledDisplayImage(Common::ImageDim target_width, Common::ImageDim target_height)
Gets a downsampled version of the working image for display. Uses GPU-accelerated downsampling,...
std::string getImageSourcePath() const
Gets the path of the image source.
std::future< bool > applyOperations(std::vector< Operations::OperationDescriptor > &&ops)
Applies a list of operations to the current image using move semantics.
std::expected< std::unique_ptr< Common::ImageRegion >, ErrorHandling::CoreError > getWorkingImageAsRegion() const
Exports the current working image data to CPU memory as an ImageRegion.
std::uint8_t ImageChan
Type for channel count.
Definition image_types.h:26
std::size_t ImageDim
Type for image dimensions (width, height).
Definition image_types.h:20
constexpr std::string_view to_string(CoreError code)
Converts a CoreError code to a human-readable string.
Definition core_error.h:197
CoreError
Enumeration of all error codes for CaptureMoment Core.
Definition core_error.h:36
@ InvalidWorkingImage
The working image state is invalid or corrupted.
@ AllocationFailed
Memory allocation failed.
Container for pipeline infrastructure (Builder and Managers).
Image source management class using OpenImageIO (OIIO).
Declaration of StateImageManager class.
Container for worker infrastructure (Builder and Workers).
Manages the lifecycle of a single WorkingImage instance and its associated data.
Factory for creating IWorkingImageHardware instances using a Registry Pattern.
CaptureMoment — footer