Load source images in the background with a progress window
Decoding a batch of photographs took seconds on the main thread, so the window froze with no sign of what was happening. loadImages now takes an optional LoadProgress (per-file callback plus a cancel flag) and hands files out from a shared cursor, so one large file cannot stall the queue behind it. The GUI decodes off the main thread and shows a progress window with the count, the current file and a Cancel button; it only appears if the batch is still running after 250 ms. Headless mode keeps a "reading n/total" line on stderr when it is attached to a terminal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+36
-11
@@ -2,14 +2,14 @@
|
||||
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace effector {
|
||||
namespace {
|
||||
@@ -108,30 +108,55 @@ void Image::save(const std::string& path) const {
|
||||
}
|
||||
|
||||
std::vector<Image> loadImages(const std::vector<std::string>& paths,
|
||||
std::vector<LoadFailure>& failures) {
|
||||
std::vector<LoadFailure>& failures,
|
||||
const LoadProgress* progress) {
|
||||
// Decoding is the dominant cost of a headless run, and the files are
|
||||
// independent, so they are decoded in parallel. Slots keep the result in
|
||||
// the order the paths were given.
|
||||
// independent, so they are decoded in parallel. Threads take the next path
|
||||
// from a shared cursor rather than a fixed slice, so one huge file cannot
|
||||
// stall the files behind it and progress arrives at a steady rate. Slots
|
||||
// keep the result in the order the paths were given.
|
||||
std::vector<Image> slots(paths.size());
|
||||
std::vector<std::string> errors(paths.size());
|
||||
std::atomic<std::size_t> cursor{0};
|
||||
std::atomic<std::size_t> done{0};
|
||||
|
||||
parallelFor(paths.size(), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
unsigned hardware = std::thread::hardware_concurrency();
|
||||
if (hardware == 0) hardware = 1;
|
||||
const std::size_t workers = std::min<std::size_t>(hardware, std::max<std::size_t>(paths.size(), 1));
|
||||
|
||||
const auto decode = [&] {
|
||||
for (;;) {
|
||||
if (progress != nullptr && progress->cancel != nullptr &&
|
||||
progress->cancel->load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
const std::size_t i = cursor.fetch_add(1, std::memory_order_relaxed);
|
||||
if (i >= paths.size()) return;
|
||||
try {
|
||||
slots[i] = Image::load(paths[i]);
|
||||
} catch (const std::exception& error) {
|
||||
errors[i] = error.what();
|
||||
}
|
||||
if (progress != nullptr && progress->onFile) {
|
||||
progress->onFile(done.fetch_add(1, std::memory_order_relaxed) + 1, paths.size(),
|
||||
paths[i]);
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(workers > 0 ? workers - 1 : 0);
|
||||
for (std::size_t w = 1; w < workers; ++w) threads.emplace_back(decode);
|
||||
decode();
|
||||
for (auto& thread : threads) thread.join();
|
||||
|
||||
std::vector<Image> images;
|
||||
images.reserve(paths.size());
|
||||
for (std::size_t i = 0; i < paths.size(); ++i) {
|
||||
if (errors[i].empty()) {
|
||||
images.push_back(std::move(slots[i]));
|
||||
} else {
|
||||
if (!errors[i].empty()) {
|
||||
failures.push_back({paths[i], errors[i]});
|
||||
} else if (!slots[i].empty()) {
|
||||
images.push_back(std::move(slots[i]));
|
||||
}
|
||||
}
|
||||
return images;
|
||||
|
||||
Reference in New Issue
Block a user