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:
hans
2026-09-18 14:39:07 +02:00
co-authored by Claude Opus 5
parent 925a719da0
commit 7e98357070
7 changed files with 317 additions and 15 deletions
+36 -11
View File
@@ -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;