From 1a6f0b1efc2f35e92026f4cb2fc724827c6b98de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sun, 1 Aug 2021 10:05:18 +0000 Subject: [PATCH 01/83] Construct strings by literal --- src/ImageButton.cpp | 2 +- src/ImageViewer.cpp | 2 +- src/Ipc.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageButton.cpp b/src/ImageButton.cpp index 2e60913c0..899bc7074 100644 --- a/src/ImageButton.cpp +++ b/src/ImageButton.cpp @@ -144,7 +144,7 @@ void ImageButton::draw(NVGcontext *ctx) { } if (mCutoff > 0 && mCutoff < mCaption.size()) { - pieces.back() = string{"…"} + pieces.back(); + pieces.back() = "…"s + pieces.back(); } Vector2f center = Vector2f{m_pos} + Vector2f{m_size} * 0.5f; diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index f60b8c6e2..376142c3b 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -1775,7 +1775,7 @@ void ImageViewer::updateTitle() { transform(begin(channelTails), end(channelTails), begin(channelTails), Channel::tail); caption = mCurrentImage->shortName(); - caption += string{" – "} + mCurrentGroup; + caption += " – "s + mCurrentGroup; auto rel = mouse_pos() - mImageCanvas->position(); vector values = mImageCanvas->getValuesAtNanoPos({rel.x(), rel.y()}, channels); diff --git a/src/Ipc.cpp b/src/Ipc.cpp index abf94a62b..af8da5289 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -303,7 +303,7 @@ static int closeSocket(Ipc::socket_t socket) { } Ipc::Ipc(const string& hostname) { - const string lockName = string{".tev-lock."} + hostname; + const string lockName = ".tev-lock."s + hostname; auto parts = split(hostname, ":"); const string& ip = parts.front(); From d24311f473efd9929019940bf93b78b0ffc6b605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sun, 1 Aug 2021 19:27:11 +0000 Subject: [PATCH 02/83] Avoid significant copy in interpreting UpdateImage packets --- include/tev/Ipc.h | 8 ++++++++ src/Ipc.cpp | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/include/tev/Ipc.h b/include/tev/Ipc.h index 28f0127c7..63404eb2f 100644 --- a/include/tev/Ipc.h +++ b/include/tev/Ipc.h @@ -147,6 +147,14 @@ class IpcPacket { mIdx += sizeof(T); return *this; } + + size_t remainingBytes() const { + return mData.size() - mIdx; + } + + const char* get() const { + return &mData[mIdx]; + } private: const std::vector& mData; size_t mIdx = 0; diff --git a/src/Ipc.cpp b/src/Ipc.cpp index af8da5289..8f982b447 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -246,8 +246,11 @@ IpcPacketUpdateImage IpcPacket::interpretAsUpdateImage() const { stridedImageDataSize = std::max(stridedImageDataSize, (DenseIndex)(result.channelOffsets[c] + (nPixels-1) * result.channelStrides[c] + 1)); } - vector stridedImageData(stridedImageDataSize); - payload >> stridedImageData; + if (payload.remainingBytes() < stridedImageDataSize * sizeof(float)) { + throw std::runtime_error{"UpdateImage: insufficient image data."}; + } + + const float* stridedImageData = (const float*)payload.get(); gThreadPool->parallelFor(0, nPixels, [&](DenseIndex px) { for (int32_t c = 0; c < result.nChannels; ++c) { From 75698872c11888092d382f583432cf409d676b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 3 Aug 2021 08:24:34 +0000 Subject: [PATCH 03/83] Add prioritization of tasks to ThreadPool --- include/tev/Channel.h | 4 +- include/tev/Image.h | 19 ++- include/tev/ImageCanvas.h | 10 +- include/tev/Lazy.h | 4 +- include/tev/ThreadPool.h | 36 ++--- include/tev/imageio/ClipboardImageLoader.h | 2 +- include/tev/imageio/DdsImageLoader.h | 2 +- include/tev/imageio/EmptyImageLoader.h | 2 +- include/tev/imageio/ExrImageLoader.h | 2 +- include/tev/imageio/ImageLoader.h | 5 +- include/tev/imageio/PfmImageLoader.h | 2 +- include/tev/imageio/StbiImageLoader.h | 2 +- src/Channel.cpp | 8 +- src/Image.cpp | 40 +++--- src/ImageCanvas.cpp | 54 +++++--- src/ImageViewer.cpp | 2 +- src/Ipc.cpp | 3 +- src/ThreadPool.cpp | 8 +- src/imageio/ClipboardImageLoader.cpp | 8 +- src/imageio/DdsImageLoader.cpp | 6 +- src/imageio/EmptyImageLoader.cpp | 6 +- src/imageio/ExrImageLoader.cpp | 147 +++++++++++---------- src/imageio/PfmImageLoader.cpp | 8 +- src/imageio/StbiImageLoader.cpp | 10 +- 24 files changed, 217 insertions(+), 173 deletions(-) diff --git a/include/tev/Channel.h b/include/tev/Channel.h index c87c97d2a..b8c652952 100644 --- a/include/tev/Channel.h +++ b/include/tev/Channel.h @@ -69,8 +69,8 @@ class Channel { return {mData.cols(), mData.rows()}; } - void divideByAsync(const Channel& other, std::vector>& futures); - void multiplyWithAsync(const Channel& other, std::vector>& futures); + void divideByAsync(const Channel& other, std::vector>& futures, int priority); + void multiplyWithAsync(const Channel& other, std::vector>& futures, int priority); void setZero() { mData.setZero(); } diff --git a/include/tev/Image.h b/include/tev/Image.h index a306e09f5..50d0237cc 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -41,7 +41,7 @@ struct ImageTexture { class Image { public: - Image(const filesystem::path& path, std::istream& iStream, const std::string& channelSelector); + Image(int id, const filesystem::path& path, std::istream& iStream, const std::string& channelSelector); virtual ~Image(); const filesystem::path& path() const { @@ -97,6 +97,10 @@ class Image { mId = sId++; } + static int drawId() { + return sId++; + } + void updateChannel(const std::string& channelName, int x, int y, int width, int height, const std::vector& data); std::string toString() const; @@ -139,7 +143,9 @@ class Image { int mId; }; +std::shared_ptr tryLoadImage(int imageId, filesystem::path path, std::istream& iStream, std::string channelSelector); std::shared_ptr tryLoadImage(filesystem::path path, std::istream& iStream, std::string channelSelector); +std::shared_ptr tryLoadImage(int imageId, filesystem::path path, std::string channelSelector); std::shared_ptr tryLoadImage(filesystem::path path, std::string channelSelector); struct ImageAddition { @@ -152,10 +158,15 @@ class BackgroundImagesLoader { void enqueue(const filesystem::path& path, const std::string& channelSelector, bool shallSelect); ImageAddition tryPop() { return mLoadedImages.tryPop(); } + void wait() { + return mWorkers.waitUntilFinished(); + } + private: - // A single worker is enough, since parallelization will happen _within_ each image load. - // We want to focus all resources to load images in order as fast as possible, rather than - // our of order. + // A separate threadpool (other than the global threadpool) is + // required to prevent deadlocking: if more images are loaded + // simultaneously than threads are available, they will starve the + // global thread pool of workers. ThreadPool mWorkers{1}; SharedQueue mLoadedImages; }; diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index b837a9a01..38e69c13f 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -117,8 +117,8 @@ class ImageCanvas : public nanogui::Canvas { return mClipToLdr; } - std::vector getHdrImageData(bool divideAlpha) const; - std::vector getLdrImageData(bool divideAlpha) const; + std::vector getHdrImageData(bool divideAlpha, int priority) const; + std::vector getLdrImageData(bool divideAlpha, int priority) const; void saveImage(const filesystem::path& filename) const; @@ -139,14 +139,16 @@ class ImageCanvas : public nanogui::Canvas { std::shared_ptr image, std::shared_ptr reference, const std::string& requestedChannelGroup, - EMetric metric + EMetric metric, + int priority ); static std::shared_ptr computeCanvasStatistics( std::shared_ptr image, std::shared_ptr reference, const std::string& requestedChannelGroup, - EMetric metric + EMetric metric, + int priority ); Eigen::Vector2f pixelOffset(const Eigen::Vector2i& size) const; diff --git a/include/tev/Lazy.h b/include/tev/Lazy.h index aac52e6a4..6b54be8f1 100644 --- a/include/tev/Lazy.h +++ b/include/tev/Lazy.h @@ -67,7 +67,7 @@ class Lazy { } } - void computeAsync() { + void computeAsync(int priority) { // No need to perform an async computation if we // already computed the value before or if one is // already running. @@ -80,7 +80,7 @@ class Lazy { T result = compute(); mBecameReadyAt = std::chrono::steady_clock::now(); return result; - }, true); + }, priority); } else { mAsyncValue = std::async(std::launch::async, [this]() { T result = compute(); diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 6b11dbb05..fd7168beb 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -6,9 +6,9 @@ #include #include -#include #include #include +#include #include #include @@ -28,7 +28,7 @@ class ThreadPool { virtual ~ThreadPool(); template - auto enqueueTask(F&& f, bool highPriority = false) -> std::future> { + auto enqueueTask(F&& f, int priority) -> std::future> { using return_type = std::invoke_result_t; ++mNumTasksInSystem; @@ -39,12 +39,7 @@ class ThreadPool { { std::lock_guard lock{mTaskQueueMutex}; - - if (highPriority) { - mTaskQueue.emplace_front([task]() { (*task)(); }); - } else { - mTaskQueue.emplace_back([task]() { (*task)(); }); - } + mTaskQueue.push({priority, [task]() { (*task)(); }}); } mWorkerCondition.notify_one(); @@ -63,7 +58,7 @@ class ThreadPool { void flushQueue(); template - void parallelForAsync(Int start, Int end, F body, std::vector>& futures) { + void parallelForAsync(Int start, Int end, F body, std::vector>& futures, int priority) { Int localNumThreads = (Int)mNumThreads; Int range = end - start; @@ -76,27 +71,38 @@ class ThreadPool { for (Int j = innerStart; j < innerEnd; ++j) { body(j); } - })); + }, priority)); } } template - std::vector> parallelForAsync(Int start, Int end, F body) { + std::vector> parallelForAsync(Int start, Int end, F body, int priority) { std::vector> futures; - parallelForAsync(start, end, body, futures); + parallelForAsync(start, end, body, futures, priority); return futures; } template - void parallelFor(Int start, Int end, F body) { - waitAll(parallelForAsync(start, end, body)); + void parallelFor(Int start, Int end, F body, int priority) { + waitAll(parallelForAsync(start, end, body, priority)); } private: size_t mNumThreads = 0; std::vector mThreads; - std::deque> mTaskQueue; + struct Task { + int priority; + std::function fun; + + struct Comparator { + bool operator()(const Task& a, const Task& b) { + return a.priority < b.priority; + } + }; + }; + + std::priority_queue, Task::Comparator> mTaskQueue; std::mutex mTaskQueueMutex; std::condition_variable mWorkerCondition; diff --git a/include/tev/imageio/ClipboardImageLoader.h b/include/tev/imageio/ClipboardImageLoader.h index 9f31a6b69..cfca614a5 100644 --- a/include/tev/imageio/ClipboardImageLoader.h +++ b/include/tev/imageio/ClipboardImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ClipboardImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const override; + std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "clipboard"; diff --git a/include/tev/imageio/DdsImageLoader.h b/include/tev/imageio/DdsImageLoader.h index e20a17e3a..7174e7e25 100644 --- a/include/tev/imageio/DdsImageLoader.h +++ b/include/tev/imageio/DdsImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class DdsImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const override; + std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "DDS"; diff --git a/include/tev/imageio/EmptyImageLoader.h b/include/tev/imageio/EmptyImageLoader.h index d058ab8de..44c8f321d 100644 --- a/include/tev/imageio/EmptyImageLoader.h +++ b/include/tev/imageio/EmptyImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class EmptyImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const override; + std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "IPC"; diff --git a/include/tev/imageio/ExrImageLoader.h b/include/tev/imageio/ExrImageLoader.h index 714dad93b..f95f1ae24 100644 --- a/include/tev/imageio/ExrImageLoader.h +++ b/include/tev/imageio/ExrImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ExrImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const override; + std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "OpenEXR"; diff --git a/include/tev/imageio/ImageLoader.h b/include/tev/imageio/ImageLoader.h index 2475d65e1..3aa8b0078 100644 --- a/include/tev/imageio/ImageLoader.h +++ b/include/tev/imageio/ImageLoader.h @@ -9,6 +9,7 @@ #include #include +#include #include TEV_NAMESPACE_BEGIN @@ -18,7 +19,9 @@ class ImageLoader { virtual ~ImageLoader() {} virtual bool canLoadFile(std::istream& iStream) const = 0; - virtual ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const = 0; + + // Return loaded image data as well as whether that data has the alpha channel pre-multiplied or not. + virtual std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; virtual std::string name() const = 0; diff --git a/include/tev/imageio/PfmImageLoader.h b/include/tev/imageio/PfmImageLoader.h index c4c5923eb..c9fe434a2 100644 --- a/include/tev/imageio/PfmImageLoader.h +++ b/include/tev/imageio/PfmImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class PfmImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const override; + std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "PFM"; diff --git a/include/tev/imageio/StbiImageLoader.h b/include/tev/imageio/StbiImageLoader.h index 2ec928661..cae283208 100644 --- a/include/tev/imageio/StbiImageLoader.h +++ b/include/tev/imageio/StbiImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class StbiImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - ImageData load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, bool& hasPremultipliedAlpha) const override; + std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "STBI"; diff --git a/src/Channel.cpp b/src/Channel.cpp index 414e74046..f2a95d681 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -52,20 +52,20 @@ Color Channel::color(string channel) { return Color(1.0f, 1.0f); } -void Channel::divideByAsync(const Channel& other, vector>& futures) { +void Channel::divideByAsync(const Channel& other, vector>& futures, int priority) { gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { if (other.at(i) != 0) { at(i) /= other.at(i); } else { at(i) = 0; } - }, futures); + }, futures, priority); } -void Channel::multiplyWithAsync(const Channel& other, vector>& futures) { +void Channel::multiplyWithAsync(const Channel& other, vector>& futures, int priority) { gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { at(i) *= other.at(i); - }, futures); + }, futures, priority); } void Channel::updateTile(int x, int y, int width, int height, const vector& newData) { diff --git a/src/Image.cpp b/src/Image.cpp index b3a447b1c..779a13ab4 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -21,8 +21,8 @@ TEV_NAMESPACE_BEGIN atomic Image::sId(0); -Image::Image(const class path& path, istream& iStream, const string& channelSelector) -: mPath{path}, mChannelSelector{channelSelector}, mId{sId++} { +Image::Image(int id, const class path& path, istream& iStream, const string& channelSelector) +: mPath{path}, mChannelSelector{channelSelector}, mId{id} { mName = channelSelector.empty() ? path.str() : tfm::format("%s:%s", path, channelSelector); auto start = chrono::system_clock::now(); @@ -43,8 +43,8 @@ Image::Image(const class path& path, istream& iStream, const string& channelSele if (useLoader) { loadMethod = imageLoader->name(); - bool hasPremultipliedAlpha = false; - mData = imageLoader->load(iStream, mPath, mChannelSelector, hasPremultipliedAlpha); + bool hasPremultipliedAlpha; + std::tie(mData, hasPremultipliedAlpha) = imageLoader->load(iStream, mPath, mChannelSelector, -mId); ensureValid(); // We assume an internal pre-multiplied-alpha representation @@ -141,12 +141,12 @@ nanogui::Texture* Image::texture(const vector& channelNames) { const auto& channelData = chan->data(); gThreadPool->parallelForAsync(0, numPixels, [&channelData, &data, i](DenseIndex j) { data[j * 4 + i] = channelData(j); - }, futures); + }, futures, std::numeric_limits::max()); } else { float val = i == 3 ? 1 : 0; gThreadPool->parallelForAsync(0, numPixels, [&data, val, i](DenseIndex j) { data[j * 4 + i] = val; - }, futures); + }, futures, std::numeric_limits::max()); } } waitAll(futures); @@ -429,7 +429,7 @@ void Image::alphaOperation(const function& func) void Image::multiplyAlpha() { vector> futures; alphaOperation([&] (Channel& target, const Channel& alpha) { - target.multiplyWithAsync(alpha, futures); + target.multiplyWithAsync(alpha, futures, -mId); }); waitAll(futures); } @@ -437,7 +437,7 @@ void Image::multiplyAlpha() { void Image::unmultiplyAlpha() { vector> futures; alphaOperation([&] (Channel& target, const Channel& alpha) { - target.divideByAsync(alpha, futures); + target.divideByAsync(alpha, futures, -mId); }); waitAll(futures); } @@ -461,7 +461,7 @@ void Image::ensureValid() { } } -shared_ptr tryLoadImage(path path, istream& iStream, string channelSelector) { +shared_ptr tryLoadImage(int imageId, path path, istream& iStream, string channelSelector) { auto handleException = [&](const exception& e) { if (channelSelector.empty()) { tlog::error() << tfm::format("Could not load '%s'. %s", path, e.what()); @@ -471,7 +471,7 @@ shared_ptr tryLoadImage(path path, istream& iStream, string channelSelect }; try { - return make_shared(path, iStream, channelSelector); + return make_shared(imageId, path, iStream, channelSelector); } catch (const invalid_argument& e) { handleException(e); } catch (const runtime_error& e) { @@ -483,7 +483,11 @@ shared_ptr tryLoadImage(path path, istream& iStream, string channelSelect return nullptr; } -shared_ptr tryLoadImage(path path, string channelSelector) { +shared_ptr tryLoadImage(path path, istream& iStream, string channelSelector) { + return tryLoadImage(Image::drawId(), path, iStream, channelSelector); +} + +shared_ptr tryLoadImage(int imageId, path path, string channelSelector) { try { path = path.make_absolute(); } catch (const runtime_error& e) { @@ -492,18 +496,24 @@ shared_ptr tryLoadImage(path path, string channelSelector) { } ifstream fileStream{nativeString(path), ios_base::binary}; - return tryLoadImage(path, fileStream, channelSelector); + return tryLoadImage(imageId, path, fileStream, channelSelector); +} + +shared_ptr tryLoadImage(path path, string channelSelector) { + return tryLoadImage(Image::drawId(), path, channelSelector); } void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { - mWorkers.enqueueTask([path, channelSelector, shallSelect, this] { - auto image = tryLoadImage(path, channelSelector); + int imageId = Image::drawId(); + + mWorkers.enqueueTask([imageId, path, channelSelector, shallSelect, this] { + auto image = tryLoadImage(imageId, path, channelSelector); if (image) { mLoadedImages.push({ shallSelect, image }); } glfwPostEmptyEvent(); - }); + }, -imageId); } TEV_NAMESPACE_END diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 327c81417..a1a395c2b 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -313,14 +313,14 @@ void ImageCanvas::resetTransform() { mTransform = Affine2f::Identity(); } -std::vector ImageCanvas::getHdrImageData(bool divideAlpha) const { +std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) const { std::vector result; if (!mImage) { return result; } - const auto& channels = channelsFromImages(mImage, mReference, mRequestedChannelGroup, mMetric); + const auto& channels = channelsFromImages(mImage, mReference, mRequestedChannelGroup, mMetric, priority); auto numPixels = mImage->count(); if (channels.empty()) { @@ -337,7 +337,7 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha) const { for (DenseIndex j = 0; j < channelData.size(); ++j) { result[j * 4 + i] = channelData(j); } - }); + }, priority); // Manually set alpha channel to 1 if the image does not have one. if (nChannelsToSave < 4) { @@ -357,13 +357,13 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha) const { result[j * 4 + i] /= alpha; } } - }); + }, priority); } return result; } -std::vector ImageCanvas::getLdrImageData(bool divideAlpha) const { +std::vector ImageCanvas::getLdrImageData(bool divideAlpha, int priority) const { std::vector result; if (!mImage) { @@ -371,7 +371,7 @@ std::vector ImageCanvas::getLdrImageData(bool divideAlpha) const { } auto numPixels = mImage->count(); - auto floatData = getHdrImageData(divideAlpha); + auto floatData = getHdrImageData(divideAlpha, priority); // Store as LDR image. result.resize(floatData.size()); @@ -389,7 +389,7 @@ std::vector ImageCanvas::getLdrImageData(bool divideAlpha) const { for (int j = 0; j < 4; ++j) { result[start + j] = (char)(floatData[start + j] * 255 + 0.5f); } - }); + }, priority); return result; } @@ -420,9 +420,19 @@ void ImageCanvas::saveImage(const path& path) const { TEV_ASSERT(hdrSaver || ldrSaver, "Each image saver must either be a HDR or an LDR saver."); if (hdrSaver) { - hdrSaver->save(f, path, getHdrImageData(!saver->hasPremultipliedAlpha()), imageSize, 4); + hdrSaver->save( + f, path, + getHdrImageData(!saver->hasPremultipliedAlpha(), + std::numeric_limits::max()), + imageSize, 4 + ); } else if (ldrSaver) { - ldrSaver->save(f, path, getLdrImageData(!saver->hasPremultipliedAlpha()), imageSize, 4); + ldrSaver->save( + f, path, + getLdrImageData(!saver->hasPremultipliedAlpha(), + std::numeric_limits::max()), + imageSize, 4 + ); } auto end = chrono::system_clock::now(); @@ -450,15 +460,19 @@ shared_ptr>> ImageCanvas::canvasStatistics() { return iter->second; } + static std::atomic sId{0}; + // Later requests must have higher priority than previous ones. + int priority = ++sId; + auto image = mImage, reference = mReference; auto requestedChannelGroup = mRequestedChannelGroup; auto metric = mMetric; - mMeanValues.insert(make_pair(key, make_shared>>([image, reference, requestedChannelGroup, metric]() { - return computeCanvasStatistics(image, reference, requestedChannelGroup, metric); + mMeanValues.insert(make_pair(key, make_shared>>([image, reference, requestedChannelGroup, metric, priority]() { + return computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority); }, &mMeanValueThreadPool))); auto val = mMeanValues.at(key); - val->computeAsync(); + val->computeAsync(priority); return val; } @@ -466,7 +480,8 @@ vector ImageCanvas::channelsFromImages( shared_ptr image, shared_ptr reference, const string& requestedChannelGroup, - EMetric metric + EMetric metric, + int priority ) { if (!image) { return {}; @@ -486,7 +501,7 @@ vector ImageCanvas::channelsFromImages( for (DenseIndex j = 0; j < chan->count(); ++j) { result[i].at(j) = chan->eval(j); } - }); + }, priority); } else { Vector2i size = image->size(); Vector2i offset = (reference->size() - size) / 2; @@ -533,7 +548,7 @@ vector ImageCanvas::channelsFromImages( } } } - }); + }, priority); } return result; @@ -543,9 +558,10 @@ shared_ptr ImageCanvas::computeCanvasStatistics( std::shared_ptr image, std::shared_ptr reference, const string& requestedChannelGroup, - EMetric metric + EMetric metric, + int priority ) { - auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric); + auto flattened = channelsFromImages(image, reference, requestedChannelGroup, metric, priority); float mean = 0; float maximum = -numeric_limits::infinity(); @@ -621,7 +637,7 @@ shared_ptr ImageCanvas::computeCanvasStatistics( const auto& channel = flattened[i]; gThreadPool->parallelForAsync(0, numElements, [&, i](DenseIndex j) { indices(j, i) = valToBin(channel.eval(j)); - }, futures); + }, futures, priority); } waitAll(futures); @@ -629,7 +645,7 @@ shared_ptr ImageCanvas::computeCanvasStatistics( for (DenseIndex j = 0; j < numElements; ++j) { result->histogram(indices(j, i), i) += alphaChannel ? alphaChannel->eval(j) : 1; } - }); + }, priority); for (int i = 0; i < NUM_BINS; ++i) { result->histogram.row(i) /= binToVal(i + 1) - binToVal(i); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 376142c3b..e9f9e747c 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -723,7 +723,7 @@ bool ImageViewer::keyboard_event(int key, int scancode, int action, int modifier imageMetadata.blue_shift = 16; imageMetadata.alpha_shift = 24; - auto imageData = mImageCanvas->getLdrImageData(true); + auto imageData = mImageCanvas->getLdrImageData(true, std::numeric_limits::max()); clip::image image(imageData.data(), imageMetadata); if (clip::set_image(image)) { diff --git a/src/Ipc.cpp b/src/Ipc.cpp index 8f982b447..e65065cec 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -251,12 +251,11 @@ IpcPacketUpdateImage IpcPacket::interpretAsUpdateImage() const { } const float* stridedImageData = (const float*)payload.get(); - gThreadPool->parallelFor(0, nPixels, [&](DenseIndex px) { for (int32_t c = 0; c < result.nChannels; ++c) { result.imageData[c][px] = stridedImageData[result.channelOffsets[c] + px * result.channelStrides[c]]; } - }); + }, std::numeric_limits::max()); return result; } diff --git a/src/ThreadPool.cpp b/src/ThreadPool.cpp index 15ac0c624..3b6fb1524 100644 --- a/src/ThreadPool.cpp +++ b/src/ThreadPool.cpp @@ -42,8 +42,8 @@ void ThreadPool::startThreads(size_t num) { break; } - function task{move(mTaskQueue.front())}; - mTaskQueue.pop_front(); + function task{move(mTaskQueue.top().fun)}; + mTaskQueue.pop(); // Unlock the lock, so we can process the task without blocking other threads lock.unlock(); @@ -104,7 +104,9 @@ void ThreadPool::flushQueue() { lock_guard lock{mTaskQueueMutex}; mNumTasksInSystem -= mTaskQueue.size(); - mTaskQueue.clear(); + while (!mTaskQueue.empty()) { + mTaskQueue.pop(); + } } TEV_NAMESPACE_END diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index 18e1732ce..a7baf49d2 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -23,7 +23,7 @@ bool ClipboardImageLoader::canLoadFile(istream& iStream) const { return result; } -ImageData ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, bool& hasPremultipliedAlpha) const { +std::tuple ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; char magic[4]; @@ -97,7 +97,7 @@ ImageData ClipboardImageLoader::load(istream& iStream, const path&, const string } } } - }); + }, priority); vector> matches; for (size_t i = 0; i < channels.size(); ++i) { @@ -119,9 +119,7 @@ ImageData ClipboardImageLoader::load(istream& iStream, const path&, const string // within a topmost root layer. result.layers.emplace_back(""); - hasPremultipliedAlpha = false; - - return result; + return {result, false}; } TEV_NAMESPACE_END diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index 0c5ddf216..31c557f42 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -153,7 +153,7 @@ static int getDxgiChannelCount(DXGI_FORMAT fmt) { } } -ImageData DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, bool& hasPremultipliedAlpha) const { +std::tuple DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { // COM must be initialized on the thread executing load(). if (CoInitializeEx(nullptr, COINIT_MULTITHREADED) != S_OK) { throw invalid_argument{"Failed to initialize COM."}; @@ -265,9 +265,7 @@ ImageData DdsImageLoader::load(istream& iStream, const path&, const string& chan // within a topmost root layer. result.layers.emplace_back(""); - hasPremultipliedAlpha = scratchImage.GetMetadata().IsPMAlpha(); - - return result; + return {result, scratchImage.GetMetadata().IsPMAlpha()}; } TEV_NAMESPACE_END diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index 14eb72122..c2944c585 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -22,7 +22,7 @@ bool EmptyImageLoader::canLoadFile(istream& iStream) const { return result; } -ImageData EmptyImageLoader::load(istream& iStream, const path&, const string&, bool& hasPremultipliedAlpha) const { +std::tuple EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { ImageData result; string magic; @@ -61,9 +61,7 @@ ImageData EmptyImageLoader::load(istream& iStream, const path&, const string&, b result.layers.emplace_back(layer); } - hasPremultipliedAlpha = true; - - return result; + return {result, true}; } TEV_NAMESPACE_END diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index ac8ea03cd..ee0c80b0b 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -88,74 +88,7 @@ bool ExrImageLoader::canLoadFile(istream& iStream) const { return result; } -// Helper class for dealing with the raw channels loaded from an exr file. -class RawChannel { -public: - RawChannel(string name, Imf::Channel imfChannel) - : mName(name), mImfChannel(imfChannel) { - } - - void resize(size_t size) { - mData.resize(size * bytesPerPixel()); - } - - void registerWith(Imf::FrameBuffer& frameBuffer, const Imath::Box2i& dw) { - int width = dw.max.x - dw.min.x + 1; - frameBuffer.insert(mName.c_str(), Imf::Slice( - mImfChannel.type, - mData.data() - (dw.min.x + dw.min.y * width) * bytesPerPixel(), - bytesPerPixel(), bytesPerPixel() * (width/mImfChannel.xSampling), - mImfChannel.xSampling, mImfChannel.ySampling, 0 - )); - } - - template - void copyToTyped(Channel& channel, vector>& futures) const { - int width = channel.size().x(); - int widthSubsampled = width/mImfChannel.ySampling; - - auto data = reinterpret_cast(mData.data()); - gThreadPool->parallelForAsync(0, channel.size().y(), [this, &channel, width, widthSubsampled, data](int y) { - for (int x = 0; x < width; ++x) { - channel.at({x, y}) = data[x/mImfChannel.xSampling + (y/mImfChannel.ySampling) * widthSubsampled]; - } - }, futures); - } - - void copyTo(Channel& channel, vector>& futures) const { - switch (mImfChannel.type) { - case Imf::HALF: - copyToTyped<::half>(channel, futures); break; - case Imf::FLOAT: - copyToTyped(channel, futures); break; - case Imf::UINT: - copyToTyped(channel, futures); break; - default: - throw runtime_error("Invalid pixel type encountered."); - } - } - - const string& name() const { - return mName; - } - -private: - int bytesPerPixel() const { - switch (mImfChannel.type) { - case Imf::HALF: return sizeof(::half); - case Imf::FLOAT: return sizeof(float); - case Imf::UINT: return sizeof(uint32_t); - default: - throw runtime_error("Invalid pixel type encountered."); - } - } - - string mName; - Imf::Channel mImfChannel; - vector mData; -}; - -ImageData ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, bool& hasPremultipliedAlpha) const { +std::tuple ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { ImageData result; StdIStream stdIStream{iStream, path.str().c_str()}; @@ -190,6 +123,78 @@ ImageData ExrImageLoader::load(istream& iStream, const path& path, const string& throw invalid_argument{"EXR image has zero pixels."}; } + // Inline helper class for dealing with the raw channels loaded from an exr file. + class RawChannel { + public: + RawChannel(string name, Imf::Channel imfChannel) + : mName(name), mImfChannel(imfChannel) { + } + + void resize(size_t size) { + mData.resize(size * bytesPerPixel()); + } + + void registerWith(Imf::FrameBuffer& frameBuffer, const Imath::Box2i& dw) { + int width = dw.max.x - dw.min.x + 1; + frameBuffer.insert(mName.c_str(), Imf::Slice( + mImfChannel.type, + mData.data() - (dw.min.x + dw.min.y * width) * bytesPerPixel(), + bytesPerPixel(), bytesPerPixel() * width, + mImfChannel.xSampling, mImfChannel.ySampling, 0 + )); + } + + void copyTo(Channel& channel, vector>& futures, int priority) const { + switch (mImfChannel.type) { + case Imf::HALF: { + auto data = reinterpret_cast(mData.data()); + gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { + channel.at(i) = data[i]; + }, futures, priority); + break; + } + + case Imf::FLOAT: { + auto data = reinterpret_cast(mData.data()); + gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { + channel.at(i) = data[i]; + }, futures, priority); + break; + } + + case Imf::UINT: { + auto data = reinterpret_cast(mData.data()); + gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { + channel.at(i) = data[i]; + }, futures, priority); + break; + } + + default: + throw runtime_error("Invalid pixel type encountered."); + } + } + + const string& name() const { + return mName; + } + + private: + int bytesPerPixel() const { + switch (mImfChannel.type) { + case Imf::HALF: return sizeof(::half); + case Imf::FLOAT: return sizeof(float); + case Imf::UINT: return sizeof(uint32_t); + default: + throw runtime_error("Invalid pixel type encountered."); + } + } + + string mName; + Imf::Channel mImfChannel; + vector mData; + }; + vector rawChannels; Imf::FrameBuffer frameBuffer; @@ -226,7 +231,7 @@ ImageData ExrImageLoader::load(istream& iStream, const path& path, const string& gThreadPool->parallelFor(0, (int)rawChannels.size(), [&](int i) { rawChannels[i].resize((DenseIndex)size.x() * size.y()); - }); + }, priority); for (size_t i = 0; i < rawChannels.size(); ++i) { rawChannels[i].registerWith(frameBuffer, dw); @@ -241,7 +246,7 @@ ImageData ExrImageLoader::load(istream& iStream, const path& path, const string& vector> futures; for (size_t i = 0; i < rawChannels.size(); ++i) { - rawChannels[i].copyTo(result.channels[i], futures); + rawChannels[i].copyTo(result.channels[i], futures, priority); } waitAll(futures); @@ -272,7 +277,7 @@ ImageData ExrImageLoader::load(istream& iStream, const path& path, const string& } } - return result; + return {result, true}; } TEV_NAMESPACE_END diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index 736d0ace1..d9973ebc1 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -21,7 +21,7 @@ bool PfmImageLoader::canLoadFile(istream& iStream) const { return result; } -ImageData PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, bool& hasPremultipliedAlpha) const { +std::tuple PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; string magic; @@ -88,7 +88,7 @@ ImageData PfmImageLoader::load(istream& iStream, const path&, const string& chan channels[c].at({x, size.y() - y - 1}) = scale * val; } } - }); + }, priority); vector> matches; for (size_t i = 0; i < channels.size(); ++i) { @@ -110,9 +110,7 @@ ImageData PfmImageLoader::load(istream& iStream, const path&, const string& chan // within a topmost root layer. result.layers.emplace_back(""); - hasPremultipliedAlpha = false; - - return result; + return {result, false}; } TEV_NAMESPACE_END diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index d02ac0e3a..ced8214c9 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -18,7 +18,7 @@ bool StbiImageLoader::canLoadFile(istream&) const { return true; } -ImageData StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, bool& hasPremultipliedAlpha) const { +std::tuple StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; static const stbi_io_callbacks callbacks = { @@ -72,7 +72,7 @@ ImageData StbiImageLoader::load(istream& iStream, const path&, const string& cha for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; } - }); + }, priority); } else { auto typedData = reinterpret_cast(data); gThreadPool->parallelFor(0, numPixels, [&](DenseIndex i) { @@ -84,7 +84,7 @@ ImageData StbiImageLoader::load(istream& iStream, const path&, const string& cha channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); } } - }); + }, priority); } vector> matches; @@ -107,9 +107,7 @@ ImageData StbiImageLoader::load(istream& iStream, const path&, const string& cha // within a topmost root layer. result.layers.emplace_back(""); - hasPremultipliedAlpha = false; - - return result; + return {result, false}; } TEV_NAMESPACE_END From d809175837df6939d8b0020b2f40a63a4e9b5a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 3 Aug 2021 08:24:50 +0000 Subject: [PATCH 04/83] Make ThreadPool::parallelFor slightly smarter --- include/tev/ThreadPool.h | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index fd7168beb..d90882695 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -59,16 +59,15 @@ class ThreadPool { template void parallelForAsync(Int start, Int end, F body, std::vector>& futures, int priority) { - Int localNumThreads = (Int)mNumThreads; - Int range = end - start; - Int chunk = (range / localNumThreads) + 1; - - for (Int i = 0; i < localNumThreads; ++i) { - futures.emplace_back(enqueueTask([i, chunk, start, end, body] { - Int innerStart = start + i * chunk; - Int innerEnd = std::min(end, start + (i + 1) * chunk); - for (Int j = innerStart; j < innerEnd; ++j) { + Int nTasks = std::min((Int)mNumThreads, range); + + for (Int i = 0; i < nTasks; ++i) { + Int taskStart = start + (range * i / nTasks); + Int taskEnd = start + (range * (i+1) / nTasks); + TEV_ASSERT(taskStart != taskEnd, "Shouldn't not produce tasks with empty range."); + futures.emplace_back(enqueueTask([taskStart, taskEnd, body] { + for (Int j = taskStart; j < taskEnd; ++j) { body(j); } }, priority)); From f1da55954a55ba2a010899837bf921f6174e5a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 3 Aug 2021 16:40:34 +0000 Subject: [PATCH 05/83] ThreadPool::parallelFor return a single std::future instead of many --- include/tev/Channel.h | 4 ++-- include/tev/Common.h | 16 ++++++++++++++-- include/tev/ThreadPool.h | 26 ++++++++++++++------------ src/Channel.cpp | 12 ++++++------ src/Image.cpp | 22 ++++++++++++++-------- src/ImageCanvas.cpp | 8 +++++--- src/imageio/ExrImageLoader.cpp | 19 ++++++++----------- 7 files changed, 63 insertions(+), 44 deletions(-) diff --git a/include/tev/Channel.h b/include/tev/Channel.h index b8c652952..468073a01 100644 --- a/include/tev/Channel.h +++ b/include/tev/Channel.h @@ -69,8 +69,8 @@ class Channel { return {mData.cols(), mData.rows()}; } - void divideByAsync(const Channel& other, std::vector>& futures, int priority); - void multiplyWithAsync(const Channel& other, std::vector>& futures, int priority); + std::future divideByAsync(const Channel& other, int priority); + std::future multiplyWithAsync(const Channel& other, int priority); void setZero() { mData.setZero(); } diff --git a/include/tev/Common.h b/include/tev/Common.h index 6031bcc6b..d7ede209b 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -201,12 +201,24 @@ inline std::string nativeString(const filesystem::path& path) { } #endif +template class ScopeGuard { public: - ScopeGuard(const std::function& callback) : mCallback{callback} {} + ScopeGuard(const T& callback) : mCallback{callback} {} + ScopeGuard(T&& callback) : mCallback{std::move(callback)} {} ~ScopeGuard() { mCallback(); } private: - std::function mCallback; + T mCallback; +}; + +template +class SharedScopeGuard { +public: + SharedScopeGuard(const T& callback) : mSharedPtr{std::make_shared>(callback)} {} + SharedScopeGuard(T&& callback) : mSharedPtr{std::make_shared>(std::move(callback))} {} +private: + // Causes `callback` to be fired upon last destruction + std::shared_ptr> mSharedPtr; }; template diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index d90882695..38163e9df 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -15,9 +15,9 @@ TEV_NAMESPACE_BEGIN template -void waitAll(const std::vector>& futures) { +void waitAll(std::vector>& futures) { for (auto& f : futures) { - f.wait(); + f.get(); } } @@ -58,32 +58,34 @@ class ThreadPool { void flushQueue(); template - void parallelForAsync(Int start, Int end, F body, std::vector>& futures, int priority) { + auto parallelForAsync(Int start, Int end, F body, int priority) { Int range = end - start; Int nTasks = std::min((Int)mNumThreads, range); + std::promise promise; + auto future = promise.get_future(); + + auto callbackGuard = SharedScopeGuard{[p = std::move(promise)] () mutable { + p.set_value(); + }}; + for (Int i = 0; i < nTasks; ++i) { Int taskStart = start + (range * i / nTasks); Int taskEnd = start + (range * (i+1) / nTasks); TEV_ASSERT(taskStart != taskEnd, "Shouldn't not produce tasks with empty range."); - futures.emplace_back(enqueueTask([taskStart, taskEnd, body] { + enqueueTask([callbackGuard, taskStart, taskEnd, body] { for (Int j = taskStart; j < taskEnd; ++j) { body(j); } - }, priority)); + }, priority); } - } - template - std::vector> parallelForAsync(Int start, Int end, F body, int priority) { - std::vector> futures; - parallelForAsync(start, end, body, futures, priority); - return futures; + return future; } template void parallelFor(Int start, Int end, F body, int priority) { - waitAll(parallelForAsync(start, end, body, priority)); + parallelForAsync(start, end, body, priority).get(); } private: diff --git a/src/Channel.cpp b/src/Channel.cpp index f2a95d681..dca52fe59 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -52,20 +52,20 @@ Color Channel::color(string channel) { return Color(1.0f, 1.0f); } -void Channel::divideByAsync(const Channel& other, vector>& futures, int priority) { - gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { +std::future Channel::divideByAsync(const Channel& other, int priority) { + return gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { if (other.at(i) != 0) { at(i) /= other.at(i); } else { at(i) = 0; } - }, futures, priority); + }, priority); } -void Channel::multiplyWithAsync(const Channel& other, vector>& futures, int priority) { - gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { +std::future Channel::multiplyWithAsync(const Channel& other, int priority) { + return gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { at(i) *= other.at(i); - }, futures, priority); + }, priority); } void Channel::updateTile(int x, int y, int width, int height, const vector& newData) { diff --git a/src/Image.cpp b/src/Image.cpp index 779a13ab4..5f176cdad 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -139,14 +139,18 @@ nanogui::Texture* Image::texture(const vector& channelNames) { } const auto& channelData = chan->data(); - gThreadPool->parallelForAsync(0, numPixels, [&channelData, &data, i](DenseIndex j) { - data[j * 4 + i] = channelData(j); - }, futures, std::numeric_limits::max()); + futures.emplace_back( + gThreadPool->parallelForAsync(0, numPixels, [&channelData, &data, i](DenseIndex j) { + data[j * 4 + i] = channelData(j); + }, std::numeric_limits::max()) + ); } else { float val = i == 3 ? 1 : 0; - gThreadPool->parallelForAsync(0, numPixels, [&data, val, i](DenseIndex j) { - data[j * 4 + i] = val; - }, futures, std::numeric_limits::max()); + futures.emplace_back( + gThreadPool->parallelForAsync(0, numPixels, [&data, val, i](DenseIndex j) { + data[j * 4 + i] = val; + }, std::numeric_limits::max()) + ); } } waitAll(futures); @@ -429,7 +433,7 @@ void Image::alphaOperation(const function& func) void Image::multiplyAlpha() { vector> futures; alphaOperation([&] (Channel& target, const Channel& alpha) { - target.multiplyWithAsync(alpha, futures, -mId); + futures.emplace_back(target.multiplyWithAsync(alpha, -mId)); }); waitAll(futures); } @@ -437,7 +441,7 @@ void Image::multiplyAlpha() { void Image::unmultiplyAlpha() { vector> futures; alphaOperation([&] (Channel& target, const Channel& alpha) { - target.divideByAsync(alpha, futures, -mId); + futures.emplace_back(target.divideByAsync(alpha, -mId)); }); waitAll(futures); } @@ -478,6 +482,8 @@ shared_ptr tryLoadImage(int imageId, path path, istream& iStream, string handleException(e); } catch (const Iex::BaseExc& e) { handleException(e); + } catch (const future_error& e) { + handleException(e); } return nullptr; diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index a1a395c2b..4da5ed350 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -635,9 +635,11 @@ shared_ptr ImageCanvas::computeCanvasStatistics( vector> futures; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; - gThreadPool->parallelForAsync(0, numElements, [&, i](DenseIndex j) { - indices(j, i) = valToBin(channel.eval(j)); - }, futures, priority); + futures.emplace_back( + gThreadPool->parallelForAsync(0, numElements, [&, i](DenseIndex j) { + indices(j, i) = valToBin(channel.eval(j)); + }, priority) + ); } waitAll(futures); diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index ee0c80b0b..b6a1d36c5 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -144,30 +144,27 @@ std::tuple ExrImageLoader::load(istream& iStream, const path& p )); } - void copyTo(Channel& channel, vector>& futures, int priority) const { + std::future copyTo(Channel& channel, int priority) const { switch (mImfChannel.type) { case Imf::HALF: { auto data = reinterpret_cast(mData.data()); - gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { + return gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { channel.at(i) = data[i]; - }, futures, priority); - break; + }, priority); } case Imf::FLOAT: { auto data = reinterpret_cast(mData.data()); - gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { + return gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { channel.at(i) = data[i]; - }, futures, priority); - break; + }, priority); } case Imf::UINT: { auto data = reinterpret_cast(mData.data()); - gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { + return gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { channel.at(i) = data[i]; - }, futures, priority); - break; + }, priority); } default: @@ -246,7 +243,7 @@ std::tuple ExrImageLoader::load(istream& iStream, const path& p vector> futures; for (size_t i = 0; i < rawChannels.size(); ++i) { - rawChannels[i].copyTo(result.channels[i], futures, priority); + futures.emplace_back(rawChannels[i].copyTo(result.channels[i], priority)); } waitAll(futures); From 99557d014b7d33ab71b6ca97072db05a2c8a8746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Wed, 4 Aug 2021 18:23:57 +0000 Subject: [PATCH 06/83] Groundwork for using coroutines to load images --- CMakeLists.txt | 4 +- include/tev/Image.h | 87 ++++--- include/tev/ThreadPool.h | 223 +++++++++++++++++- include/tev/imageio/ClipboardImageLoader.h | 2 +- include/tev/imageio/DdsImageLoader.h | 2 +- include/tev/imageio/EmptyImageLoader.h | 2 +- include/tev/imageio/ExrImageLoader.h | 2 +- include/tev/imageio/ImageLoader.h | 3 +- include/tev/imageio/PfmImageLoader.h | 2 +- include/tev/imageio/StbiImageLoader.h | 2 +- src/Image.cpp | 259 +++++++++++---------- src/ImageViewer.cpp | 4 +- src/imageio/ClipboardImageLoader.cpp | 4 +- src/imageio/DdsImageLoader.cpp | 4 +- src/imageio/EmptyImageLoader.cpp | 4 +- src/imageio/ExrImageLoader.cpp | 4 +- src/imageio/PfmImageLoader.cpp | 4 +- src/imageio/StbiImageLoader.cpp | 4 +- src/main.cpp | 2 +- 19 files changed, 442 insertions(+), 176 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 99b9e3160..87b5e47be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ if (APPLE) set(CMAKE_MACOSX_RPATH ON) endif() -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) if (MSVC) # Disable annoying secure CRT warnings @@ -124,7 +124,7 @@ if (MSVC) elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-parameter") if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-gnu-anonymous-struct -Wno-c99-extensions -Wno-nested-anon-types -Wno-deprecated-register") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-gnu-anonymous-struct -Wno-c99-extensions -Wno-nested-anon-types -Wno-deprecated-register -Wno-deprecated-anon-enum-enum-conversion") endif() if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-misleading-indentation -Wno-deprecated-declarations") diff --git a/include/tev/Image.h b/include/tev/Image.h index 50d0237cc..b45d3ffe6 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -26,6 +26,55 @@ struct ImageData { std::vector channels; std::vector layers; nanogui::Matrix4f toRec709 = nanogui::Matrix4f{1.0f}; // Identity by default + + Eigen::Vector2i size() const { + return channels.front().size(); + } + + Eigen::DenseIndex count() const { + return channels.front().count(); + } + + std::vector channelsInLayer(std::string layerName) const; + + void alphaOperation(const std::function& func); + + void multiplyAlpha(int priority); + void unmultiplyAlpha(int priority); + + void ensureValid(); + + bool hasChannel(const std::string& channelName) const { + return channel(channelName) != nullptr; + } + + const Channel* channel(const std::string& channelName) const { + auto it = std::find_if( + std::begin(channels), + std::end(channels), + [&channelName](const Channel& c) { return c.name() == channelName; } + ); + + if (it != std::end(channels)) { + return &(*it); + } else { + return nullptr; + } + } + + Channel* mutableChannel(const std::string& channelName) { + auto it = std::find_if( + std::begin(channels), + std::end(channels), + [&channelName](const Channel& c) { return c.name() == channelName; } + ); + + if (it != std::end(channels)) { + return &(*it); + } else { + return nullptr; + } + } }; struct ChannelGroup { @@ -41,7 +90,7 @@ struct ImageTexture { class Image { public: - Image(int id, const filesystem::path& path, std::istream& iStream, const std::string& channelSelector); + Image(int id, const filesystem::path& path, ImageData&& data, const std::string& channelSelector); virtual ~Image(); const filesystem::path& path() const { @@ -59,16 +108,11 @@ class Image { std::string shortName() const; bool hasChannel(const std::string& channelName) const { - return channel(channelName) != nullptr; + return mData.hasChannel(channelName); } const Channel* channel(const std::string& channelName) const { - auto it = std::find_if(std::begin(mData.channels), std::end(mData.channels), [&channelName](const Channel& c) { return c.name() == channelName; }); - if (it != std::end(mData.channels)) { - return &(*it); - } else { - return nullptr; - } + return mData.channel(channelName); } nanogui::Texture* texture(const std::string& channelGroupName); @@ -78,11 +122,11 @@ class Image { std::vector getSortedChannels(const std::string& layerName) const; Eigen::Vector2i size() const { - return mData.channels.front().size(); + return mData.size(); } Eigen::DenseIndex count() const { - return mData.channels.front().count(); + return mData.count(); } const std::vector& channelGroups() const { @@ -109,26 +153,13 @@ class Image { static std::atomic sId; Channel* mutableChannel(const std::string& channelName) { - auto it = std::find_if(std::begin(mData.channels), std::end(mData.channels), [&channelName](const Channel& c) { return c.name() == channelName; }); - if (it != std::end(mData.channels)) { - return &(*it); - } else { - return nullptr; - } + return mData.mutableChannel(channelName); } - std::vector channelsInLayer(std::string layerName) const; std::vector getGroupedChannels(const std::string& layerName) const; void toRec709(); - void alphaOperation(const std::function& func); - - void multiplyAlpha(); - void unmultiplyAlpha(); - - void ensureValid(); - filesystem::path mPath; std::string mChannelSelector; @@ -143,10 +174,10 @@ class Image { int mId; }; -std::shared_ptr tryLoadImage(int imageId, filesystem::path path, std::istream& iStream, std::string channelSelector); -std::shared_ptr tryLoadImage(filesystem::path path, std::istream& iStream, std::string channelSelector); -std::shared_ptr tryLoadImage(int imageId, filesystem::path path, std::string channelSelector); -std::shared_ptr tryLoadImage(filesystem::path path, std::string channelSelector); +Task> tryLoadImage(int imageId, filesystem::path path, std::istream& iStream, std::string channelSelector); +Task> tryLoadImage(filesystem::path path, std::istream& iStream, std::string channelSelector); +Task> tryLoadImage(int imageId, filesystem::path path, std::string channelSelector); +Task> tryLoadImage(filesystem::path path, std::string channelSelector); struct ImageAddition { bool shallSelect; diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 38163e9df..f59620d65 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -8,10 +8,45 @@ #include #include #include +// #include #include #include #include +#include + +class Latch { +public: + Latch(int val) : mCounter{val} {} + bool countDown() noexcept { + bool result = (--mCounter == 0); + if (result) { + std::unique_lock lock{mMutex}; + mCv.notify_all(); + } + return result; + } + + bool tryWait() { + return mCounter == 0; + } + + void wait() { + if (mCounter <= 0) { + return; + } + + std::unique_lock lock{mMutex}; + if (mCounter > 0) { + mCv.wait(lock); + } + } +private: + std::atomic mCounter; + std::mutex mMutex; + std::condition_variable mCv; +}; + TEV_NAMESPACE_BEGIN template @@ -21,6 +56,163 @@ void waitAll(std::vector>& futures) { } } +template +struct Task { + struct promise_type { + std::experimental::coroutine_handle<> precursor; + Latch latch{2}; + + T data; + + Task get_return_object() noexcept { + return {std::experimental::coroutine_handle::from_promise(*this)}; + } + + std::experimental::suspend_never initial_suspend() const noexcept { return {}; } + + void unhandled_exception() { + tlog::error() << "Unhandled exception in Task"; + } + + // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). + // The awaiter returned here defines what happens next + auto final_suspend() const noexcept { + struct awaiter { + // Return false here to return control to the thread's event loop. Remember that we're + // running on some async thread at this point. + bool await_ready() const noexcept { return false; } + + void await_resume() const noexcept {} + + // Returning a coroutine handle here resumes the coroutine it refers to (needed for + // continuation handling). If we wanted, we could instead enqueue that coroutine handle + // instead of immediately resuming it by enqueuing it and returning void. + std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle h) const noexcept { + bool isLast = h.promise().latch.countDown(); + if (isLast) { + if (!h.promise().precursor) { + tlog::error() << "Precursor must be defined when being last."; + } + return h.promise().precursor; + } + + return std::experimental::noop_coroutine(); + } + }; + + return awaiter{}; + } + + // When the coroutine co_returns a value, this method is used to publish the result + void return_value(T value) noexcept { + data = std::move(value); + } + }; + + // The following methods make our task type conform to the awaitable concept, so we can + // co_await for a task to complete + + bool await_ready() const noexcept { + // No need to suspend if this task has no outstanding work + return handle.done(); + } + + T await_resume() const noexcept { + // The returned value here is what `co_await our_task` evaluates to + return std::move(handle.promise().data); + } + + bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { + // The coroutine itself is being suspended (async work can beget other async work) + // Record the argument as the continuation point when this is resumed later. See + // the final_suspend awaiter on the promise_type above for where this gets used + handle.promise().precursor = coroutine; + + return !handle.promise().latch.countDown(); + } + + void wait() const { + handle.promise().latch.wait(); + } + + T get() const { + wait(); + return std::move(handle.promise().data); + } + + // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) + std::experimental::coroutine_handle handle; +}; + +template <> +struct Task { + struct promise_type { + std::experimental::coroutine_handle<> precursor; + Latch latch{2}; + + Task get_return_object() noexcept { + return {std::experimental::coroutine_handle::from_promise(*this)}; + } + + std::experimental::suspend_never initial_suspend() const noexcept { return {}; } + + void unhandled_exception() { + tlog::error() << "Unhandled exception in Task"; + } + + auto final_suspend() const noexcept { + struct awaiter { + bool await_ready() const noexcept { return false; } + void await_resume() const noexcept {} + + std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle h) const noexcept { + bool isLast = h.promise().latch.countDown(); + if (isLast) { + if (!h.promise().precursor) { + tlog::error() << "Precursor must be defined when being last."; + } + return h.promise().precursor; + } + + return std::experimental::noop_coroutine(); + } + }; + + return awaiter{}; + } + + // When the coroutine co_returns a value, this method is used to publish the result + void return_void() noexcept {} + }; + + // The following methods make our task type conform to the awaitable concept, so we can + // co_await for a task to complete + + bool await_ready() const noexcept { + // No need to suspend if this task has no outstanding work + return handle.done(); + } + + void await_resume() const noexcept {} + + bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { + // The coroutine itself is being suspended (async work can beget other async work) + // Record the argument as the continuation point when this is resumed later. See + // the final_suspend awaiter on the promise_type above for where this gets used + handle.promise().precursor = coroutine; + + // No need to suspend if the task has already finished + return !handle.promise().latch.countDown(); + } + + void wait() { + handle.promise().latch.wait(); + } + + // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) + std::experimental::coroutine_handle handle; +}; + class ThreadPool { public: ThreadPool(); @@ -28,7 +220,7 @@ class ThreadPool { virtual ~ThreadPool(); template - auto enqueueTask(F&& f, int priority) -> std::future> { + auto enqueueTask(F&& f, int priority) { using return_type = std::invoke_result_t; ++mNumTasksInSystem; @@ -46,6 +238,35 @@ class ThreadPool { return res; } + inline auto schedule(int priority) noexcept { + class Awaiter { + public: + Awaiter(ThreadPool* pool, int priority) + : mPool{pool}, mPriority{priority} {} + + // Unlike the OS event case, there's no case where we suspend and the work + // is immediately ready + bool await_ready() const noexcept { return false; } + + // Since await_ready() always returns false, when suspend is called, we will + // always immediately suspend and call this function (which enqueues the coroutine + // for immediate reactivation on a different thread) + void await_suspend(std::experimental::coroutine_handle<> coroutine) noexcept { + mPool->enqueueTask(coroutine, mPriority); + } + + void await_resume() const noexcept { + tlog::info() << "Suspended task now running on thread " << std::this_thread::get_id(); + } + + private: + ThreadPool* mPool; + int mPriority; + }; + + return Awaiter{this, priority}; + } + void startThreads(size_t num); void shutdownThreads(size_t num); diff --git a/include/tev/imageio/ClipboardImageLoader.h b/include/tev/imageio/ClipboardImageLoader.h index cfca614a5..eeb303da3 100644 --- a/include/tev/imageio/ClipboardImageLoader.h +++ b/include/tev/imageio/ClipboardImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ClipboardImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "clipboard"; diff --git a/include/tev/imageio/DdsImageLoader.h b/include/tev/imageio/DdsImageLoader.h index 7174e7e25..219594455 100644 --- a/include/tev/imageio/DdsImageLoader.h +++ b/include/tev/imageio/DdsImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class DdsImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "DDS"; diff --git a/include/tev/imageio/EmptyImageLoader.h b/include/tev/imageio/EmptyImageLoader.h index 44c8f321d..d7870595a 100644 --- a/include/tev/imageio/EmptyImageLoader.h +++ b/include/tev/imageio/EmptyImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class EmptyImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "IPC"; diff --git a/include/tev/imageio/ExrImageLoader.h b/include/tev/imageio/ExrImageLoader.h index f95f1ae24..770ce9b47 100644 --- a/include/tev/imageio/ExrImageLoader.h +++ b/include/tev/imageio/ExrImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ExrImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "OpenEXR"; diff --git a/include/tev/imageio/ImageLoader.h b/include/tev/imageio/ImageLoader.h index 3aa8b0078..45d77af40 100644 --- a/include/tev/imageio/ImageLoader.h +++ b/include/tev/imageio/ImageLoader.h @@ -5,6 +5,7 @@ #include #include +#include #include @@ -21,7 +22,7 @@ class ImageLoader { virtual bool canLoadFile(std::istream& iStream) const = 0; // Return loaded image data as well as whether that data has the alpha channel pre-multiplied or not. - virtual std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; + virtual Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; virtual std::string name() const = 0; diff --git a/include/tev/imageio/PfmImageLoader.h b/include/tev/imageio/PfmImageLoader.h index c9fe434a2..124756f2e 100644 --- a/include/tev/imageio/PfmImageLoader.h +++ b/include/tev/imageio/PfmImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class PfmImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "PFM"; diff --git a/include/tev/imageio/StbiImageLoader.h b/include/tev/imageio/StbiImageLoader.h index cae283208..369ca3bb2 100644 --- a/include/tev/imageio/StbiImageLoader.h +++ b/include/tev/imageio/StbiImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class StbiImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - std::tuple load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "STBI"; diff --git a/src/Image.cpp b/src/Image.cpp index 5f176cdad..d27ad554c 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -19,56 +19,98 @@ using namespace std; TEV_NAMESPACE_BEGIN -atomic Image::sId(0); +vector ImageData::channelsInLayer(string layerName) const { + vector result; -Image::Image(int id, const class path& path, istream& iStream, const string& channelSelector) -: mPath{path}, mChannelSelector{channelSelector}, mId{id} { - mName = channelSelector.empty() ? path.str() : tfm::format("%s:%s", path, channelSelector); + if (layerName.empty()) { + for (const auto& c : channels) { + if (c.name().find(".") == string::npos) { + result.emplace_back(c.name()); + } + } + } else { + for (const auto& c : channels) { + // If the layer name starts at the beginning, and + // if no other dot is found after the end of the layer name, + // then we have found a channel of this layer. + if (c.name().starts_with(layerName) == 0 && c.name().length() > layerName.length()) { + const auto& channelWithoutLayer = c.name().substr(layerName.length() + 1); + if (channelWithoutLayer.find(".") == string::npos) { + result.emplace_back(c.name()); + } + } + } + } - auto start = chrono::system_clock::now(); + return result; +} - if (!iStream) { - throw invalid_argument{tfm::format("Image %s could not be opened.", mName)}; - } +void ImageData::alphaOperation(const function& func) { + for (const auto& layer : layers) { + string layerPrefix = layer.empty() ? "" : (layer + "."); + string alphaChannelName = layerPrefix + "A"; + + if (!hasChannel(alphaChannelName)) { + continue; + } - std::string loadMethod; - for (const auto& imageLoader : ImageLoader::getLoaders()) { - // If we arrived at the last loader, then we want to at least try loading the image, - // even if it is likely to fail. - bool useLoader = imageLoader == ImageLoader::getLoaders().back() || imageLoader->canLoadFile(iStream); - - // Reset file cursor in case file load check changed it. - iStream.clear(); - iStream.seekg(0); - - if (useLoader) { - loadMethod = imageLoader->name(); - bool hasPremultipliedAlpha; - std::tie(mData, hasPremultipliedAlpha) = imageLoader->load(iStream, mPath, mChannelSelector, -mId); - ensureValid(); - - // We assume an internal pre-multiplied-alpha representation - if (!hasPremultipliedAlpha) { - multiplyAlpha(); + const Channel* alphaChannel = channel(alphaChannelName); + for (auto& channelName : channelsInLayer(layer)) { + if (channelName != alphaChannelName) { + func(*mutableChannel(channelName), *alphaChannel); } - break; } } +} + +void ImageData::multiplyAlpha(int priority) { + vector> futures; + alphaOperation([&] (Channel& target, const Channel& alpha) { + futures.emplace_back(target.multiplyWithAsync(alpha, priority)); + }); + waitAll(futures); +} + +void ImageData::unmultiplyAlpha(int priority) { + vector> futures; + alphaOperation([&] (Channel& target, const Channel& alpha) { + futures.emplace_back(target.divideByAsync(alpha, priority)); + }); + waitAll(futures); +} + +void ImageData::ensureValid() { + if (layers.empty()) { + throw runtime_error{"Images must have at least one layer."}; + } + + if (channels.empty()) { + throw runtime_error{"Images must have at least one channel."}; + } + + for (const auto& c : channels) { + if (c.size() != size()) { + throw runtime_error{tfm::format( + "All channels must have the same size as their image. (%s:%dx%d != %dx%d)", + c.name(), c.size().x(), c.size().y(), size().x(), size().y() + )}; + } + } +} + +atomic Image::sId(0); + +Image::Image(int id, const class path& path, ImageData&& data, const string& channelSelector) +: mPath{path}, mChannelSelector{channelSelector}, mData{std::move(data)}, mId{id} { + mName = channelSelector.empty() ? path.str() : tfm::format("%s:%s", path, channelSelector); for (const auto& layer : mData.layers) { auto groups = getGroupedChannels(layer); mChannelGroups.insert(end(mChannelGroups), begin(groups), end(groups)); } - + // Convert chromaticities to sRGB / Rec 709 if they aren't already. toRec709(); - - auto end = chrono::system_clock::now(); - chrono::duration elapsedSeconds = end - start; - - ensureValid(); - - tlog::success() << tfm::format("Loaded '%s' via %s after %.3f seconds.", mName, loadMethod, elapsedSeconds.count()); } Image::~Image() { @@ -160,32 +202,6 @@ nanogui::Texture* Image::texture(const vector& channelNames) { return texture.get(); } -vector Image::channelsInLayer(string layerName) const { - vector result; - - if (layerName.empty()) { - for (const auto& c : mData.channels) { - if (c.name().find(".") == string::npos) { - result.emplace_back(c.name()); - } - } - } else { - for (const auto& c : mData.channels) { - // If the layer name starts at the beginning, and - // if no other dot is found after the end of the layer name, - // then we have found a channel of this layer. - if (c.name().find(layerName) == 0 && c.name().length() > layerName.length()) { - const auto& channelWithoutLayer = c.name().substr(layerName.length() + 1); - if (channelWithoutLayer.find(".") == string::npos) { - result.emplace_back(c.name()); - } - } - } - } - - return result; -} - vector Image::channelsInGroup(const string& groupName) const { for (const auto& group : mChannelGroups) { if (group.name == groupName) { @@ -232,7 +248,7 @@ vector Image::getGroupedChannels(const string& layerName) const { string layerPrefix = layerName.empty() ? "" : (layerName + "."); string alphaChannelName = layerPrefix + "A"; - vector allChannels = channelsInLayer(layerName); + vector allChannels = mData.channelsInLayer(layerName); auto alphaIt = find(begin(allChannels), end(allChannels), alphaChannelName); bool hasAlpha = alphaIt != end(allChannels); @@ -363,7 +379,7 @@ string Image::toString() const { auto localLayers = mData.layers; transform(begin(localLayers), end(localLayers), begin(localLayers), [this](string layer) { - auto channels = channelsInLayer(layer); + auto channels = mData.channelsInLayer(layer); transform(begin(channels), end(channels), begin(channels), [](string channel) { return Channel::tail(channel); }); @@ -412,70 +428,57 @@ void Image::toRec709() { waitAll(futures); } -void Image::alphaOperation(const function& func) { - for (const auto& layer : mData.layers) { - string layerPrefix = layer.empty() ? "" : (layer + "."); - string alphaChannelName = layerPrefix + "A"; - - if (!hasChannel(alphaChannelName)) { - continue; +Task> tryLoadImage(int imageId, path path, istream& iStream, string channelSelector) { + auto handleException = [&](const exception& e) { + if (channelSelector.empty()) { + tlog::error() << tfm::format("Could not load '%s'. %s", path, e.what()); + } else { + tlog::error() << tfm::format("Could not load '%s:%s'. %s", path, channelSelector, e.what()); } + }; - const Channel* alphaChannel = channel(alphaChannelName); - for (auto& channelName : channelsInLayer(layer)) { - if (channelName != alphaChannelName) { - func(*mutableChannel(channelName), *alphaChannel); - } + try { + auto start = chrono::system_clock::now(); + + if (!iStream) { + throw invalid_argument{tfm::format("Image %s could not be opened.", path)}; } - } -} -void Image::multiplyAlpha() { - vector> futures; - alphaOperation([&] (Channel& target, const Channel& alpha) { - futures.emplace_back(target.multiplyWithAsync(alpha, -mId)); - }); - waitAll(futures); -} + std::string loadMethod; + for (const auto& imageLoader : ImageLoader::getLoaders()) { + // If we arrived at the last loader, then we want to at least try loading the image, + // even if it is likely to fail. + bool useLoader = imageLoader == ImageLoader::getLoaders().back() || imageLoader->canLoadFile(iStream); -void Image::unmultiplyAlpha() { - vector> futures; - alphaOperation([&] (Channel& target, const Channel& alpha) { - futures.emplace_back(target.divideByAsync(alpha, -mId)); - }); - waitAll(futures); -} + // Reset file cursor in case file load check changed it. + iStream.clear(); + iStream.seekg(0); -void Image::ensureValid() { - if (mData.layers.empty()) { - throw runtime_error{"Images must have at least one layer."}; - } + if (useLoader) { + // Earlier images should be prioritized when loading. + int taskPriority = -imageId; - if (mData.channels.empty()) { - throw runtime_error{"Images must have at least one channel."}; - } + loadMethod = imageLoader->name(); + auto [data, hasPremultipliedAlpha] = co_await imageLoader->load(iStream, path, channelSelector, taskPriority); + data.ensureValid(); - for (const auto& c : mData.channels) { - if (c.size() != size()) { - throw runtime_error{tfm::format( - "All channels must have the same size as their image. (%s:%dx%d != %dx%d)", - c.name(), c.size().x(), c.size().y(), size().x(), size().y() - )}; - } - } -} + // We assume an internal pre-multiplied-alpha representation + if (!hasPremultipliedAlpha) { + data.multiplyAlpha(taskPriority); + } -shared_ptr tryLoadImage(int imageId, path path, istream& iStream, string channelSelector) { - auto handleException = [&](const exception& e) { - if (channelSelector.empty()) { - tlog::error() << tfm::format("Could not load '%s'. %s", path, e.what()); - } else { - tlog::error() << tfm::format("Could not load '%s:%s'. %s", path, channelSelector, e.what()); + auto image = make_shared(imageId, path, std::move(data), channelSelector); + + auto end = chrono::system_clock::now(); + chrono::duration elapsedSeconds = end - start; + + tlog::success() << tfm::format("Loaded '%s' via %s after %.3f seconds.", image->name(), loadMethod, elapsedSeconds.count()); + + co_return image; + } } - }; - try { - return make_shared(imageId, path, iStream, channelSelector); + throw runtime_error{"No suitable image loader found."}; } catch (const invalid_argument& e) { handleException(e); } catch (const runtime_error& e) { @@ -486,14 +489,14 @@ shared_ptr tryLoadImage(int imageId, path path, istream& iStream, string handleException(e); } - return nullptr; + co_return nullptr; } -shared_ptr tryLoadImage(path path, istream& iStream, string channelSelector) { +Task> tryLoadImage(path path, istream& iStream, string channelSelector) { return tryLoadImage(Image::drawId(), path, iStream, channelSelector); } -shared_ptr tryLoadImage(int imageId, path path, string channelSelector) { +Task> tryLoadImage(int imageId, path path, string channelSelector) { try { path = path.make_absolute(); } catch (const runtime_error& e) { @@ -505,15 +508,25 @@ shared_ptr tryLoadImage(int imageId, path path, string channelSelector) { return tryLoadImage(imageId, path, fileStream, channelSelector); } -shared_ptr tryLoadImage(path path, string channelSelector) { +Task> tryLoadImage(path path, string channelSelector) { return tryLoadImage(Image::drawId(), path, channelSelector); } void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { int imageId = Image::drawId(); + + // auto task = [imageId, path, channelSelector, shallSelect, this] { + // co_await gThreadPool->schedule(-imageId); + + // co_await tryLoadImage(imageId, path, channelSelector); + + // if (image) { + // mLoadedImages.push({ shallSelect, image }); + // } + // }(); - mWorkers.enqueueTask([imageId, path, channelSelector, shallSelect, this] { - auto image = tryLoadImage(imageId, path, channelSelector); + mWorkers.enqueueTask([imageId, path, channelSelector, shallSelect, this]() -> Task { + auto image = co_await tryLoadImage(imageId, path, channelSelector); if (image) { mLoadedImages.push({ shallSelect, image }); } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index e9f9e747c..1fd8a90f1 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -759,7 +759,7 @@ bool ImageViewer::keyboard_event(int key, int scancode, int action, int modifier << string(clipImage.data(), clipImage.spec().bytes_per_row * clipImage.spec().height) ; - auto image = tryLoadImage(tfm::format("clipboard (%d)", ++mClipboardIndex), imageStream, ""); + auto image = tryLoadImage(tfm::format("clipboard (%d)", ++mClipboardIndex), imageStream, "").get(); if (image) { addImage(image, true); } else { @@ -1141,7 +1141,7 @@ void ImageViewer::reloadImage(shared_ptr image, bool shallSelect) { int referenceId = imageId(mCurrentReference); - auto newImage = tryLoadImage(image->path(), image->channelSelector()); + auto newImage = tryLoadImage(image->path(), image->channelSelector()).get(); if (newImage) { removeImage(image); insertImage(newImage, id, shallSelect); diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index a7baf49d2..6cdbf0670 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -23,7 +23,7 @@ bool ClipboardImageLoader::canLoadFile(istream& iStream) const { return result; } -std::tuple ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task> ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; char magic[4]; @@ -119,7 +119,7 @@ std::tuple ClipboardImageLoader::load(istream& iStream, const p // within a topmost root layer. result.layers.emplace_back(""); - return {result, false}; + co_return {result, false}; } TEV_NAMESPACE_END diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index 31c557f42..a41651823 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -153,7 +153,7 @@ static int getDxgiChannelCount(DXGI_FORMAT fmt) { } } -std::tuple DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task> DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { // COM must be initialized on the thread executing load(). if (CoInitializeEx(nullptr, COINIT_MULTITHREADED) != S_OK) { throw invalid_argument{"Failed to initialize COM."}; @@ -265,7 +265,7 @@ std::tuple DdsImageLoader::load(istream& iStream, const path&, // within a topmost root layer. result.layers.emplace_back(""); - return {result, scratchImage.GetMetadata().IsPMAlpha()}; + co_return {result, scratchImage.GetMetadata().IsPMAlpha()}; } TEV_NAMESPACE_END diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index c2944c585..d874f02c3 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -22,7 +22,7 @@ bool EmptyImageLoader::canLoadFile(istream& iStream) const { return result; } -std::tuple EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { +Task> EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { ImageData result; string magic; @@ -61,7 +61,7 @@ std::tuple EmptyImageLoader::load(istream& iStream, const path& result.layers.emplace_back(layer); } - return {result, true}; + co_return {result, true}; } TEV_NAMESPACE_END diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index b6a1d36c5..7c104ef9a 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -88,7 +88,7 @@ bool ExrImageLoader::canLoadFile(istream& iStream) const { return result; } -std::tuple ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { +Task> ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { ImageData result; StdIStream stdIStream{iStream, path.str().c_str()}; @@ -274,7 +274,7 @@ std::tuple ExrImageLoader::load(istream& iStream, const path& p } } - return {result, true}; + co_return {result, true}; } TEV_NAMESPACE_END diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index d9973ebc1..bd0606f35 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -21,7 +21,7 @@ bool PfmImageLoader::canLoadFile(istream& iStream) const { return result; } -std::tuple PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task> PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; string magic; @@ -110,7 +110,7 @@ std::tuple PfmImageLoader::load(istream& iStream, const path&, // within a topmost root layer. result.layers.emplace_back(""); - return {result, false}; + co_return {result, false}; } TEV_NAMESPACE_END diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index ced8214c9..597846d2a 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -18,7 +18,7 @@ bool StbiImageLoader::canLoadFile(istream&) const { return true; } -std::tuple StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task> StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; static const stbi_io_callbacks callbacks = { @@ -107,7 +107,7 @@ std::tuple StbiImageLoader::load(istream& iStream, const path&, // within a topmost root layer. result.layers.emplace_back(""); - return {result, false}; + co_return {result, false}; } TEV_NAMESPACE_END diff --git a/src/main.cpp b/src/main.cpp index 87f8775cd..ee1ab470d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -114,7 +114,7 @@ void handleIpcPacket(const IpcPacket& packet, const std::shared_ptraddImage(image, info.grabFocus); } From dc8f67582c4b71acd03fc728614db8174fe90f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Thu, 5 Aug 2021 08:41:37 +0000 Subject: [PATCH 07/83] Consolidate Task<> --- include/tev/ThreadPool.h | 175 +++++++++++++-------------------------- 1 file changed, 59 insertions(+), 116 deletions(-) diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index f59620d65..26fb9519e 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -56,62 +56,75 @@ void waitAll(std::vector>& futures) { } } -template -struct Task { - struct promise_type { - std::experimental::coroutine_handle<> precursor; - Latch latch{2}; +template +struct TaskPromiseBase { + data_t data; - T data; + // When the coroutine co_returns a value, this method is used to publish the result + void return_value(data_t value) noexcept { + data = std::move(value); + } +}; - Task get_return_object() noexcept { - return {std::experimental::coroutine_handle::from_promise(*this)}; - } +template <> +struct TaskPromiseBase { + void return_void() noexcept {} +}; - std::experimental::suspend_never initial_suspend() const noexcept { return {}; } +template +struct TaskPromise : public TaskPromiseBase { + std::experimental::coroutine_handle<> precursor; + Latch latch{2}; - void unhandled_exception() { - tlog::error() << "Unhandled exception in Task"; - } + future_t get_return_object() noexcept { + return {std::experimental::coroutine_handle>::from_promise(*this)}; + } - // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). - // The awaiter returned here defines what happens next - auto final_suspend() const noexcept { - struct awaiter { - // Return false here to return control to the thread's event loop. Remember that we're - // running on some async thread at this point. - bool await_ready() const noexcept { return false; } - - void await_resume() const noexcept {} - - // Returning a coroutine handle here resumes the coroutine it refers to (needed for - // continuation handling). If we wanted, we could instead enqueue that coroutine handle - // instead of immediately resuming it by enqueuing it and returning void. - std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle h) const noexcept { - bool isLast = h.promise().latch.countDown(); - if (isLast) { - if (!h.promise().precursor) { - tlog::error() << "Precursor must be defined when being last."; - } - return h.promise().precursor; - } + std::experimental::suspend_never initial_suspend() const noexcept { return {}; } - return std::experimental::noop_coroutine(); + void unhandled_exception() { + tlog::error() << "Unhandled exception in Task"; + } + + // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). + // The awaiter returned here defines what happens next + auto final_suspend() const noexcept { + struct awaiter { + // Return false here to return control to the thread's event loop. Remember that we're + // running on some async thread at this point. + bool await_ready() const noexcept { return false; } + + void await_resume() const noexcept {} + + // Returning a coroutine handle here resumes the coroutine it refers to (needed for + // continuation handling). If we wanted, we could instead enqueue that coroutine handle + // instead of immediately resuming it by enqueuing it and returning void. + std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle> h) const noexcept { + bool isLast = h.promise().latch.countDown(); + if (isLast) { + if (!h.promise().precursor) { + tlog::error() << "Precursor must be defined when being last."; + } + return h.promise().precursor; } - }; - return awaiter{}; - } + return std::experimental::noop_coroutine(); + } + }; - // When the coroutine co_returns a value, this method is used to publish the result - void return_value(T value) noexcept { - data = std::move(value); - } - }; + return awaiter{}; + } +}; + +template +struct Task { + using promise_type = TaskPromise, T>; + + // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) + std::experimental::coroutine_handle handle; // The following methods make our task type conform to the awaitable concept, so we can // co_await for a task to complete - bool await_ready() const noexcept { // No need to suspend if this task has no outstanding work return handle.done(); @@ -137,80 +150,10 @@ struct Task { T get() const { wait(); - return std::move(handle.promise().data); - } - - // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) - std::experimental::coroutine_handle handle; -}; - -template <> -struct Task { - struct promise_type { - std::experimental::coroutine_handle<> precursor; - Latch latch{2}; - - Task get_return_object() noexcept { - return {std::experimental::coroutine_handle::from_promise(*this)}; - } - - std::experimental::suspend_never initial_suspend() const noexcept { return {}; } - - void unhandled_exception() { - tlog::error() << "Unhandled exception in Task"; + if constexpr (!std::is_void_v) { + return std::move(handle.promise().data); } - - auto final_suspend() const noexcept { - struct awaiter { - bool await_ready() const noexcept { return false; } - void await_resume() const noexcept {} - - std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle h) const noexcept { - bool isLast = h.promise().latch.countDown(); - if (isLast) { - if (!h.promise().precursor) { - tlog::error() << "Precursor must be defined when being last."; - } - return h.promise().precursor; - } - - return std::experimental::noop_coroutine(); - } - }; - - return awaiter{}; - } - - // When the coroutine co_returns a value, this method is used to publish the result - void return_void() noexcept {} - }; - - // The following methods make our task type conform to the awaitable concept, so we can - // co_await for a task to complete - - bool await_ready() const noexcept { - // No need to suspend if this task has no outstanding work - return handle.done(); - } - - void await_resume() const noexcept {} - - bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { - // The coroutine itself is being suspended (async work can beget other async work) - // Record the argument as the continuation point when this is resumed later. See - // the final_suspend awaiter on the promise_type above for where this gets used - handle.promise().precursor = coroutine; - - // No need to suspend if the task has already finished - return !handle.promise().latch.countDown(); } - - void wait() { - handle.promise().latch.wait(); - } - - // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) - std::experimental::coroutine_handle handle; }; class ThreadPool { From 5fb9cc38fe457f4be48b5c43be13994797bc6fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Fri, 6 Aug 2021 12:07:08 +0000 Subject: [PATCH 08/83] ThreadPool::parallelForAsync as coroutine --- include/tev/Channel.h | 18 +++++++++-- include/tev/Image.h | 13 ++------ include/tev/ImageCanvas.h | 2 +- include/tev/ThreadPool.h | 56 +++++++++++++++------------------- src/Channel.cpp | 16 ---------- src/Image.cpp | 36 ++++++++++++---------- src/ImageCanvas.cpp | 26 ++++++++++------ src/imageio/ExrImageLoader.cpp | 31 +++++++++++-------- 8 files changed, 98 insertions(+), 100 deletions(-) diff --git a/include/tev/Channel.h b/include/tev/Channel.h index 468073a01..c1d79a4fd 100644 --- a/include/tev/Channel.h +++ b/include/tev/Channel.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include @@ -69,8 +70,21 @@ class Channel { return {mData.cols(), mData.rows()}; } - std::future divideByAsync(const Channel& other, int priority); - std::future multiplyWithAsync(const Channel& other, int priority); + auto divideByAsync(const Channel& other, int priority) { + return gThreadPool->parallelForAsync(0, other.count(), [&](Eigen::DenseIndex i) { + if (other.at(i) != 0) { + at(i) /= other.at(i); + } else { + at(i) = 0; + } + }, priority); + } + + auto multiplyWithAsync(const Channel& other, int priority) { + return gThreadPool->parallelForAsync(0, other.count(), [&](Eigen::DenseIndex i) { + at(i) *= other.at(i); + }, priority); + } void setZero() { mData.setZero(); } diff --git a/include/tev/Image.h b/include/tev/Image.h index b45d3ffe6..a2de5ad4f 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -39,8 +39,8 @@ struct ImageData { void alphaOperation(const std::function& func); - void multiplyAlpha(int priority); - void unmultiplyAlpha(int priority); + Task multiplyAlpha(int priority); + Task unmultiplyAlpha(int priority); void ensureValid(); @@ -189,16 +189,7 @@ class BackgroundImagesLoader { void enqueue(const filesystem::path& path, const std::string& channelSelector, bool shallSelect); ImageAddition tryPop() { return mLoadedImages.tryPop(); } - void wait() { - return mWorkers.waitUntilFinished(); - } - private: - // A separate threadpool (other than the global threadpool) is - // required to prevent deadlocking: if more images are loaded - // simultaneously than threads are available, they will starve the - // global thread pool of workers. - ThreadPool mWorkers{1}; SharedQueue mLoadedImages; }; diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 38e69c13f..c9e14132d 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -143,7 +143,7 @@ class ImageCanvas : public nanogui::Canvas { int priority ); - static std::shared_ptr computeCanvasStatistics( + static Task> computeCanvasStatistics( std::shared_ptr image, std::shared_ptr reference, const std::string& requestedChannelGroup, diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 26fb9519e..4886f0c37 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -8,13 +8,14 @@ #include #include #include -// #include #include #include #include #include +TEV_NAMESPACE_BEGIN + class Latch { public: Latch(int val) : mCounter{val} {} @@ -27,8 +28,8 @@ class Latch { return result; } - bool tryWait() { - return mCounter == 0; + int count() noexcept { + return mCounter; } void wait() { @@ -47,10 +48,8 @@ class Latch { std::condition_variable mCv; }; -TEV_NAMESPACE_BEGIN - template -void waitAll(std::vector>& futures) { +void waitAll(std::vector& futures) { for (auto& f : futures) { f.get(); } @@ -101,10 +100,7 @@ struct TaskPromise : public TaskPromiseBase { // instead of immediately resuming it by enqueuing it and returning void. std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle> h) const noexcept { bool isLast = h.promise().latch.countDown(); - if (isLast) { - if (!h.promise().precursor) { - tlog::error() << "Precursor must be defined when being last."; - } + if (isLast && h.promise().precursor) { return h.promise().precursor; } @@ -131,8 +127,10 @@ struct Task { } T await_resume() const noexcept { - // The returned value here is what `co_await our_task` evaluates to - return std::move(handle.promise().data); + if constexpr (!std::is_void_v) { + // The returned value here is what `co_await our_task` evaluates to + return std::move(handle.promise().data); + } } bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { @@ -140,11 +138,11 @@ struct Task { // Record the argument as the continuation point when this is resumed later. See // the final_suspend awaiter on the promise_type above for where this gets used handle.promise().precursor = coroutine; - return !handle.promise().latch.countDown(); } void wait() const { + handle.promise().latch.countDown(); handle.promise().latch.wait(); } @@ -198,9 +196,7 @@ class ThreadPool { mPool->enqueueTask(coroutine, mPriority); } - void await_resume() const noexcept { - tlog::info() << "Suspended task now running on thread " << std::this_thread::get_id(); - } + void await_resume() const noexcept {} private: ThreadPool* mPool; @@ -222,29 +218,27 @@ class ThreadPool { void flushQueue(); template - auto parallelForAsync(Int start, Int end, F body, int priority) { + Task parallelForAsync(Int start, Int end, F body, int priority) { Int range = end - start; Int nTasks = std::min((Int)mNumThreads, range); - std::promise promise; - auto future = promise.get_future(); - - auto callbackGuard = SharedScopeGuard{[p = std::move(promise)] () mutable { - p.set_value(); - }}; - + std::vector> tasks; for (Int i = 0; i < nTasks; ++i) { Int taskStart = start + (range * i / nTasks); Int taskEnd = start + (range * (i+1) / nTasks); TEV_ASSERT(taskStart != taskEnd, "Shouldn't not produce tasks with empty range."); - enqueueTask([callbackGuard, taskStart, taskEnd, body] { - for (Int j = taskStart; j < taskEnd; ++j) { + + tasks.emplace_back([this](Int start, Int end, F body, int priority) -> Task { + co_await schedule(priority); + for (Int j = start; j < end; ++j) { body(j); } - }, priority); + }(taskStart, taskEnd, body, priority)); } - return future; + for (auto& task : tasks) { + co_await task; + } } template @@ -256,18 +250,18 @@ class ThreadPool { size_t mNumThreads = 0; std::vector mThreads; - struct Task { + struct QueuedTask { int priority; std::function fun; struct Comparator { - bool operator()(const Task& a, const Task& b) { + bool operator()(const QueuedTask& a, const QueuedTask& b) { return a.priority < b.priority; } }; }; - std::priority_queue, Task::Comparator> mTaskQueue; + std::priority_queue, QueuedTask::Comparator> mTaskQueue; std::mutex mTaskQueueMutex; std::condition_variable mWorkerCondition; diff --git a/src/Channel.cpp b/src/Channel.cpp index dca52fe59..a363bba57 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -52,22 +52,6 @@ Color Channel::color(string channel) { return Color(1.0f, 1.0f); } -std::future Channel::divideByAsync(const Channel& other, int priority) { - return gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { - if (other.at(i) != 0) { - at(i) /= other.at(i); - } else { - at(i) = 0; - } - }, priority); -} - -std::future Channel::multiplyWithAsync(const Channel& other, int priority) { - return gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { - at(i) *= other.at(i); - }, priority); -} - void Channel::updateTile(int x, int y, int width, int height, const vector& newData) { if (x < 0 || y < 0 || x + width > size().x() || y + height > size().y()) { tlog::warning() << "Tile [" << x << "," << y << "," << width << "," << height << "] could not be updated because it does not fit into the channel's size " << size(); diff --git a/src/Image.cpp b/src/Image.cpp index d27ad554c..34bd0cc2e 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -63,20 +63,24 @@ void ImageData::alphaOperation(const function& f } } -void ImageData::multiplyAlpha(int priority) { - vector> futures; +Task ImageData::multiplyAlpha(int priority) { + vector> tasks; alphaOperation([&] (Channel& target, const Channel& alpha) { - futures.emplace_back(target.multiplyWithAsync(alpha, priority)); + tasks.emplace_back(target.multiplyWithAsync(alpha, priority)); }); - waitAll(futures); + for (auto& task : tasks) { + co_await task; + } } -void ImageData::unmultiplyAlpha(int priority) { - vector> futures; +Task ImageData::unmultiplyAlpha(int priority) { + vector> tasks; alphaOperation([&] (Channel& target, const Channel& alpha) { - futures.emplace_back(target.divideByAsync(alpha, priority)); + tasks.emplace_back(target.divideByAsync(alpha, priority)); }); - waitAll(futures); + for (auto& task : tasks) { + co_await task; + } } void ImageData::ensureValid() { @@ -171,7 +175,7 @@ nanogui::Texture* Image::texture(const vector& channelNames) { auto numPixels = count(); vector data(numPixels * 4); - vector> futures; + vector> tasks; for (size_t i = 0; i < 4; ++i) { if (i < channelNames.size()) { const auto& channelName = channelNames[i]; @@ -181,21 +185,21 @@ nanogui::Texture* Image::texture(const vector& channelNames) { } const auto& channelData = chan->data(); - futures.emplace_back( + tasks.emplace_back( gThreadPool->parallelForAsync(0, numPixels, [&channelData, &data, i](DenseIndex j) { data[j * 4 + i] = channelData(j); }, std::numeric_limits::max()) ); } else { float val = i == 3 ? 1 : 0; - futures.emplace_back( + tasks.emplace_back( gThreadPool->parallelForAsync(0, numPixels, [&data, val, i](DenseIndex j) { data[j * 4 + i] = val; }, std::numeric_limits::max()) ); } } - waitAll(futures); + waitAll(tasks); texture->upload((uint8_t*)data.data()); texture->generate_mipmap(); @@ -493,7 +497,7 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s } Task> tryLoadImage(path path, istream& iStream, string channelSelector) { - return tryLoadImage(Image::drawId(), path, iStream, channelSelector); + co_return co_await tryLoadImage(Image::drawId(), path, iStream, channelSelector); } Task> tryLoadImage(int imageId, path path, string channelSelector) { @@ -505,11 +509,11 @@ Task> tryLoadImage(int imageId, path path, string channelSelec } ifstream fileStream{nativeString(path), ios_base::binary}; - return tryLoadImage(imageId, path, fileStream, channelSelector); + co_return co_await tryLoadImage(imageId, path, fileStream, channelSelector); } Task> tryLoadImage(path path, string channelSelector) { - return tryLoadImage(Image::drawId(), path, channelSelector); + co_return co_await tryLoadImage(Image::drawId(), path, channelSelector); } void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { @@ -525,7 +529,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele // } // }(); - mWorkers.enqueueTask([imageId, path, channelSelector, shallSelect, this]() -> Task { + gThreadPool->enqueueTask([imageId, path, channelSelector, shallSelect, this]() -> Task { auto image = co_await tryLoadImage(imageId, path, channelSelector); if (image) { mLoadedImages.push({ shallSelect, image }); diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 4da5ed350..3136671d9 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -467,9 +467,12 @@ shared_ptr>> ImageCanvas::canvasStatistics() { auto image = mImage, reference = mReference; auto requestedChannelGroup = mRequestedChannelGroup; auto metric = mMetric; - mMeanValues.insert(make_pair(key, make_shared>>([image, reference, requestedChannelGroup, metric, priority]() { - return computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority); - }, &mMeanValueThreadPool))); + mMeanValues.insert(make_pair(key, make_shared>>( + [image, reference, requestedChannelGroup, metric, priority]() { + return computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority).get(); + }, + &mMeanValueThreadPool + ))); auto val = mMeanValues.at(key); val->computeAsync(priority); @@ -554,7 +557,7 @@ vector ImageCanvas::channelsFromImages( return result; } -shared_ptr ImageCanvas::computeCanvasStatistics( +Task> ImageCanvas::computeCanvasStatistics( std::shared_ptr image, std::shared_ptr reference, const string& requestedChannelGroup, @@ -626,24 +629,27 @@ shared_ptr ImageCanvas::computeCanvasStatistics( // In the strange case that we have 0 channels, early return, because the histogram makes no sense. if (nChannels == 0) { - return result; + co_return result; } auto numElements = image->count(); Eigen::MatrixXi indices(numElements, nChannels); - vector> futures; + vector> tasks; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; - futures.emplace_back( + tasks.emplace_back( gThreadPool->parallelForAsync(0, numElements, [&, i](DenseIndex j) { indices(j, i) = valToBin(channel.eval(j)); }, priority) ); } - waitAll(futures); - gThreadPool->parallelFor(0, nChannels, [&](int i) { + for (auto& task : tasks) { + co_await task; + } + + co_await gThreadPool->parallelForAsync(0, nChannels, [&](int i) { for (DenseIndex j = 0; j < numElements; ++j) { result->histogram(indices(j, i), i) += alphaChannel ? alphaChannel->eval(j) : 1; } @@ -660,7 +666,7 @@ shared_ptr ImageCanvas::computeCanvasStatistics( nth_element(temp.data(), temp.data() + idx, temp.data() + temp.size()); result->histogram /= max(temp(idx), 0.1f) * 1.3f; - return result; + co_return result; } Vector2f ImageCanvas::pixelOffset(const Vector2i& size) const { diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 7c104ef9a..ad9879373 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -144,7 +144,7 @@ Task> ExrImageLoader::load(istream& iStream, const p )); } - std::future copyTo(Channel& channel, int priority) const { + auto copyTo(Channel& channel, int priority) const { switch (mImfChannel.type) { case Imf::HALF: { auto data = reinterpret_cast(mData.data()); @@ -192,7 +192,9 @@ Task> ExrImageLoader::load(istream& iStream, const p vector mData; }; - vector rawChannels; + // Allocate raw channels on the heap, because it'll be references + // by nested parallel for coroutine. + auto rawChannels = std::make_unique>(); Imf::FrameBuffer frameBuffer; const Imf::ChannelList& imfChannels = file.header().channels(); @@ -215,10 +217,10 @@ Task> ExrImageLoader::load(istream& iStream, const p for (const auto& match : matches) { const auto& c = match.second; - rawChannels.emplace_back(c.name(), c.channel()); + rawChannels->emplace_back(c.name(), c.channel().type); } - if (rawChannels.empty()) { + if (rawChannels->empty()) { throw invalid_argument{tfm::format("No channels match '%s'.", channelSelector)}; } @@ -226,26 +228,29 @@ Task> ExrImageLoader::load(istream& iStream, const p result.layers.emplace_back(layer); } - gThreadPool->parallelFor(0, (int)rawChannels.size(), [&](int i) { - rawChannels[i].resize((DenseIndex)size.x() * size.y()); + co_await gThreadPool->parallelForAsync(0, (int)rawChannels->size(), [c = rawChannels.get(), size](int i) { + c->at(i).resize((DenseIndex)size.x() * size.y()); }, priority); - for (size_t i = 0; i < rawChannels.size(); ++i) { - rawChannels[i].registerWith(frameBuffer, dw); + for (size_t i = 0; i < rawChannels->size(); ++i) { + rawChannels->at(i).registerWith(frameBuffer, dw); } file.setFrameBuffer(frameBuffer); file.readPixels(dw.min.y, dw.max.y); - for (const auto& rawChannel : rawChannels) { + for (const auto& rawChannel : *rawChannels) { result.channels.emplace_back(Channel{rawChannel.name(), size}); } - vector> futures; - for (size_t i = 0; i < rawChannels.size(); ++i) { - futures.emplace_back(rawChannels[i].copyTo(result.channels[i], priority)); + vector> tasks; + for (size_t i = 0; i < rawChannels->size(); ++i) { + tasks.emplace_back(rawChannels->at(i).copyTo(result.channels[i], priority)); + } + + for (auto& task : tasks) { + co_await task; } - waitAll(futures); hasPremultipliedAlpha = true; From 0334a183d335b70eb6a8ec2a2f93766737c25c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Fri, 6 Aug 2021 12:12:14 +0000 Subject: [PATCH 09/83] Fix possible deadlock in Latch --- include/tev/ThreadPool.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 4886f0c37..ead9f4cde 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -20,18 +20,14 @@ class Latch { public: Latch(int val) : mCounter{val} {} bool countDown() noexcept { + std::unique_lock lock{mMutex}; bool result = (--mCounter == 0); if (result) { - std::unique_lock lock{mMutex}; mCv.notify_all(); } return result; } - int count() noexcept { - return mCounter; - } - void wait() { if (mCounter <= 0) { return; From fc3b20d5c6ee80bb4823b79ec144439f1dfbfa10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Fri, 6 Aug 2021 19:48:29 +0000 Subject: [PATCH 10/83] Fix incorrect use of std::string::starts_with --- src/Image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Image.cpp b/src/Image.cpp index 34bd0cc2e..e75ff2b98 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -33,7 +33,7 @@ vector ImageData::channelsInLayer(string layerName) const { // If the layer name starts at the beginning, and // if no other dot is found after the end of the layer name, // then we have found a channel of this layer. - if (c.name().starts_with(layerName) == 0 && c.name().length() > layerName.length()) { + if (c.name().starts_with(layerName) && c.name().length() > layerName.length()) { const auto& channelWithoutLayer = c.name().substr(layerName.length() + 1); if (channelWithoutLayer.find(".") == string::npos) { result.emplace_back(c.name()); From 48b3725d7a50a63a93974c9531bc00433e32cac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Fri, 6 Aug 2021 19:48:41 +0000 Subject: [PATCH 11/83] Add missing co_await to alpha pre-multiplication --- src/Image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Image.cpp b/src/Image.cpp index e75ff2b98..6eff3ca0c 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -468,7 +468,7 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s // We assume an internal pre-multiplied-alpha representation if (!hasPremultipliedAlpha) { - data.multiplyAlpha(taskPriority); + co_await data.multiplyAlpha(taskPriority); } auto image = make_shared(imageId, path, std::move(data), channelSelector); From 0407a6e41cef4161d9bdb194e3e650eb56674e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 03:11:14 +0000 Subject: [PATCH 12/83] Exception forwarding in Task --- include/tev/ThreadPool.h | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index ead9f4cde..d6b5b7fa6 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -70,6 +70,7 @@ template struct TaskPromise : public TaskPromiseBase { std::experimental::coroutine_handle<> precursor; Latch latch{2}; + std::exception_ptr eptr; future_t get_return_object() noexcept { return {std::experimental::coroutine_handle>::from_promise(*this)}; @@ -78,7 +79,8 @@ struct TaskPromise : public TaskPromiseBase { std::experimental::suspend_never initial_suspend() const noexcept { return {}; } void unhandled_exception() { - tlog::error() << "Unhandled exception in Task"; + eptr = std::current_exception(); + } } // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). @@ -122,7 +124,11 @@ struct Task { return handle.done(); } - T await_resume() const noexcept { + T await_resume() const { + if (handle.promise().eptr) { + std::rethrow_exception(handle.promise().eptr); + } + if constexpr (!std::is_void_v) { // The returned value here is what `co_await our_task` evaluates to return std::move(handle.promise().data); @@ -144,6 +150,9 @@ struct Task { T get() const { wait(); + if (handle.promise().eptr) { + std::rethrow_exception(handle.promise().eptr); + } if constexpr (!std::is_void_v) { return std::move(handle.promise().data); } From 6f539b47d89c19f1446600ae172b3935614ee08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 03:12:26 +0000 Subject: [PATCH 13/83] Support for Task::finally() + coroutine lambdas --- include/tev/ThreadPool.h | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index d6b5b7fa6..21ac8365c 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -71,6 +71,7 @@ struct TaskPromise : public TaskPromiseBase { std::experimental::coroutine_handle<> precursor; Latch latch{2}; std::exception_ptr eptr; + std::vector> onFinalSuspend; future_t get_return_object() noexcept { return {std::experimental::coroutine_handle>::from_promise(*this)}; @@ -81,11 +82,19 @@ struct TaskPromise : public TaskPromiseBase { void unhandled_exception() { eptr = std::current_exception(); } + + template + void finally(F&& fun) { + onFinalSuspend.emplace_back(std::forward(fun)); } // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). // The awaiter returned here defines what happens next auto final_suspend() const noexcept { + for (auto& f : onFinalSuspend) { + f(); + } + struct awaiter { // Return false here to return control to the thread's event loop. Remember that we're // running on some async thread at this point. @@ -124,6 +133,11 @@ struct Task { return handle.done(); } + template + void finally(F&& fun) { + handle.promise().finally(std::forward(fun)); + } + T await_resume() const { if (handle.promise().eptr) { std::rethrow_exception(handle.promise().eptr); @@ -159,6 +173,26 @@ struct Task { } }; +template +void coCaptureVar(Task& task, auto* var) { + task.finally([var]() { + delete var; + }); +} + +inline auto coLambda(auto&& executor) { + return [executor=std::move(executor)](Args&&... args) { + using ReturnType = decltype(executor(args...)); + // copy the lambda into a new std::function pointer + auto exec = new std::function(executor); + // execute the lambda and save the result + auto result = (*exec)(args...); + // call custom method to save lambda until task ends + coCaptureVar(result, exec); + return result; + }; +} + class ThreadPool { public: ThreadPool(); From d485fd78d02bf0f6c0843dfe7abe12df5802f6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 03:13:41 +0000 Subject: [PATCH 14/83] Restore ordering of loaded images --- include/tev/Image.h | 19 +++++++++++++++++++ src/Image.cpp | 40 ++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index a2de5ad4f..d1cb54e3f 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -180,8 +180,15 @@ Task> tryLoadImage(int imageId, filesystem::path path, st Task> tryLoadImage(filesystem::path path, std::string channelSelector); struct ImageAddition { + int loadId; bool shallSelect; std::shared_ptr image; + + struct Comparator { + bool operator()(const ImageAddition& a, const ImageAddition& b) { + return a.loadId > b.loadId; + } + }; }; class BackgroundImagesLoader { @@ -189,8 +196,20 @@ class BackgroundImagesLoader { void enqueue(const filesystem::path& path, const std::string& channelSelector, bool shallSelect); ImageAddition tryPop() { return mLoadedImages.tryPop(); } + bool publishSortedLoads(); + private: SharedQueue mLoadedImages; + + std::priority_queue< + ImageAddition, + std::vector, + ImageAddition::Comparator + > mPendingLoadedImages; + std::mutex mPendingLoadedImagesMutex; + + std::atomic mLoadCounter{0}; + std::atomic mUnsortedLoadCounter{0}; }; TEV_NAMESPACE_END diff --git a/src/Image.cpp b/src/Image.cpp index 6eff3ca0c..7813d7b97 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -518,25 +518,37 @@ Task> tryLoadImage(path path, string channelSelector) { void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { int imageId = Image::drawId(); + int loadId = mUnsortedLoadCounter++; + + gThreadPool->enqueueTask(coLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { + auto image = co_await tryLoadImage(imageId, path, channelSelector); - // auto task = [imageId, path, channelSelector, shallSelect, this] { - // co_await gThreadPool->schedule(-imageId); + { + std::lock_guard lock{mPendingLoadedImagesMutex}; + mPendingLoadedImages.push({ loadId, shallSelect, image }); + } - // co_await tryLoadImage(imageId, path, channelSelector); + if (publishSortedLoads()) { + glfwPostEmptyEvent(); + } + }), -imageId); +} - // if (image) { - // mLoadedImages.push({ shallSelect, image }); - // } - // }(); - - gThreadPool->enqueueTask([imageId, path, channelSelector, shallSelect, this]() -> Task { - auto image = co_await tryLoadImage(imageId, path, channelSelector); - if (image) { - mLoadedImages.push({ shallSelect, image }); +bool BackgroundImagesLoader::publishSortedLoads() { + std::lock_guard lock{mPendingLoadedImagesMutex}; + bool pushed = false; + while (mPendingLoadedImages.top().loadId == mLoadCounter) { + ++mLoadCounter; + + // null image pointers indicate failed loads. These shouldn't be pushed. + if (mPendingLoadedImages.top().image) { + mLoadedImages.push(std::move(mPendingLoadedImages.top())); } - glfwPostEmptyEvent(); - }, -imageId); + mPendingLoadedImages.pop(); + pushed = true; + } + return pushed; } TEV_NAMESPACE_END From 3935d9d56a3ad2fdbded372b3f83ecb9241980f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 03:39:10 +0000 Subject: [PATCH 15/83] Fix broken loading of subsampled EXR channels --- src/imageio/ExrImageLoader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index ad9879373..9fa1aa869 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -217,7 +217,7 @@ Task> ExrImageLoader::load(istream& iStream, const p for (const auto& match : matches) { const auto& c = match.second; - rawChannels->emplace_back(c.name(), c.channel().type); + rawChannels->emplace_back(c.name(), c.channel()); } if (rawChannels->empty()) { From b7e398c98e520f6f5e13ba240b705f9d250fcdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 06:08:45 +0000 Subject: [PATCH 16/83] nanogui::shutdown() at least on Windows and Linux --- src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index ee1ab470d..899784aae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -432,7 +432,9 @@ int mainFunc(const vector& arguments) { // On some linux distributions glfwTerminate() (which is called by // nanogui::shutdown()) causes segfaults. Since we are done with our // program here anyways, let's let the OS clean up after us. - //nanogui::shutdown(); +#if defined(__APPLE__) or defined(_WIN32) + nanogui::shutdown(); +#endif if (ipcThread.joinable()) { ipcThread.join(); From a3392939c15a475f172633d5b8e5a11f1d8b648f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 06:10:23 +0000 Subject: [PATCH 17/83] Blow up subsampled channels to full resolution --- src/imageio/ExrImageLoader.cpp | 136 ++++++++++++++++----------------- 1 file changed, 67 insertions(+), 69 deletions(-) diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 9fa1aa869..1f2d08ecf 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -88,6 +88,73 @@ bool ExrImageLoader::canLoadFile(istream& iStream) const { return result; } +// Helper class for dealing with the raw channels loaded from an exr file. +class RawChannel { +public: + RawChannel(string name, Imf::Channel imfChannel) + : mName(name), mImfChannel(imfChannel) { + } + + void resize(size_t size) { + mData.resize(size * bytesPerPixel()); + } + + void registerWith(Imf::FrameBuffer& frameBuffer, const Imath::Box2i& dw) { + int width = dw.max.x - dw.min.x + 1; + frameBuffer.insert(mName.c_str(), Imf::Slice( + mImfChannel.type, + mData.data() - (dw.min.x + dw.min.y * width) * bytesPerPixel(), + bytesPerPixel(), bytesPerPixel() * (width/mImfChannel.xSampling), + mImfChannel.xSampling, mImfChannel.ySampling, 0 + )); + } + + template + Task copyToTyped(Channel& channel, int priority) const { + int width = channel.size().x(); + int widthSubsampled = width/mImfChannel.ySampling; + + auto data = reinterpret_cast(mData.data()); + co_await gThreadPool->parallelForAsync(0, channel.size().y(), [&, data](int y) { + for (int x = 0; x < width; ++x) { + channel.at({x, y}) = data[x/mImfChannel.xSampling + (y/mImfChannel.ySampling) * widthSubsampled]; + } + }, priority); + } + + Task copyTo(Channel& channel, int priority) const { + switch (mImfChannel.type) { + case Imf::HALF: + co_await copyToTyped<::half>(channel, priority); break; + case Imf::FLOAT: + co_await copyToTyped(channel, priority); break; + case Imf::UINT: + co_await copyToTyped(channel, priority); break; + default: + throw runtime_error("Invalid pixel type encountered."); + } + } + + const string& name() const { + return mName; + } + +private: + int bytesPerPixel() const { + switch (mImfChannel.type) { + case Imf::HALF: return sizeof(::half); + case Imf::FLOAT: return sizeof(float); + case Imf::UINT: return sizeof(uint32_t); + default: + throw runtime_error("Invalid pixel type encountered."); + } + } + + string mName; + Imf::Channel mImfChannel; + vector mData; +}; + Task> ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { ImageData result; @@ -123,75 +190,6 @@ Task> ExrImageLoader::load(istream& iStream, const p throw invalid_argument{"EXR image has zero pixels."}; } - // Inline helper class for dealing with the raw channels loaded from an exr file. - class RawChannel { - public: - RawChannel(string name, Imf::Channel imfChannel) - : mName(name), mImfChannel(imfChannel) { - } - - void resize(size_t size) { - mData.resize(size * bytesPerPixel()); - } - - void registerWith(Imf::FrameBuffer& frameBuffer, const Imath::Box2i& dw) { - int width = dw.max.x - dw.min.x + 1; - frameBuffer.insert(mName.c_str(), Imf::Slice( - mImfChannel.type, - mData.data() - (dw.min.x + dw.min.y * width) * bytesPerPixel(), - bytesPerPixel(), bytesPerPixel() * width, - mImfChannel.xSampling, mImfChannel.ySampling, 0 - )); - } - - auto copyTo(Channel& channel, int priority) const { - switch (mImfChannel.type) { - case Imf::HALF: { - auto data = reinterpret_cast(mData.data()); - return gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { - channel.at(i) = data[i]; - }, priority); - } - - case Imf::FLOAT: { - auto data = reinterpret_cast(mData.data()); - return gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { - channel.at(i) = data[i]; - }, priority); - } - - case Imf::UINT: { - auto data = reinterpret_cast(mData.data()); - return gThreadPool->parallelForAsync(0, channel.count(), [&, data](DenseIndex i) { - channel.at(i) = data[i]; - }, priority); - } - - default: - throw runtime_error("Invalid pixel type encountered."); - } - } - - const string& name() const { - return mName; - } - - private: - int bytesPerPixel() const { - switch (mImfChannel.type) { - case Imf::HALF: return sizeof(::half); - case Imf::FLOAT: return sizeof(float); - case Imf::UINT: return sizeof(uint32_t); - default: - throw runtime_error("Invalid pixel type encountered."); - } - } - - string mName; - Imf::Channel mImfChannel; - vector mData; - }; - // Allocate raw channels on the heap, because it'll be references // by nested parallel for coroutine. auto rawChannels = std::make_unique>(); From 63a25450bb3ea1fc33e17994299abae4c764cbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 06:15:56 +0000 Subject: [PATCH 18/83] Add source of coLambda --- include/tev/ThreadPool.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 21ac8365c..c5b5e426c 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -180,6 +180,9 @@ void coCaptureVar(Task& task, auto* var) { }); } +// Ties the lifetime of a lambda coroutine's captures +// to that of the coroutine. +// Taken from https://stackoverflow.com/a/68630143 inline auto coLambda(auto&& executor) { return [executor=std::move(executor)](Args&&... args) { using ReturnType = decltype(executor(args...)); From 826d6a2b4e60be27331613bc82445960bcb2b9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 06:44:57 +0000 Subject: [PATCH 19/83] Implement Task in a separate file --- CMakeLists.txt | 1 + include/tev/Task.h | 186 +++++++++++++++++++++++++++++++++++++++ include/tev/ThreadPool.h | 181 +------------------------------------ src/Task.cpp | 10 +++ 4 files changed, 198 insertions(+), 180 deletions(-) create mode 100644 include/tev/Task.h create mode 100644 src/Task.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 87b5e47be..272c2716f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ set(TEV_SOURCES include/tev/Lazy.h src/Lazy.cpp include/tev/MultiGraph.h src/MultiGraph.cpp include/tev/SharedQueue.h src/SharedQueue.cpp + include/tev/Task.h src/Task.cpp include/tev/ThreadPool.h src/ThreadPool.cpp include/tev/UberShader.h src/UberShader.cpp diff --git a/include/tev/Task.h b/include/tev/Task.h new file mode 100644 index 000000000..ea17ca964 --- /dev/null +++ b/include/tev/Task.h @@ -0,0 +1,186 @@ +// This file was developed by Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#pragma once + +#include + +#include + +TEV_NAMESPACE_BEGIN + +// TODO: replace with std::latch when it's supported everywhere +class Latch { +public: + Latch(int val) : mCounter{val} {} + bool countDown() noexcept { + std::unique_lock lock{mMutex}; + bool result = (--mCounter == 0); + if (result) { + mCv.notify_all(); + } + return result; + } + + void wait() { + if (mCounter <= 0) { + return; + } + + std::unique_lock lock{mMutex}; + if (mCounter > 0) { + mCv.wait(lock); + } + } +private: + std::atomic mCounter; + std::mutex mMutex; + std::condition_variable mCv; +}; + +template +void waitAll(std::vector& futures) { + for (auto& f : futures) { + f.get(); + } +} + +template +struct TaskPromiseBase { + data_t data; + + // When the coroutine co_returns a value, this method is used to publish the result + void return_value(data_t value) noexcept { + data = std::move(value); + } +}; + +template <> +struct TaskPromiseBase { + void return_void() noexcept {} +}; + +template +struct TaskPromise : public TaskPromiseBase { + std::experimental::coroutine_handle<> precursor; + Latch latch{2}; + std::exception_ptr eptr; + std::vector> onFinalSuspend; + + future_t get_return_object() noexcept { + return {std::experimental::coroutine_handle>::from_promise(*this)}; + } + + std::experimental::suspend_never initial_suspend() const noexcept { return {}; } + + void unhandled_exception() { + eptr = std::current_exception(); + } + + template + void finally(F&& fun) { + onFinalSuspend.emplace_back(std::forward(fun)); + } + + // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). + // The awaiter returned here defines what happens next + auto final_suspend() const noexcept { + for (auto& f : onFinalSuspend) { + f(); + } + + struct awaiter { + // Return false here to return control to the thread's event loop. Remember that we're + // running on some async thread at this point. + bool await_ready() const noexcept { return false; } + + void await_resume() const noexcept {} + + // Returning a coroutine handle here resumes the coroutine it refers to (needed for + // continuation handling). If we wanted, we could instead enqueue that coroutine handle + // instead of immediately resuming it by enqueuing it and returning void. + std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle> h) const noexcept { + bool isLast = h.promise().latch.countDown(); + if (isLast && h.promise().precursor) { + return h.promise().precursor; + } + + return std::experimental::noop_coroutine(); + } + }; + + return awaiter{}; + } +}; + +template +struct Task { + using promise_type = TaskPromise, T>; + + // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) + std::experimental::coroutine_handle handle; + + // The following methods make our task type conform to the awaitable concept, so we can + // co_await for a task to complete + bool await_ready() const noexcept { + // No need to suspend if this task has no outstanding work + return handle.done(); + } + + template + void finally(F&& fun) { + handle.promise().finally(std::forward(fun)); + } + + T await_resume() const { + if (handle.promise().eptr) { + std::rethrow_exception(handle.promise().eptr); + } + + if constexpr (!std::is_void_v) { + // The returned value here is what `co_await our_task` evaluates to + return std::move(handle.promise().data); + } + } + + bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { + // The coroutine itself is being suspended (async work can beget other async work) + // Record the argument as the continuation point when this is resumed later. See + // the final_suspend awaiter on the promise_type above for where this gets used + handle.promise().precursor = coroutine; + return !handle.promise().latch.countDown(); + } + + void wait() const { + handle.promise().latch.countDown(); + handle.promise().latch.wait(); + } + + T get() const { + wait(); + if (handle.promise().eptr) { + std::rethrow_exception(handle.promise().eptr); + } + if constexpr (!std::is_void_v) { + return std::move(handle.promise().data); + } + } +}; + +// Ties the lifetime of a lambda coroutine's captures +// to that of the coroutine. +// Taken from https://stackoverflow.com/a/68630143 +auto coLambda(auto&& executor) { + return [executor=std::move(executor)](Args&&... args) { + using ReturnType = decltype(executor(args...)); + // copy the lambda into a new std::function pointer + auto exec = new std::function(executor); + // execute the lambda and save the result + auto result = (*exec)(args...); + // call custom method to save lambda until task ends + result.finally([exec]() { delete exec; }); + return result; + }; +} + +TEV_NAMESPACE_END \ No newline at end of file diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index c5b5e426c..a5ce744f1 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -16,186 +17,6 @@ TEV_NAMESPACE_BEGIN -class Latch { -public: - Latch(int val) : mCounter{val} {} - bool countDown() noexcept { - std::unique_lock lock{mMutex}; - bool result = (--mCounter == 0); - if (result) { - mCv.notify_all(); - } - return result; - } - - void wait() { - if (mCounter <= 0) { - return; - } - - std::unique_lock lock{mMutex}; - if (mCounter > 0) { - mCv.wait(lock); - } - } -private: - std::atomic mCounter; - std::mutex mMutex; - std::condition_variable mCv; -}; - -template -void waitAll(std::vector& futures) { - for (auto& f : futures) { - f.get(); - } -} - -template -struct TaskPromiseBase { - data_t data; - - // When the coroutine co_returns a value, this method is used to publish the result - void return_value(data_t value) noexcept { - data = std::move(value); - } -}; - -template <> -struct TaskPromiseBase { - void return_void() noexcept {} -}; - -template -struct TaskPromise : public TaskPromiseBase { - std::experimental::coroutine_handle<> precursor; - Latch latch{2}; - std::exception_ptr eptr; - std::vector> onFinalSuspend; - - future_t get_return_object() noexcept { - return {std::experimental::coroutine_handle>::from_promise(*this)}; - } - - std::experimental::suspend_never initial_suspend() const noexcept { return {}; } - - void unhandled_exception() { - eptr = std::current_exception(); - } - - template - void finally(F&& fun) { - onFinalSuspend.emplace_back(std::forward(fun)); - } - - // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). - // The awaiter returned here defines what happens next - auto final_suspend() const noexcept { - for (auto& f : onFinalSuspend) { - f(); - } - - struct awaiter { - // Return false here to return control to the thread's event loop. Remember that we're - // running on some async thread at this point. - bool await_ready() const noexcept { return false; } - - void await_resume() const noexcept {} - - // Returning a coroutine handle here resumes the coroutine it refers to (needed for - // continuation handling). If we wanted, we could instead enqueue that coroutine handle - // instead of immediately resuming it by enqueuing it and returning void. - std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle> h) const noexcept { - bool isLast = h.promise().latch.countDown(); - if (isLast && h.promise().precursor) { - return h.promise().precursor; - } - - return std::experimental::noop_coroutine(); - } - }; - - return awaiter{}; - } -}; - -template -struct Task { - using promise_type = TaskPromise, T>; - - // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) - std::experimental::coroutine_handle handle; - - // The following methods make our task type conform to the awaitable concept, so we can - // co_await for a task to complete - bool await_ready() const noexcept { - // No need to suspend if this task has no outstanding work - return handle.done(); - } - - template - void finally(F&& fun) { - handle.promise().finally(std::forward(fun)); - } - - T await_resume() const { - if (handle.promise().eptr) { - std::rethrow_exception(handle.promise().eptr); - } - - if constexpr (!std::is_void_v) { - // The returned value here is what `co_await our_task` evaluates to - return std::move(handle.promise().data); - } - } - - bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { - // The coroutine itself is being suspended (async work can beget other async work) - // Record the argument as the continuation point when this is resumed later. See - // the final_suspend awaiter on the promise_type above for where this gets used - handle.promise().precursor = coroutine; - return !handle.promise().latch.countDown(); - } - - void wait() const { - handle.promise().latch.countDown(); - handle.promise().latch.wait(); - } - - T get() const { - wait(); - if (handle.promise().eptr) { - std::rethrow_exception(handle.promise().eptr); - } - if constexpr (!std::is_void_v) { - return std::move(handle.promise().data); - } - } -}; - -template -void coCaptureVar(Task& task, auto* var) { - task.finally([var]() { - delete var; - }); -} - -// Ties the lifetime of a lambda coroutine's captures -// to that of the coroutine. -// Taken from https://stackoverflow.com/a/68630143 -inline auto coLambda(auto&& executor) { - return [executor=std::move(executor)](Args&&... args) { - using ReturnType = decltype(executor(args...)); - // copy the lambda into a new std::function pointer - auto exec = new std::function(executor); - // execute the lambda and save the result - auto result = (*exec)(args...); - // call custom method to save lambda until task ends - coCaptureVar(result, exec); - return result; - }; -} - class ThreadPool { public: ThreadPool(); diff --git a/src/Task.cpp b/src/Task.cpp new file mode 100644 index 000000000..106791772 --- /dev/null +++ b/src/Task.cpp @@ -0,0 +1,10 @@ +// This file was developed by Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#include + +using namespace std; + +TEV_NAMESPACE_BEGIN + +TEV_NAMESPACE_END From aa7fd57f0c866ac4a145ac98a3914ee45c996606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 7 Aug 2021 06:45:18 +0000 Subject: [PATCH 20/83] Move Channel::*async implementations to .cpp --- include/tev/Channel.h | 18 +++--------------- src/Channel.cpp | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/include/tev/Channel.h b/include/tev/Channel.h index c1d79a4fd..15b439fb2 100644 --- a/include/tev/Channel.h +++ b/include/tev/Channel.h @@ -4,7 +4,7 @@ #pragma once #include -#include +#include #include @@ -70,21 +70,9 @@ class Channel { return {mData.cols(), mData.rows()}; } - auto divideByAsync(const Channel& other, int priority) { - return gThreadPool->parallelForAsync(0, other.count(), [&](Eigen::DenseIndex i) { - if (other.at(i) != 0) { - at(i) /= other.at(i); - } else { - at(i) = 0; - } - }, priority); - } + Task divideByAsync(const Channel& other, int priority); - auto multiplyWithAsync(const Channel& other, int priority) { - return gThreadPool->parallelForAsync(0, other.count(), [&](Eigen::DenseIndex i) { - at(i) *= other.at(i); - }, priority); - } + Task multiplyWithAsync(const Channel& other, int priority); void setZero() { mData.setZero(); } diff --git a/src/Channel.cpp b/src/Channel.cpp index a363bba57..97a5a737b 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -17,6 +17,22 @@ Channel::Channel(const std::string& name, Eigen::Vector2i size) mData.resize(size.y(), size.x()); } +Task Channel::divideByAsync(const Channel& other, int priority) { + co_await gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { + if (other.at(i) != 0) { + at(i) /= other.at(i); + } else { + at(i) = 0; + } + }, priority); +} + +Task Channel::multiplyWithAsync(const Channel& other, int priority) { + co_await gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { + at(i) *= other.at(i); + }, priority); +} + pair Channel::split(const string& channel) { size_t dotPosition = channel.rfind("."); if (dotPosition != string::npos) { From 94e9e9d46d7d5bc009ec1361c01f5cc3dcbb4868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 7 Aug 2021 19:38:11 +0200 Subject: [PATCH 21/83] Fix windows compilation - Update Eigen to fix outdated use of std::result_of - Include instead of - Fix broken DdsImageLoader --- dependencies/eigen | 2 +- include/tev/Task.h | 24 ++++++++++++++++-------- include/tev/ThreadPool.h | 10 +++++++++- src/imageio/DdsImageLoader.cpp | 4 ++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/dependencies/eigen b/dependencies/eigen index a45d28256..4e0357c6d 160000 --- a/dependencies/eigen +++ b/dependencies/eigen @@ -1 +1 @@ -Subproject commit a45d28256d020a4e871267c9bf00206fe9d2265e +Subproject commit 4e0357c6dd6fa4f024362f3affdcac6b24253815 diff --git a/include/tev/Task.h b/include/tev/Task.h index ea17ca964..37ad72205 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -5,7 +5,15 @@ #include +#ifdef __APPLE__ #include +#define COROUTINE_NAMESPACE COROUTINE_NAMESPACE +#else +#include +#define COROUTINE_NAMESPACE std +#endif + +#include TEV_NAMESPACE_BEGIN @@ -62,16 +70,16 @@ struct TaskPromiseBase { template struct TaskPromise : public TaskPromiseBase { - std::experimental::coroutine_handle<> precursor; + COROUTINE_NAMESPACE::coroutine_handle<> precursor; Latch latch{2}; std::exception_ptr eptr; std::vector> onFinalSuspend; future_t get_return_object() noexcept { - return {std::experimental::coroutine_handle>::from_promise(*this)}; + return {COROUTINE_NAMESPACE::coroutine_handle>::from_promise(*this)}; } - std::experimental::suspend_never initial_suspend() const noexcept { return {}; } + COROUTINE_NAMESPACE::suspend_never initial_suspend() const noexcept { return {}; } void unhandled_exception() { eptr = std::current_exception(); @@ -99,13 +107,13 @@ struct TaskPromise : public TaskPromiseBase { // Returning a coroutine handle here resumes the coroutine it refers to (needed for // continuation handling). If we wanted, we could instead enqueue that coroutine handle // instead of immediately resuming it by enqueuing it and returning void. - std::experimental::coroutine_handle<> await_suspend(std::experimental::coroutine_handle> h) const noexcept { + COROUTINE_NAMESPACE::coroutine_handle<> await_suspend(COROUTINE_NAMESPACE::coroutine_handle> h) const noexcept { bool isLast = h.promise().latch.countDown(); if (isLast && h.promise().precursor) { return h.promise().precursor; } - return std::experimental::noop_coroutine(); + return COROUTINE_NAMESPACE::noop_coroutine(); } }; @@ -118,7 +126,7 @@ struct Task { using promise_type = TaskPromise, T>; // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) - std::experimental::coroutine_handle handle; + COROUTINE_NAMESPACE::coroutine_handle handle; // The following methods make our task type conform to the awaitable concept, so we can // co_await for a task to complete @@ -143,7 +151,7 @@ struct Task { } } - bool await_suspend(std::experimental::coroutine_handle<> coroutine) const noexcept { + bool await_suspend(COROUTINE_NAMESPACE::coroutine_handle<> coroutine) const noexcept { // The coroutine itself is being suspended (async work can beget other async work) // Record the argument as the continuation point when this is resumed later. See // the final_suspend awaiter on the promise_type above for where this gets used @@ -183,4 +191,4 @@ auto coLambda(auto&& executor) { }; } -TEV_NAMESPACE_END \ No newline at end of file +TEV_NAMESPACE_END diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index a5ce744f1..2f6dee0fa 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -13,7 +13,15 @@ #include #include +#ifdef __APPLE__ #include +#define COROUTINE_NAMESPACE COROUTINE_NAMESPACE +#else +#include +#define COROUTINE_NAMESPACE std +#endif + + TEV_NAMESPACE_BEGIN @@ -55,7 +63,7 @@ class ThreadPool { // Since await_ready() always returns false, when suspend is called, we will // always immediately suspend and call this function (which enqueues the coroutine // for immediate reactivation on a different thread) - void await_suspend(std::experimental::coroutine_handle<> coroutine) noexcept { + void await_suspend(COROUTINE_NAMESPACE::coroutine_handle<> coroutine) noexcept { mPool->enqueueTask(coroutine, mPriority); } diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index a41651823..2ab78d68f 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -226,7 +226,7 @@ Task> DdsImageLoader::load(istream& iStream, const p for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; } - }); + }, priority); } else { // Ideally, we'd be able to assume that only *_SRGB format images were in // sRGB space, and only they need to converted to linear. However, @@ -242,7 +242,7 @@ Task> DdsImageLoader::load(istream& iStream, const p channels[c].at(i) = toLinear(typedData[baseIdx + c]); } } - }); + }, priority); } vector> matches; From 67e0ea9946127554d41a4ab3ac8bbf8f67be352d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 7 Aug 2021 19:38:53 +0200 Subject: [PATCH 22/83] Fix incorrect use of std::priority_queue in background image loading --- src/Image.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Image.cpp b/src/Image.cpp index 7813d7b97..7458d77b1 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -472,7 +472,7 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s } auto image = make_shared(imageId, path, std::move(data), channelSelector); - + auto end = chrono::system_clock::now(); chrono::duration elapsedSeconds = end - start; @@ -503,7 +503,7 @@ Task> tryLoadImage(path path, istream& iStream, string channel Task> tryLoadImage(int imageId, path path, string channelSelector) { try { path = path.make_absolute(); - } catch (const runtime_error& e) { + } catch (const runtime_error&) { // If for some strange reason we can not obtain an absolute path, let's still // try to open the image at the given path just to make sure. } @@ -519,7 +519,7 @@ Task> tryLoadImage(path path, string channelSelector) { void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { int imageId = Image::drawId(); int loadId = mUnsortedLoadCounter++; - + gThreadPool->enqueueTask(coLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { auto image = co_await tryLoadImage(imageId, path, channelSelector); @@ -537,7 +537,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele bool BackgroundImagesLoader::publishSortedLoads() { std::lock_guard lock{mPendingLoadedImagesMutex}; bool pushed = false; - while (mPendingLoadedImages.top().loadId == mLoadCounter) { + while (!mPendingLoadedImages.empty() && mPendingLoadedImages.top().loadId == mLoadCounter) { ++mLoadCounter; // null image pointers indicate failed loads. These shouldn't be pushed. From 9116dc175f7d7301d671e38cec77ed16e8f0ed06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 14:21:57 +0200 Subject: [PATCH 23/83] Disable irrelevant warnings from certain dependencies on Windows --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 272c2716f..bee188cac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,10 @@ if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") endif() + # Disable warnings that are present in dependencies and irrelevant to us + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4100") # unused arguments + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd5054") # deprecated enum & operator + # To allow for wildcards in command-line path arguments on windows, # we need to link to wsetargv.obj # http://msdn.microsoft.com/en-us/library/8bch7bkk.aspx From bda322092a5ae0ce4820d53f2344e3c3407efb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 14:23:49 +0200 Subject: [PATCH 24/83] Fix coroutine include/ifdef --- include/tev/Task.h | 2 +- include/tev/ThreadPool.h | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index 37ad72205..cca01e79f 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -7,7 +7,7 @@ #ifdef __APPLE__ #include -#define COROUTINE_NAMESPACE COROUTINE_NAMESPACE +#define COROUTINE_NAMESPACE std::experimental #else #include #define COROUTINE_NAMESPACE std diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 2f6dee0fa..d408b7487 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -13,15 +13,6 @@ #include #include -#ifdef __APPLE__ -#include -#define COROUTINE_NAMESPACE COROUTINE_NAMESPACE -#else -#include -#define COROUTINE_NAMESPACE std -#endif - - TEV_NAMESPACE_BEGIN From 1df3f5f13bdc86d12ffdacee0412bc3fae0bf165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 14:35:03 +0200 Subject: [PATCH 25/83] Use coroutine parallelFor in other-than-exr image loaders --- src/imageio/ClipboardImageLoader.cpp | 2 +- src/imageio/DdsImageLoader.cpp | 4 ++-- src/imageio/PfmImageLoader.cpp | 2 +- src/imageio/StbiImageLoader.cpp | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index 6cdbf0670..ec88ab913 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -83,7 +83,7 @@ Task> ClipboardImageLoader::load(istream& iStream, c // clip doesn't properly handle this... so copy&pasting transparent images // from browsers tends to produce incorrect color values in alpha!=1/0 regions. bool premultipliedAlpha = false && numChannels >= 4; - gThreadPool->parallelFor(0, size.y(), [&](DenseIndex y) { + co_await gThreadPool->parallelForAsync(0, size.y(), [&](DenseIndex y) { for (int x = 0; x < size.x(); ++x) { int baseIdx = y * numBytesPerRow + x * numChannels; for (int c = numChannels-1; c >= 0; --c) { diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index 2ab78d68f..fd20722d7 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -221,7 +221,7 @@ Task> DdsImageLoader::load(istream& iStream, const p assert(!DirectX::IsSRGB(metadata.format)); // Assume that the image data is already in linear space. auto typedData = reinterpret_cast(scratchImage.GetPixels()); - gThreadPool->parallelFor(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; @@ -233,7 +233,7 @@ Task> DdsImageLoader::load(istream& iStream, const p // RGB(A) DDS images tend to be in sRGB space, even those not // explicitly stored in an *_SRGB format. auto typedData = reinterpret_cast(scratchImage.GetPixels()); - gThreadPool->parallelFor(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { int baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == 3) { diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index bd0606f35..7271f4de6 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -72,7 +72,7 @@ Task> PfmImageLoader::load(istream& iStream, const p // Reverse bytes of every float if endianness does not match up with system const bool shallSwapBytes = isSystemLittleEndian() != isPfmLittleEndian; - gThreadPool->parallelFor(0, size.y(), [&](DenseIndex y) { + co_await gThreadPool->parallelForAsync(0, size.y(), [&](DenseIndex y) { for (int x = 0; x < size.x(); ++x) { int baseIdx = (y * size.x() + x) * numChannels; for (int c = 0; c < numChannels; ++c) { diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index 597846d2a..e6dc6c619 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -67,7 +67,7 @@ Task> StbiImageLoader::load(istream& iStream, const auto numPixels = (DenseIndex)size.x() * size.y(); if (isHdr) { auto typedData = reinterpret_cast(data); - gThreadPool->parallelFor(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { int baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; @@ -75,7 +75,7 @@ Task> StbiImageLoader::load(istream& iStream, const }, priority); } else { auto typedData = reinterpret_cast(data); - gThreadPool->parallelFor(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { int baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == alphaChannelIndex) { From 90570231529c3986ae6b5928dae751ec30d2f2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 14:41:19 +0200 Subject: [PATCH 26/83] Credits to Jeremy's blog post about cpp20 task pools --- include/tev/Task.h | 16 ++++++---------- include/tev/ThreadPool.h | 6 +----- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index cca01e79f..a6542a3da 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -53,6 +53,8 @@ void waitAll(std::vector& futures) { } } +// The task implementation is inspired by a sketch from the following blog post: +// https://www.jeremyong.com/cpp/2021/01/04/cpp20-coroutines-a-minimal-async-framework/ template struct TaskPromiseBase { data_t data; @@ -97,16 +99,12 @@ struct TaskPromise : public TaskPromiseBase { f(); } - struct awaiter { - // Return false here to return control to the thread's event loop. Remember that we're - // running on some async thread at this point. + struct Awaiter { bool await_ready() const noexcept { return false; } - void await_resume() const noexcept {} - // Returning a coroutine handle here resumes the coroutine it refers to (needed for - // continuation handling). If we wanted, we could instead enqueue that coroutine handle - // instead of immediately resuming it by enqueuing it and returning void. + // Returning the parent coroutine has the effect of destroying this coroutine handle + // and continuing execution where the parent co_await'ed us. COROUTINE_NAMESPACE::coroutine_handle<> await_suspend(COROUTINE_NAMESPACE::coroutine_handle> h) const noexcept { bool isLast = h.promise().latch.countDown(); if (isLast && h.promise().precursor) { @@ -117,7 +115,7 @@ struct TaskPromise : public TaskPromiseBase { } }; - return awaiter{}; + return Awaiter{}; } }; @@ -128,8 +126,6 @@ struct Task { // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) COROUTINE_NAMESPACE::coroutine_handle handle; - // The following methods make our task type conform to the awaitable concept, so we can - // co_await for a task to complete bool await_ready() const noexcept { // No need to suspend if this task has no outstanding work return handle.done(); diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index d408b7487..4c66d5156 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -47,13 +47,9 @@ class ThreadPool { Awaiter(ThreadPool* pool, int priority) : mPool{pool}, mPriority{priority} {} - // Unlike the OS event case, there's no case where we suspend and the work - // is immediately ready bool await_ready() const noexcept { return false; } - // Since await_ready() always returns false, when suspend is called, we will - // always immediately suspend and call this function (which enqueues the coroutine - // for immediate reactivation on a different thread) + // Suspend and enqueue coroutine continuation onto the threadpool void await_suspend(COROUTINE_NAMESPACE::coroutine_handle<> coroutine) noexcept { mPool->enqueueTask(coroutine, mPriority); } From 68764a7a5cce593334798bdfc3759ffa099f70ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 10 Aug 2021 16:51:42 +0200 Subject: [PATCH 27/83] Move away from Eigen during imageio due to alignment issues with coroutine state on the heap In the future, tev should entirely rid itself of Eigen. There isn't any need for heavy linear algebra, so nanogui's vector and matrix classes should be more than sufficient --- include/tev/Channel.h | 63 +++++++++++++++---------- include/tev/Image.h | 10 ++-- include/tev/ImageCanvas.h | 4 +- include/tev/imageio/ExrImageSaver.h | 2 +- include/tev/imageio/ImageLoader.h | 4 +- include/tev/imageio/ImageSaver.h | 4 +- include/tev/imageio/StbiHdrImageSaver.h | 2 +- include/tev/imageio/StbiLdrImageSaver.h | 2 +- src/Channel.cpp | 11 +++-- src/Image.cpp | 30 ++++++------ src/ImageCanvas.cpp | 41 ++++++++-------- src/ImageViewer.cpp | 10 ++-- src/imageio/ClipboardImageLoader.cpp | 10 ++-- src/imageio/EmptyImageLoader.cpp | 4 +- src/imageio/ExrImageLoader.cpp | 4 +- src/imageio/ExrImageSaver.cpp | 2 +- src/imageio/ImageLoader.cpp | 4 +- src/imageio/PfmImageLoader.cpp | 8 ++-- src/imageio/StbiHdrImageSaver.cpp | 2 +- src/imageio/StbiImageLoader.cpp | 18 +++---- src/imageio/StbiLdrImageSaver.cpp | 2 +- 21 files changed, 124 insertions(+), 113 deletions(-) diff --git a/include/tev/Channel.h b/include/tev/Channel.h index 15b439fb2..c8465026f 100644 --- a/include/tev/Channel.h +++ b/include/tev/Channel.h @@ -8,8 +8,6 @@ #include -#include - #include #include #include @@ -18,63 +16,77 @@ TEV_NAMESPACE_BEGIN class Channel { public: - using RowMatrixXf = Eigen::Matrix; - - Channel(const std::string& name, Eigen::Vector2i size); + Channel(const std::string& name, nanogui::Vector2i size); const std::string& name() const { return mName; } - const RowMatrixXf& data() const { + const std::vector& data() const { return mData; } - float eval(Eigen::DenseIndex index) const { + float eval(size_t index) const { if (index >= mData.size()) { return 0; } - return mData(index); + return mData[index]; } - float eval(Eigen::Vector2i index) const { - if (index.x() < 0 || index.x() >= mData.cols() || - index.y() < 0 || index.y() >= mData.rows()) { + float eval(nanogui::Vector2i index) const { + if (index.x() < 0 || index.x() >= mCols || + index.y() < 0 || index.y() >= mRows) { return 0; } - return mData(index.x() + index.y() * mData.cols()); + return mData[index.x() + index.y() * mCols]; } - float& at(Eigen::DenseIndex index) { - return mData(index); + float& at(size_t index) { + return mData[index]; } - float at(Eigen::DenseIndex index) const { - return mData(index); + float at(size_t index) const { + return mData[index]; } - float& at(Eigen::Vector2i index) { - return at(index.x() + index.y() * mData.cols()); + float& at(nanogui::Vector2i index) { + return at(index.x() + index.y() * mCols); } - float at(Eigen::Vector2i index) const { - return at(index.x() + index.y() * mData.cols()); + float at(nanogui::Vector2i index) const { + return at(index.x() + index.y() * mCols); } - Eigen::DenseIndex count() const { + size_t count() const { return mData.size(); } - Eigen::Vector2i size() const { - return {mData.cols(), mData.rows()}; + nanogui::Vector2i size() const { + return {mCols, mRows}; + } + + std::tuple minMaxMean() const { + float min = std::numeric_limits::infinity(); + float max = -std::numeric_limits::infinity(); + float mean = 0; + for (float f : mData) { + mean += f; + if (f < min) { + min = f; + } + if (f > max) { + max = f; + } + } + return {min, max, mean/count()}; } Task divideByAsync(const Channel& other, int priority); Task multiplyWithAsync(const Channel& other, int priority); - void setZero() { mData.setZero(); } + void setZero() { std::memset(mData.data(), 0, mData.size()*sizeof(float)); } void updateTile(int x, int y, int width, int height, const std::vector& newData); @@ -89,7 +101,8 @@ class Channel { private: std::string mName; - RowMatrixXf mData; + int mCols, mRows; + std::vector mData; }; TEV_NAMESPACE_END diff --git a/include/tev/Image.h b/include/tev/Image.h index d1cb54e3f..92c2df387 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -9,8 +9,6 @@ #include -#include - #include #include #include @@ -27,11 +25,11 @@ struct ImageData { std::vector layers; nanogui::Matrix4f toRec709 = nanogui::Matrix4f{1.0f}; // Identity by default - Eigen::Vector2i size() const { + nanogui::Vector2i size() const { return channels.front().size(); } - Eigen::DenseIndex count() const { + size_t count() const { return channels.front().count(); } @@ -121,11 +119,11 @@ class Image { std::vector channelsInGroup(const std::string& groupName) const; std::vector getSortedChannels(const std::string& layerName) const; - Eigen::Vector2i size() const { + nanogui::Vector2i size() const { return mData.size(); } - Eigen::DenseIndex count() const { + size_t count() const { return mData.count(); } diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index c9e14132d..e553d075b 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -63,7 +63,7 @@ class ImageCanvas : public nanogui::Canvas { mRequestedChannelGroup = groupName; } - Eigen::Vector2i getImageCoords(const Image& image, Eigen::Vector2i mousePos); + nanogui::Vector2i getImageCoords(const Image& image, Eigen::Vector2i mousePos); void getValuesAtNanoPos(Eigen::Vector2i nanoPos, std::vector& result, const std::vector& channels); std::vector getValuesAtNanoPos(Eigen::Vector2i nanoPos, const std::vector& channels) { @@ -151,7 +151,7 @@ class ImageCanvas : public nanogui::Canvas { int priority ); - Eigen::Vector2f pixelOffset(const Eigen::Vector2i& size) const; + Eigen::Vector2f pixelOffset(const nanogui::Vector2i& size) const; // Assembles the transform from canonical space to // the [-1, 1] square for the current image. diff --git a/include/tev/imageio/ExrImageSaver.h b/include/tev/imageio/ExrImageSaver.h index 534013af7..2810bb860 100644 --- a/include/tev/imageio/ExrImageSaver.h +++ b/include/tev/imageio/ExrImageSaver.h @@ -11,7 +11,7 @@ TEV_NAMESPACE_BEGIN class ExrImageSaver : public TypedImageSaver { public: - void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const Eigen::Vector2i& imageSize, int nChannels) const override; + void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; bool hasPremultipliedAlpha() const override { return true; diff --git a/include/tev/imageio/ImageLoader.h b/include/tev/imageio/ImageLoader.h index 45d77af40..6dec1a92e 100644 --- a/include/tev/imageio/ImageLoader.h +++ b/include/tev/imageio/ImageLoader.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include @@ -29,7 +29,7 @@ class ImageLoader { static const std::vector>& getLoaders(); protected: - static std::vector makeNChannels(int numChannels, Eigen::Vector2i size); + static std::vector makeNChannels(int numChannels, const nanogui::Vector2i& size); }; TEV_NAMESPACE_END diff --git a/include/tev/imageio/ImageSaver.h b/include/tev/imageio/ImageSaver.h index d3d71aa34..255292128 100644 --- a/include/tev/imageio/ImageSaver.h +++ b/include/tev/imageio/ImageSaver.h @@ -5,7 +5,7 @@ #include -#include +#include #include #include @@ -33,7 +33,7 @@ class ImageSaver { template class TypedImageSaver : public ImageSaver { public: - virtual void save(std::ostream& oStream, const ::filesystem::path& path, const std::vector& data, const Eigen::Vector2i& imageSize, int nChannels) const = 0; + virtual void save(std::ostream& oStream, const ::filesystem::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const = 0; }; TEV_NAMESPACE_END diff --git a/include/tev/imageio/StbiHdrImageSaver.h b/include/tev/imageio/StbiHdrImageSaver.h index fbc70f0f1..d563448c8 100644 --- a/include/tev/imageio/StbiHdrImageSaver.h +++ b/include/tev/imageio/StbiHdrImageSaver.h @@ -11,7 +11,7 @@ TEV_NAMESPACE_BEGIN class StbiHdrImageSaver : public TypedImageSaver { public: - void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const Eigen::Vector2i& imageSize, int nChannels) const override; + void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; bool hasPremultipliedAlpha() const override { return false; diff --git a/include/tev/imageio/StbiLdrImageSaver.h b/include/tev/imageio/StbiLdrImageSaver.h index ffa6b7f79..ab86a1acb 100644 --- a/include/tev/imageio/StbiLdrImageSaver.h +++ b/include/tev/imageio/StbiLdrImageSaver.h @@ -11,7 +11,7 @@ TEV_NAMESPACE_BEGIN class StbiLdrImageSaver : public TypedImageSaver { public: - void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const Eigen::Vector2i& imageSize, int nChannels) const override; + void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; bool hasPremultipliedAlpha() const override { return false; diff --git a/src/Channel.cpp b/src/Channel.cpp index 97a5a737b..bc1c82bf6 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -6,19 +6,20 @@ #include -using namespace Eigen; using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN -Channel::Channel(const std::string& name, Eigen::Vector2i size) +Channel::Channel(const std::string& name, nanogui::Vector2i size) : mName{name} { - mData.resize(size.y(), size.x()); + mCols = size.x(); + mRows = size.y(); + mData.resize((size_t)mCols * mRows); } Task Channel::divideByAsync(const Channel& other, int priority) { - co_await gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, other.count(), [&](size_t i) { if (other.at(i) != 0) { at(i) /= other.at(i); } else { @@ -28,7 +29,7 @@ Task Channel::divideByAsync(const Channel& other, int priority) { } Task Channel::multiplyWithAsync(const Channel& other, int priority) { - co_await gThreadPool->parallelForAsync(0, other.count(), [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, other.count(), [&](size_t i) { at(i) *= other.at(i); }, priority); } diff --git a/src/Image.cpp b/src/Image.cpp index 7458d77b1..779ed8c4a 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -13,8 +13,8 @@ #include #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -140,11 +140,11 @@ string Image::shortName() const { return result; } -nanogui::Texture* Image::texture(const string& channelGroupName) { +Texture* Image::texture(const string& channelGroupName) { return texture(channelsInGroup(channelGroupName)); } -nanogui::Texture* Image::texture(const vector& channelNames) { +Texture* Image::texture(const vector& channelNames) { string lookup = join(channelNames, ","); auto iter = mTextures.find(lookup); if (iter != end(mTextures)) { @@ -157,14 +157,14 @@ nanogui::Texture* Image::texture(const vector& channelNames) { } mTextures.emplace(lookup, ImageTexture{ - new nanogui::Texture{ - nanogui::Texture::PixelFormat::RGBA, - nanogui::Texture::ComponentFormat::Float32, + new Texture{ + Texture::PixelFormat::RGBA, + Texture::ComponentFormat::Float32, {size().x(), size().y()}, - nanogui::Texture::InterpolationMode::Trilinear, - nanogui::Texture::InterpolationMode::Nearest, - nanogui::Texture::WrapMode::ClampToEdge, - 1, nanogui::Texture::TextureFlags::ShaderRead, + Texture::InterpolationMode::Trilinear, + Texture::InterpolationMode::Nearest, + Texture::WrapMode::ClampToEdge, + 1, Texture::TextureFlags::ShaderRead, true, }, channelNames, @@ -186,14 +186,14 @@ nanogui::Texture* Image::texture(const vector& channelNames) { const auto& channelData = chan->data(); tasks.emplace_back( - gThreadPool->parallelForAsync(0, numPixels, [&channelData, &data, i](DenseIndex j) { - data[j * 4 + i] = channelData(j); + gThreadPool->parallelForAsync(0, numPixels, [&channelData, &data, i](size_t j) { + data[j * 4 + i] = channelData[j]; }, std::numeric_limits::max()) ); } else { float val = i == 3 ? 1 : 0; tasks.emplace_back( - gThreadPool->parallelForAsync(0, numPixels, [&data, val, i](DenseIndex j) { + gThreadPool->parallelForAsync(0, numPixels, [&data, val, i](size_t j) { data[j * 4 + i] = val; }, std::numeric_limits::max()) ); @@ -349,7 +349,7 @@ void Image::updateChannel(const string& channelName, int x, int y, int width, in continue; } - auto numPixels = width * height; + auto numPixels = (size_t)width * height; vector textureData(numPixels * 4); // Populate data for sub-region of the texture to be updated @@ -367,7 +367,7 @@ void Image::updateChannel(const string& channelName, int x, int y, int width, in } } else { float val = i == 3 ? 1 : 0; - for (DenseIndex j = 0; j < numPixels; ++j) { + for (size_t j = 0; j < numPixels; ++j) { textureData[j * 4 + i] = val; } } diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 3136671d9..f10dcc0d2 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -220,7 +220,7 @@ float ImageCanvas::applyExposureAndOffset(float value) const { return pow(2.0f, mExposure) * value + mOffset; } -Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i mousePos) { +nanogui::Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i mousePos) { Vector2f imagePos = textureToNanogui(&image).inverse() * mousePos.cast(); return { static_cast(floor(imagePos.x())), @@ -234,7 +234,7 @@ void ImageCanvas::getValuesAtNanoPos(Vector2i nanoPos, vector& result, co return; } - Vector2i imageCoords = getImageCoords(*mImage, nanoPos); + auto imageCoords = getImageCoords(*mImage, nanoPos); for (const auto& channel : channels) { const Channel* c = mImage->channel(channel); TEV_ASSERT(c, "Requested channel must exist."); @@ -243,7 +243,7 @@ void ImageCanvas::getValuesAtNanoPos(Vector2i nanoPos, vector& result, co // Subtract reference if it exists. if (mReference) { - Vector2i referenceCoords = getImageCoords(*mReference, nanoPos); + auto referenceCoords = getImageCoords(*mReference, nanoPos); auto referenceChannels = mReference->channelsInGroup(mRequestedChannelGroup); for (size_t i = 0; i < result.size(); ++i) { float reference = i < referenceChannels.size() ? @@ -305,7 +305,7 @@ float ImageCanvas::applyMetric(float image, float reference, EMetric metric) { } void ImageCanvas::fitImageToScreen(const Image& image) { - Vector2f nanoguiImageSize = image.size().cast() / mPixelRatio; + Vector2f nanoguiImageSize = Vector2f{image.size().x(), image.size().y()} / mPixelRatio; mTransform = Scaling(Vector2f{m_size.x(), m_size.y()}.cwiseQuotient(nanoguiImageSize).minCoeff()); } @@ -334,14 +334,14 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) gThreadPool->parallelFor(0, nChannelsToSave, [&channels, &result](int i) { const auto& channelData = channels[i].data(); - for (DenseIndex j = 0; j < channelData.size(); ++j) { - result[j * 4 + i] = channelData(j); + for (size_t j = 0; j < channelData.size(); ++j) { + result[j * 4 + i] = channelData[j]; } }, priority); // Manually set alpha channel to 1 if the image does not have one. if (nChannelsToSave < 4) { - for (DenseIndex i = 0; i < numPixels; ++i) { + for (size_t i = 0; i < numPixels; ++i) { result[i * 4 + 3] = 1; } } @@ -349,7 +349,7 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) // Divide alpha out if needed (for storing in non-premultiplied formats) if (divideAlpha) { gThreadPool->parallelFor(0, min(nChannelsToSave, 3), [&result,numPixels](int i) { - for (DenseIndex j = 0; j < numPixels; ++j) { + for (size_t j = 0; j < numPixels; ++j) { float alpha = result[j * 4 + 3]; if (alpha == 0) { result[j * 4 + i] = 0; @@ -399,7 +399,7 @@ void ImageCanvas::saveImage(const path& path) const { return; } - Vector2i imageSize = mImage->size(); + nanogui::Vector2i imageSize = mImage->size(); tlog::info() << "Saving currently displayed image as '" << path << "'."; auto start = chrono::system_clock::now(); @@ -501,13 +501,13 @@ vector ImageCanvas::channelsFromImages( if (!reference) { gThreadPool->parallelFor(0, (int)channelNames.size(), [&](int i) { const auto* chan = image->channel(channelNames[i]); - for (DenseIndex j = 0; j < chan->count(); ++j) { + for (size_t j = 0; j < chan->count(); ++j) { result[i].at(j) = chan->eval(j); } }, priority); } else { - Vector2i size = image->size(); - Vector2i offset = (reference->size() - size) / 2; + Vector2i size = Vector2i{image->size().x(), image->size().y()}; + Vector2i offset = (Vector2i{reference->size().x(), reference->size().y()} - size) / 2; auto referenceChannels = reference->channelsInGroup(requestedChannelGroup); gThreadPool->parallelFor(0, channelNames.size(), [&](size_t i) { @@ -589,9 +589,10 @@ Task> ImageCanvas::computeCanvasStatistics( for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; - mean += channel.data().mean(); - maximum = max(maximum, channel.data().maxCoeff()); - minimum = min(minimum, channel.data().minCoeff()); + auto [cmin, cmax, cmean] = channel.minMaxMean(); + mean += cmean; + maximum = max(maximum, cmax); + minimum = min(minimum, cmin); } auto result = make_shared(); @@ -639,7 +640,7 @@ Task> ImageCanvas::computeCanvasStatistics( for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; tasks.emplace_back( - gThreadPool->parallelForAsync(0, numElements, [&, i](DenseIndex j) { + gThreadPool->parallelForAsync(0, numElements, [&, i](size_t j) { indices(j, i) = valToBin(channel.eval(j)); }, priority) ); @@ -650,7 +651,7 @@ Task> ImageCanvas::computeCanvasStatistics( } co_await gThreadPool->parallelForAsync(0, nChannels, [&](int i) { - for (DenseIndex j = 0; j < numElements; ++j) { + for (size_t j = 0; j < numElements; ++j) { result->histogram(indices(j, i), i) += alphaChannel ? alphaChannel->eval(j) : 1; } }, priority); @@ -669,7 +670,7 @@ Task> ImageCanvas::computeCanvasStatistics( co_return result; } -Vector2f ImageCanvas::pixelOffset(const Vector2i& size) const { +Vector2f ImageCanvas::pixelOffset(const nanogui::Vector2i& size) const { // Translate by half of a pixel to avoid pixel boundaries aligning perfectly with texels. // The translation only needs to happen for axes with even resolution. Odd-resolution // axes are implicitly shifted by half a pixel due to the centering operation. @@ -693,7 +694,7 @@ Transform ImageCanvas::transform(const Image* image) { mTransform * Scaling(1.0f / mPixelRatio) * Translation2f(pixelOffset(image->size())) * - Scaling(image->size().cast()) * + Scaling(Vector2f{image->size().x(), image->size().y()}) * Translation2f(Vector2f::Constant(-0.5f)); } @@ -707,7 +708,7 @@ Transform ImageCanvas::textureToNanogui(const Image* image) { Translation2f(0.5f * Vector2f{m_size.x(), m_size.y()}) * mTransform * Scaling(1.0f / mPixelRatio) * - Translation2f(-0.5f * image->size().cast() + pixelOffset(image->size())); + Translation2f(-0.5f * Vector2f{image->size().x(), image->size().y()} + pixelOffset(image->size())); } TEV_NAMESPACE_END diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 1fd8a90f1..d8fc8b72c 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -1403,11 +1403,9 @@ void ImageViewer::normalizeExposureAndOffset() { float maximum = numeric_limits::min(); for (const auto& channelName : channels) { const auto& channel = mCurrentImage->channel(channelName); - for (DenseIndex i = 0; i < channel->count(); ++i) { - float val = channel->eval(i); - maximum = max(maximum, val); - minimum = min(minimum, val); - } + auto [cmin, cmax, cmean] = channel->minMaxMean(); + maximum = max(maximum, cmax); + minimum = min(minimum, cmin); } float factor = 1.0f / (maximum - minimum); @@ -1779,7 +1777,7 @@ void ImageViewer::updateTitle() { auto rel = mouse_pos() - mImageCanvas->position(); vector values = mImageCanvas->getValuesAtNanoPos({rel.x(), rel.y()}, channels); - Eigen::Vector2i imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); + nanogui::Vector2i imageCoords = mImageCanvas->getImageCoords(*mCurrentImage, {rel.x(), rel.y()}); TEV_ASSERT(values.size() >= channelTails.size(), "Should obtain a value for every existing channel."); string valuesString; diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index ec88ab913..7ea90e216 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -6,8 +6,8 @@ #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -41,9 +41,9 @@ Task> ClipboardImageLoader::load(istream& iStream, c throw invalid_argument{tfm::format("Not sufficient bytes to read image spec (%d vs %d)", iStream.gcount(), sizeof(clip::image_spec))}; } - Vector2i size{spec.width, spec.height}; + Vector2i size{(int)spec.width, (int)spec.height}; - auto numPixels = (DenseIndex)size.x() * size.y(); + auto numPixels = (size_t)size.x() * size.y(); if (numPixels == 0) { throw invalid_argument{"Image has zero pixels."}; } @@ -55,7 +55,7 @@ Task> ClipboardImageLoader::load(istream& iStream, c auto numBytesPerRow = numChannels * size.x(); - auto numBytes = (DenseIndex)numBytesPerRow * size.y(); + auto numBytes = (size_t)numBytesPerRow * size.y(); int alphaChannelIndex = 3; vector channels = makeNChannels(numChannels, size); @@ -83,7 +83,7 @@ Task> ClipboardImageLoader::load(istream& iStream, c // clip doesn't properly handle this... so copy&pasting transparent images // from browsers tends to produce incorrect color values in alpha!=1/0 regions. bool premultipliedAlpha = false && numChannels >= 4; - co_await gThreadPool->parallelForAsync(0, size.y(), [&](DenseIndex y) { + co_await gThreadPool->parallelForAsync(0, size.y(), [&](int y) { for (int x = 0; x < size.x(); ++x) { int baseIdx = y * numBytesPerRow + x * numChannels; for (int c = numChannels-1; c >= 0; --c) { diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index d874f02c3..29a5e8af0 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -5,8 +5,8 @@ #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -34,7 +34,7 @@ Task> EmptyImageLoader::load(istream& iStream, const throw invalid_argument{tfm::format("Invalid magic empty string %s", magic)}; } - auto numPixels = (DenseIndex)size.x() * size.y(); + auto numPixels = (size_t)size.x() * size.y(); if (numPixels == 0) { throw invalid_argument{"Image has zero pixels."}; } diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 1f2d08ecf..b86dc0088 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -16,8 +16,8 @@ #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -227,7 +227,7 @@ Task> ExrImageLoader::load(istream& iStream, const p } co_await gThreadPool->parallelForAsync(0, (int)rawChannels->size(), [c = rawChannels.get(), size](int i) { - c->at(i).resize((DenseIndex)size.x() * size.y()); + c->at(i).resize((size_t)size.x() * size.y()); }, priority); for (size_t i = 0; i < rawChannels->size(); ++i) { diff --git a/src/imageio/ExrImageSaver.cpp b/src/imageio/ExrImageSaver.cpp index 1f4e54090..843a3c438 100644 --- a/src/imageio/ExrImageSaver.cpp +++ b/src/imageio/ExrImageSaver.cpp @@ -13,8 +13,8 @@ #include #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN diff --git a/src/imageio/ImageLoader.cpp b/src/imageio/ImageLoader.cpp index efcc5d75f..e6eea56d9 100644 --- a/src/imageio/ImageLoader.cpp +++ b/src/imageio/ImageLoader.cpp @@ -11,7 +11,7 @@ # include #endif -using namespace Eigen; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -34,7 +34,7 @@ const vector>& ImageLoader::getLoaders() { return imageLoaders; } -vector ImageLoader::makeNChannels(int numChannels, Vector2i size) { +vector ImageLoader::makeNChannels(int numChannels, const Vector2i& size) { vector channels; if (numChannels > 1) { const vector channelNames = {"R", "G", "B", "A"}; diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index 7271f4de6..bafd8832c 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -4,8 +4,8 @@ #include #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -50,7 +50,7 @@ Task> PfmImageLoader::load(istream& iStream, const p vector channels = makeNChannels(numChannels, size); - auto numPixels = (DenseIndex)size.x() * size.y(); + auto numPixels = (size_t)size.x() * size.y(); if (numPixels == 0) { throw invalid_argument{"Image has zero pixels."}; } @@ -72,7 +72,7 @@ Task> PfmImageLoader::load(istream& iStream, const p // Reverse bytes of every float if endianness does not match up with system const bool shallSwapBytes = isSystemLittleEndian() != isPfmLittleEndian; - co_await gThreadPool->parallelForAsync(0, size.y(), [&](DenseIndex y) { + co_await gThreadPool->parallelForAsync(0, size.y(), [&](int y) { for (int x = 0; x < size.x(); ++x) { int baseIdx = (y * size.x() + x) * numChannels; for (int c = 0; c < numChannels; ++c) { @@ -85,7 +85,7 @@ Task> PfmImageLoader::load(istream& iStream, const p } // Flip image vertically due to PFM format - channels[c].at({x, size.y() - y - 1}) = scale * val; + channels[c].at({x, size.y() - (int)y - 1}) = scale * val; } } }, priority); diff --git a/src/imageio/StbiHdrImageSaver.cpp b/src/imageio/StbiHdrImageSaver.cpp index 2af099b34..c9f97c7f4 100644 --- a/src/imageio/StbiHdrImageSaver.cpp +++ b/src/imageio/StbiHdrImageSaver.cpp @@ -8,8 +8,8 @@ #include #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index e6dc6c619..70d1ef9ec 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -6,8 +6,8 @@ #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -61,21 +61,21 @@ Task> StbiImageLoader::load(istream& iStream, const ScopeGuard dataGuard{[data] { stbi_image_free(data); }}; - vector channels = makeNChannels(numChannels, size); + auto channels = makeNChannels(numChannels, size); int alphaChannelIndex = 3; - auto numPixels = (DenseIndex)size.x() * size.y(); + auto numPixels = (size_t)size.x() * size.y(); if (isHdr) { - auto typedData = reinterpret_cast(data); - co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { + auto typedData = reinterpret_cast(data); int baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; } }, priority); } else { - auto typedData = reinterpret_cast(data); - co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { + auto typedData = reinterpret_cast(data); int baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == alphaChannelIndex) { @@ -90,7 +90,7 @@ Task> StbiImageLoader::load(istream& iStream, const vector> matches; for (size_t i = 0; i < channels.size(); ++i) { size_t matchId; - if (matchesFuzzy(channels[i].name(), channelSelector, &matchId)) { + if (matchesFuzzy(channels.at(i).name(), channelSelector, &matchId)) { matches.emplace_back(matchId, i); } } @@ -100,7 +100,7 @@ Task> StbiImageLoader::load(istream& iStream, const } for (const auto& match : matches) { - result.channels.emplace_back(move(channels[match.second])); + result.channels.emplace_back(move(channels.at(match.second))); } // STBI can not load layers, so all channels simply reside diff --git a/src/imageio/StbiLdrImageSaver.cpp b/src/imageio/StbiLdrImageSaver.cpp index 9cbb86a3c..bb5adb75b 100644 --- a/src/imageio/StbiLdrImageSaver.cpp +++ b/src/imageio/StbiLdrImageSaver.cpp @@ -9,8 +9,8 @@ #include #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN From 7dd45b4866d74b949d90260ed65cc525add9d7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 17:02:56 +0200 Subject: [PATCH 28/83] Fix DdsImageLoader --- src/imageio/DdsImageLoader.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index fd20722d7..272bcc88c 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -6,8 +6,8 @@ #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN @@ -208,9 +208,9 @@ Task> DdsImageLoader::load(istream& iStream, const p std::swap(scratchImage, convertedImage); } - vector channels = makeNChannels(numChannels, { metadata.width, metadata.height }); + vector channels = makeNChannels(numChannels, { (int)metadata.width, (int)metadata.height }); - auto numPixels = (DenseIndex)metadata.width * metadata.height; + auto numPixels = (size_t)metadata.width * metadata.height; if (numPixels == 0) { throw invalid_argument{"DDS image has zero pixels."}; } @@ -221,7 +221,7 @@ Task> DdsImageLoader::load(istream& iStream, const p assert(!DirectX::IsSRGB(metadata.format)); // Assume that the image data is already in linear space. auto typedData = reinterpret_cast(scratchImage.GetPixels()); - co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; @@ -233,8 +233,8 @@ Task> DdsImageLoader::load(istream& iStream, const p // RGB(A) DDS images tend to be in sRGB space, even those not // explicitly stored in an *_SRGB format. auto typedData = reinterpret_cast(scratchImage.GetPixels()); - co_await gThreadPool->parallelForAsync(0, numPixels, [&](DenseIndex i) { - int baseIdx = i * numChannels; + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { + size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == 3) { channels[c].at(i) = typedData[baseIdx + c]; From f45a1b4181aa7ebbe161f05abe7ad1893089db88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 19:16:48 +0200 Subject: [PATCH 29/83] Goodbye, Eigen --- .gitmodules | 3 - CMakeLists.txt | 6 -- dependencies/CMakeLists.txt | 2 - dependencies/eigen | 1 - include/tev/Common.h | 2 +- include/tev/ImageCanvas.h | 38 ++++----- include/tev/MultiGraph.h | 15 ++-- include/tev/UberShader.h | 15 ++-- src/HelpWindow.cpp | 1 - src/ImageCanvas.cpp | 142 +++++++++++++++++--------------- src/ImageViewer.cpp | 7 +- src/Ipc.cpp | 19 ++--- src/MultiGraph.cpp | 18 ++-- src/UberShader.cpp | 58 +++++++------ src/imageio/StbiImageLoader.cpp | 4 +- 15 files changed, 157 insertions(+), 174 deletions(-) delete mode 160000 dependencies/eigen diff --git a/.gitmodules b/.gitmodules index 4e5870b02..521bbb1e1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,3 @@ [submodule "dependencies/nanogui"] path = dependencies/nanogui url = https://github.com/Tom94/nanogui-1 -[submodule "dependencies/eigen"] - path = dependencies/eigen - url = https://gitlab.com/libeigen/eigen.git diff --git a/CMakeLists.txt b/CMakeLists.txt index bee188cac..38364769c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,11 +44,6 @@ if (MSVC) # Parallel build set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") - # 32 bit windows - if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DEIGEN_DONT_ALIGN") - endif() - # Static build set(CompilerFlags CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE @@ -199,7 +194,6 @@ include_directories( ${ARGS_INCLUDE} ${CLIP_INCLUDE} ${DIRECTXTEX_INCLUDE} - ${EIGEN_INCLUDE} ${FILESYSTEM_INCLUDE} ${GLFW_INCLUDE} ${NANOGUI_EXTRA_INCS} diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index eb875b7df..66d2e9fdf 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -68,8 +68,6 @@ set(ARGS_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/args PARENT_SCOPE) set(CLIP_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/clip PARENT_SCOPE) -set(EIGEN_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/eigen PARENT_SCOPE) - if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") set(GLFW_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nanogui/ext/glfw/include PARENT_SCOPE) endif() diff --git a/dependencies/eigen b/dependencies/eigen deleted file mode 160000 index 4e0357c6d..000000000 --- a/dependencies/eigen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e0357c6dd6fa4f024362f3affdcac6b24253815 diff --git a/include/tev/Common.h b/include/tev/Common.h index d7ede209b..81b7fe026 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -123,7 +123,7 @@ namespace nanogui { result.v[i] = accum; } } - return result / w; + return result;// / w; } template diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index e553d075b..68e2e4c4b 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -17,7 +17,8 @@ struct CanvasStatistics { float mean; float maximum; float minimum; - Eigen::MatrixXf histogram; + std::vector histogram; + int nChannels; int histogramZero; }; @@ -31,10 +32,11 @@ class ImageCanvas : public nanogui::Canvas { void draw(NVGcontext *ctx) override; - void translate(const Eigen::Vector2f& amount); - void scale(float amount, const Eigen::Vector2f& origin); + void translate(const nanogui::Vector2f& amount); + void scale(float amount, const nanogui::Vector2f& origin); float extractScale() const { - return std::sqrt(mTransform.linear().determinant()); + float det = mTransform.m[0][0] * mTransform.m[1][1] - mTransform.m[0][1] * mTransform.m[1][0]; + return std::sqrt(det); } void setExposure(float exposure) { @@ -63,10 +65,10 @@ class ImageCanvas : public nanogui::Canvas { mRequestedChannelGroup = groupName; } - nanogui::Vector2i getImageCoords(const Image& image, Eigen::Vector2i mousePos); + nanogui::Vector2i getImageCoords(const Image& image, nanogui::Vector2i mousePos); - void getValuesAtNanoPos(Eigen::Vector2i nanoPos, std::vector& result, const std::vector& channels); - std::vector getValuesAtNanoPos(Eigen::Vector2i nanoPos, const std::vector& channels) { + void getValuesAtNanoPos(nanogui::Vector2i nanoPos, std::vector& result, const std::vector& channels); + std::vector getValuesAtNanoPos(nanogui::Vector2i nanoPos, const std::vector& channels) { std::vector result; getValuesAtNanoPos(nanoPos, result, channels); return result; @@ -80,8 +82,8 @@ class ImageCanvas : public nanogui::Canvas { mTonemap = tonemap; } - static Eigen::Vector3f applyTonemap(const Eigen::Vector3f& value, float gamma, ETonemap tonemap); - Eigen::Vector3f applyTonemap(const Eigen::Vector3f& value) const { + static nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value, float gamma, ETonemap tonemap); + nanogui::Vector3f applyTonemap(const nanogui::Vector3f& value) const { return applyTonemap(value, mGamma, mTonemap); } @@ -124,16 +126,6 @@ class ImageCanvas : public nanogui::Canvas { std::shared_ptr>> canvasStatistics(); - static nanogui::Matrix3f toNanogui(const Eigen::Matrix3f& transform) { - nanogui::Matrix3f result; - for (int m = 0; m < 3; ++m) { - for (int n = 0; n < 3; ++n) { - result.m[n][m] = transform(m, n); - } - } - return result; - } - private: static std::vector channelsFromImages( std::shared_ptr image, @@ -151,12 +143,12 @@ class ImageCanvas : public nanogui::Canvas { int priority ); - Eigen::Vector2f pixelOffset(const nanogui::Vector2i& size) const; + nanogui::Vector2f pixelOffset(const nanogui::Vector2i& size) const; // Assembles the transform from canonical space to // the [-1, 1] square for the current image. - Eigen::Transform transform(const Image* image); - Eigen::Transform textureToNanogui(const Image* image); + nanogui::Matrix3f transform(const Image* image); + nanogui::Matrix3f textureToNanogui(const Image* image); float mPixelRatio = 1; float mExposure = 0; @@ -170,7 +162,7 @@ class ImageCanvas : public nanogui::Canvas { std::string mRequestedChannelGroup = ""; - Eigen::Transform mTransform = Eigen::Affine2f::Identity(); + nanogui::Matrix3f mTransform = nanogui::Matrix3f::scale(nanogui::Vector3f(1.0f)); std::unique_ptr mShader; diff --git a/include/tev/MultiGraph.h b/include/tev/MultiGraph.h index 9cfbb0f3d..ede82835e 100644 --- a/include/tev/MultiGraph.h +++ b/include/tev/MultiGraph.h @@ -9,8 +9,6 @@ #include -#include - TEV_NAMESPACE_BEGIN class MultiGraph : public nanogui::Widget { @@ -35,9 +33,11 @@ class MultiGraph : public nanogui::Widget { const nanogui::Color &textColor() const { return mTextColor; } void setTextColor(const nanogui::Color &textColor) { mTextColor = textColor; } - const Eigen::MatrixXf &values() const { return mValues; } - Eigen::MatrixXf &values() { return mValues; } - void setValues(const Eigen::MatrixXf &values) { mValues = values; } + const std::vector &values() const { return mValues; } + std::vector &values() { return mValues; } + void setValues(const std::vector &values) { mValues = values; } + + void setNChannels(int nChannels) { mNChannels = nChannels; } virtual nanogui::Vector2i preferred_size(NVGcontext *ctx) const override; virtual void draw(NVGcontext *ctx) override; @@ -61,11 +61,10 @@ class MultiGraph : public nanogui::Widget { protected: std::string mCaption, mHeader, mFooter; nanogui::Color mBackgroundColor, mForegroundColor, mTextColor; - Eigen::MatrixXf mValues; + std::vector mValues; + int mNChannels = 1; float mMinimum = 0, mMean = 0, mMaximum = 0; int mZeroBin = 0; -public: - EIGEN_MAKE_ALIGNED_OPERATOR_NEW }; TEV_NAMESPACE_END diff --git a/include/tev/UberShader.h b/include/tev/UberShader.h index 9586bafdc..30c581f49 100644 --- a/include/tev/UberShader.h +++ b/include/tev/UberShader.h @@ -5,8 +5,7 @@ #include #include - -#include +#include TEV_NAMESPACE_BEGIN @@ -16,12 +15,12 @@ class UberShader { virtual ~UberShader(); // Draws just a checkerboard. - void draw(const Eigen::Vector2f& pixelSize, const Eigen::Vector2f& checkerSize); + void draw(const nanogui::Vector2f& pixelSize, const nanogui::Vector2f& checkerSize); // Draws an image. void draw( - const Eigen::Vector2f& pixelSize, - const Eigen::Vector2f& checkerSize, + const nanogui::Vector2f& pixelSize, + const nanogui::Vector2f& checkerSize, nanogui::Texture* textureImage, const nanogui::Matrix3f& transformImage, float exposure, @@ -33,8 +32,8 @@ class UberShader { // Draws a difference between a reference and an image. void draw( - const Eigen::Vector2f& pixelSize, - const Eigen::Vector2f& checkerSize, + const nanogui::Vector2f& pixelSize, + const nanogui::Vector2f& checkerSize, nanogui::Texture* textureImage, const nanogui::Matrix3f& transformImage, nanogui::Texture* textureReference, @@ -56,7 +55,7 @@ class UberShader { } private: - void bindCheckerboardData(const Eigen::Vector2f& pixelSize, const Eigen::Vector2f& checkerSize); + void bindCheckerboardData(const nanogui::Vector2f& pixelSize, const nanogui::Vector2f& checkerSize); void bindImageData( nanogui::Texture* textureImage, diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 82ff7345c..1f6cdd276 100644 --- a/src/HelpWindow.cpp +++ b/src/HelpWindow.cpp @@ -164,7 +164,6 @@ HelpWindow::HelpWindow(Widget *parent, bool supportsHdr, function closeC addLibrary(about, "args", "", "Single-Header Argument Parsing Library"); addLibrary(about, "clip", "", "Cross-Platform Clipboard Library"); - addLibrary(about, "Eigen", "", "C++ Template Library for Linear Algebra"); addLibrary(about, "filesystem", "", "Lightweight Path Manipulation Library"); addLibrary(about, "Glad", "", "Multi-Language GL Loader-Generator"); addLibrary(about, "GLFW", "", "OpenGL Desktop Development Library"); diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index f10dcc0d2..b79316b9c 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -16,19 +16,19 @@ #include #include -using namespace Eigen; using namespace filesystem; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN -ImageCanvas::ImageCanvas(nanogui::Widget* parent, float pixelRatio) +ImageCanvas::ImageCanvas(Widget* parent, float pixelRatio) : Canvas{parent, 1, false, false, false}, mPixelRatio{pixelRatio} { mShader.reset(new UberShader{render_pass()}); set_draw_border(false); } -bool ImageCanvas::scroll_event(const nanogui::Vector2i& p, const nanogui::Vector2f& rel) { +bool ImageCanvas::scroll_event(const Vector2i& p, const Vector2f& rel) { if (Canvas::scroll_event(p, rel)) { return true; } @@ -43,7 +43,7 @@ bool ImageCanvas::scroll_event(const nanogui::Vector2i& p, const nanogui::Vector scaleAmount /= std::log2(1.1f); } - scale(scaleAmount, {p.x(), p.y()}); + scale(scaleAmount, Vector2f{p}); return true; } @@ -53,20 +53,20 @@ void ImageCanvas::draw_contents() { if (!image) { mShader->draw( - 2.0f * Vector2f{m_size.x(), m_size.y()}.cwiseInverse() / mPixelRatio, - Vector2f::Constant(20) + 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, + Vector2f{20.0f} ); return; } if (!mReference || glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) || image == mReference.get()) { mShader->draw( - 2.0f * Vector2f{m_size.x(), m_size.y()}.cwiseInverse() / mPixelRatio, - Vector2f::Constant(20), + 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, + Vector2f{20.0f}, image->texture(mRequestedChannelGroup), // The uber shader operates in [-1, 1] coordinates and requires the _inserve_ // image transform to obtain texture coordinates in [0, 1]-space. - toNanogui(transform(image).inverse().matrix()), + inverse(transform(image)), mExposure, mOffset, mGamma, @@ -77,14 +77,14 @@ void ImageCanvas::draw_contents() { } mShader->draw( - 2.0f * Vector2f{m_size.x(), m_size.y()}.cwiseInverse() / mPixelRatio, - Vector2f::Constant(20), + 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, + Vector2f{20.0f}, mImage->texture(mRequestedChannelGroup), // The uber shader operates in [-1, 1] coordinates and requires the _inserve_ // image transform to obtain texture coordinates in [0, 1]-space. - toNanogui(transform(mImage.get()).inverse().matrix()), + inverse(transform(mImage.get())), mReference->texture(mRequestedChannelGroup), - toNanogui(transform(mReference.get()).inverse().matrix()), + inverse(transform(mReference.get())), mExposure, mOffset, mGamma, @@ -95,16 +95,16 @@ void ImageCanvas::draw_contents() { } void ImageCanvas::draw(NVGcontext *ctx) { - nanogui::Canvas::draw(ctx); + Canvas::draw(ctx); if (mImage) { auto texToNano = textureToNanogui(mImage.get()); - auto nanoToTex = texToNano.inverse(); + auto nanoToTex = inverse(texToNano); - Vector2f pixelSize = texToNano * Vector2f::Ones() - texToNano * Vector2f::Zero(); + Vector2f pixelSize = texToNano * Vector2f{1.0f} - texToNano * Vector2f{0.0f}; - Vector2f topLeft = (nanoToTex * Vector2f::Zero()); - Vector2f bottomRight = (nanoToTex * Vector2f{m_size.x(), m_size.y()}); + Vector2f topLeft = (nanoToTex * Vector2f{0.0f}); + Vector2f bottomRight = (nanoToTex * Vector2f{m_size}); Vector2i startIndices = Vector2i{ static_cast(floor(topLeft.x())), @@ -121,7 +121,7 @@ void ImageCanvas::draw(NVGcontext *ctx) { // Remove duplicates channels.erase(unique(begin(channels), end(channels)), end(channels)); - vector colors; + vector colors; for (const auto& channel : channels) { colors.emplace_back(Channel::color(channel)); } @@ -143,7 +143,7 @@ void ImageCanvas::draw(NVGcontext *ctx) { vector values; for (cur.y() = startIndices.y(); cur.y() < endIndices.y(); ++cur.y()) { for (cur.x() = startIndices.x(); cur.x() < endIndices.x(); ++cur.x()) { - Vector2i nano = (texToNano * (cur.cast() + Vector2f::Constant(0.5f))).cast(); + Vector2i nano = Vector2i{texToNano * (Vector2f{cur} + Vector2f{0.5f})}; getValuesAtNanoPos(nano, values, channels); TEV_ASSERT(values.size() >= colors.size(), "Can not have more values than channels."); @@ -159,19 +159,19 @@ void ImageCanvas::draw(NVGcontext *ctx) { pos = Vector2f{ m_pos.x() + nano.x() + (i - 0.5f * (colors.size() - 1)) * fontSize * 0.88f, - m_pos.y() + nano.y(), + (float)m_pos.y() + nano.y(), }; } else { str = tfm::format("%.4f", values[i]); pos = Vector2f{ - m_pos.x() + nano.x(), + (float)m_pos.x() + nano.x(), m_pos.y() + nano.y() + (i - 0.5f * (colors.size() - 1)) * fontSize, }; } - nanogui::Color col = colors[i]; - nvgFillColor(ctx, nanogui::Color(col.r(), col.g(), col.b(), fontAlpha)); + Color col = colors[i]; + nvgFillColor(ctx, Color(col.r(), col.g(), col.b(), fontAlpha)); drawTextWithShadow(ctx, pos.x(), pos.y(), str, fontAlpha); } } @@ -179,7 +179,7 @@ void ImageCanvas::draw(NVGcontext *ctx) { } } - // If we're not in fullscreen mode draw an inner drop shadow. (adapted from nanogui::Window) + // If we're not in fullscreen mode draw an inner drop shadow. (adapted from Window) if (m_pos.x() != 0) { int ds = m_theme->m_window_drop_shadow_size, cr = m_theme->m_window_corner_radius; NVGpaint shadowPaint = nvgBoxGradient( @@ -200,18 +200,18 @@ void ImageCanvas::draw(NVGcontext *ctx) { } void ImageCanvas::translate(const Vector2f& amount) { - mTransform = Translation2f(amount) * mTransform; + mTransform = Matrix3f::translate(amount) * mTransform; } void ImageCanvas::scale(float amount, const Vector2f& origin) { float scaleFactor = pow(1.1f, amount); // Use the current cursor position as the origin to scale around. - Vector2f offset = -(origin - Eigen::Vector2f(position().x(), position().y())) + 0.5f * Eigen::Vector2f(m_size.x(), m_size.y()); + Vector2f offset = -(origin - Vector2f{position()}) + 0.5f * Vector2f{m_size}; auto scaleTransform = - Translation2f(-offset) * - Scaling(scaleFactor) * - Translation2f(offset); + Matrix3f::translate(-offset) * + Matrix3f::scale(Vector2f{scaleFactor}) * + Matrix3f::translate(offset); mTransform = scaleTransform * mTransform; } @@ -220,8 +220,8 @@ float ImageCanvas::applyExposureAndOffset(float value) const { return pow(2.0f, mExposure) * value + mOffset; } -nanogui::Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i mousePos) { - Vector2f imagePos = textureToNanogui(&image).inverse() * mousePos.cast(); +Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i mousePos) { + Vector2f imagePos = inverse(textureToNanogui(&image)) * Vector2f{mousePos}; return { static_cast(floor(imagePos.x())), static_cast(floor(imagePos.y())), @@ -276,19 +276,19 @@ Vector3f ImageCanvas::applyTonemap(const Vector3f& value, float gamma, ETonemap return Vector3f{fcd[start], fcd[start + 1], fcd[start + 2]}; }; - result = falseColor(log2(value.mean() + 0.03125f) / 10 + 0.5f); + result = falseColor(log2(mean(value) + 0.03125f) / 10 + 0.5f); break; } case ETonemap::PositiveNegative: { - result = {-2.0f * value.cwiseMin(Vector3f::Zero()).mean(), 2.0f * value.cwiseMax(Vector3f::Zero()).mean(), 0.0f}; + result = {-2.0f * mean(min(value, Vector3f{0.0f})), 2.0f * mean(max(value, Vector3f{0.0f})), 0.0f}; break; } default: throw runtime_error{"Invalid tonemap selected."}; } - return result.cwiseMax(Vector3f::Zero()).cwiseMin(Vector3f::Ones()); + return min(max(result, Vector3f{0.0f}), Vector3f{1.0f}); } float ImageCanvas::applyMetric(float image, float reference, EMetric metric) { @@ -305,12 +305,12 @@ float ImageCanvas::applyMetric(float image, float reference, EMetric metric) { } void ImageCanvas::fitImageToScreen(const Image& image) { - Vector2f nanoguiImageSize = Vector2f{image.size().x(), image.size().y()} / mPixelRatio; - mTransform = Scaling(Vector2f{m_size.x(), m_size.y()}.cwiseQuotient(nanoguiImageSize).minCoeff()); + Vector2f nanoguiImageSize = Vector2f{image.size()} / mPixelRatio; + mTransform = Matrix3f::scale(Vector2f{m_size} / min(nanoguiImageSize.x(), nanoguiImageSize.y())); } void ImageCanvas::resetTransform() { - mTransform = Affine2f::Identity(); + mTransform = Matrix3f::scale(Vector2f{1.0f}); } std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) const { @@ -376,7 +376,7 @@ std::vector ImageCanvas::getLdrImageData(bool divideAlpha, int priority) c // Store as LDR image. result.resize(floatData.size()); - gThreadPool->parallelFor(0, numPixels, [&](DenseIndex i) { + gThreadPool->parallelFor(0, numPixels, [&](size_t i) { size_t start = 4 * i; Vector3f value = applyTonemap({ applyExposureAndOffset(floatData[start]), @@ -399,7 +399,7 @@ void ImageCanvas::saveImage(const path& path) const { return; } - nanogui::Vector2i imageSize = mImage->size(); + Vector2i imageSize = mImage->size(); tlog::info() << "Saving currently displayed image as '" << path << "'."; auto start = chrono::system_clock::now(); @@ -585,7 +585,9 @@ Task> ImageCanvas::computeCanvasStatistics( } } - int nChannels = alphaChannel ? (int)flattened.size() - 1 : (int)flattened.size(); + auto result = make_shared(); + + int nChannels = result->nChannels = alphaChannel ? (int)flattened.size() - 1 : (int)flattened.size(); for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; @@ -595,15 +597,13 @@ Task> ImageCanvas::computeCanvasStatistics( minimum = min(minimum, cmin); } - auto result = make_shared(); - result->mean = nChannels > 0 ? (mean / nChannels) : 0; result->maximum = maximum; result->minimum = minimum; // Now that we know the maximum and minimum value we can define our histogram bin size. static const int NUM_BINS = 400; - result->histogram = MatrixXf::Zero(NUM_BINS, nChannels); + result->histogram.resize(NUM_BINS*nChannels); // We're going to draw our histogram in log space. static const float addition = 0.001f; @@ -634,14 +634,14 @@ Task> ImageCanvas::computeCanvasStatistics( } auto numElements = image->count(); - Eigen::MatrixXi indices(numElements, nChannels); + std::vector indices(numElements*nChannels); vector> tasks; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; tasks.emplace_back( gThreadPool->parallelForAsync(0, numElements, [&, i](size_t j) { - indices(j, i) = valToBin(channel.eval(j)); + indices[j + i * numElements] = valToBin(channel.eval(j)); }, priority) ); } @@ -652,25 +652,33 @@ Task> ImageCanvas::computeCanvasStatistics( co_await gThreadPool->parallelForAsync(0, nChannels, [&](int i) { for (size_t j = 0; j < numElements; ++j) { - result->histogram(indices(j, i), i) += alphaChannel ? alphaChannel->eval(j) : 1; + result->histogram[indices[j + i * numElements] + i * NUM_BINS] += alphaChannel ? alphaChannel->eval(j) : 1; } }, priority); - for (int i = 0; i < NUM_BINS; ++i) { - result->histogram.row(i) /= binToVal(i + 1) - binToVal(i); + for (int i = 0; i < nChannels; ++i) { + for (int j = 0; j < NUM_BINS; ++j) { + result->histogram[j + i * NUM_BINS] /= binToVal(j + 1) - binToVal(j); + } } // Normalize the histogram according to the 10th-largest // element to avoid a couple spikes ruining the entire graph. - MatrixXf temp = result->histogram; - DenseIndex idx = temp.size() - 10; - nth_element(temp.data(), temp.data() + idx, temp.data() + temp.size()); - result->histogram /= max(temp(idx), 0.1f) * 1.3f; + auto tmp = result->histogram; + size_t idx = tmp.size() - 10; + nth_element(tmp.data(), tmp.data() + idx, tmp.data() + tmp.size()); + + float norm = 1.0f / (max(tmp[idx], 0.1f) * 1.3f); + for (int i = 0; i < nChannels; ++i) { + for (int j = 0; j < NUM_BINS; ++j) { + result->histogram[j + i * NUM_BINS] *= norm; + } + } co_return result; } -Vector2f ImageCanvas::pixelOffset(const nanogui::Vector2i& size) const { +Vector2f ImageCanvas::pixelOffset(const Vector2i& size) const { // Translate by half of a pixel to avoid pixel boundaries aligning perfectly with texels. // The translation only needs to happen for axes with even resolution. Odd-resolution // axes are implicitly shifted by half a pixel due to the centering operation. @@ -679,36 +687,36 @@ Vector2f ImageCanvas::pixelOffset(const nanogui::Vector2i& size) const { return Vector2f{ size.x() % 2 == 0 ? 0.5f : 0.0f, size.y() % 2 == 0 ? -0.5f : 0.0f, - } + Vector2f::Constant(0.1111111f); + } + Vector2f{0.1111111f}; } -Transform ImageCanvas::transform(const Image* image) { +Matrix3f ImageCanvas::transform(const Image* image) { if (!image) { - return Transform::Identity(); + return Matrix3f::scale(Vector2f{1.0f}); } // Center image, scale to pixel space, translate to desired position, // then rescale to the [-1, 1] square for drawing. return - Scaling(2.0f / m_size.x(), -2.0f / m_size.y()) * + Matrix3f::scale(Vector2f{2.0f / m_size.x(), -2.0f / m_size.y()}) * mTransform * - Scaling(1.0f / mPixelRatio) * - Translation2f(pixelOffset(image->size())) * - Scaling(Vector2f{image->size().x(), image->size().y()}) * - Translation2f(Vector2f::Constant(-0.5f)); + Matrix3f::scale(Vector2f{1.0f / mPixelRatio}) * + Matrix3f::translate(pixelOffset(image->size())) * + Matrix3f::scale(Vector2f{image->size()}) * + Matrix3f::translate(Vector2f{-0.5f}); } -Transform ImageCanvas::textureToNanogui(const Image* image) { +Matrix3f ImageCanvas::textureToNanogui(const Image* image) { if (!image) { - return Transform::Identity(); + return Matrix3f::scale(Vector2f{1.0f}); } // Move origin to centre of image, scale pixels, apply our transform, move origin back to top-left. return - Translation2f(0.5f * Vector2f{m_size.x(), m_size.y()}) * + Matrix3f::translate(0.5f * Vector2f{m_size}) * mTransform * - Scaling(1.0f / mPixelRatio) * - Translation2f(-0.5f * Vector2f{image->size().x(), image->size().y()} + pixelOffset(image->size())); + Matrix3f::scale(Vector2f{1.0f / mPixelRatio}) * + Matrix3f::translate(-0.5f * Vector2f{image->size()} + pixelOffset(image->size())); } TEV_NAMESPACE_END diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index d8fc8b72c..8b3318fc5 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -23,7 +23,6 @@ #include #include -using namespace Eigen; using namespace filesystem; using namespace nanogui; using namespace std; @@ -536,7 +535,7 @@ bool ImageViewer::mouse_motion_event(const nanogui::Vector2i& p, const nanogui:: mSidebar->set_fixed_width(clamp(p.x(), 210, m_size.x() - 10)); requestLayoutUpdate(); } else if (mIsDraggingImage) { - Eigen::Vector2f relativeMovement = {rel.x(), rel.y()}; + nanogui::Vector2f relativeMovement = {rel}; auto* glfwWindow = screen()->glfw_window(); // There is no explicit access to the currently pressed modifier keys here, so we // need to directly ask GLFW. @@ -949,6 +948,7 @@ void ImageViewer::draw_contents() { if (lazyCanvasStatistics) { if (lazyCanvasStatistics->isReady()) { auto statistics = lazyCanvasStatistics->get(); + mHistogram->setNChannels(statistics->nChannels); mHistogram->setValues(statistics->histogram); mHistogram->setMinimum(statistics->minimum); mHistogram->setMean(statistics->mean); @@ -966,7 +966,8 @@ void ImageViewer::draw_contents() { ); } } else { - mHistogram->setValues(MatrixXf::Zero(1, 1)); + mHistogram->setNChannels({1}); + mHistogram->setValues({0.0f}); mHistogram->setMinimum(0); mHistogram->setMean(0); mHistogram->setMaximum(0); diff --git a/src/Ipc.cpp b/src/Ipc.cpp index e65065cec..6cc8f18cd 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -12,8 +12,6 @@ #include #include -#include - #ifdef _WIN32 using socklen_t = int; #else @@ -32,7 +30,6 @@ using socklen_t = int; # define INVALID_SOCKET (-1) #endif -using namespace Eigen; using namespace std; TEV_NAMESPACE_BEGIN @@ -103,14 +100,14 @@ void IpcPacket::setUpdateImage(const string& imageName, bool grabFocus, const st payload << channelOffsets; payload << channelStrides; - DenseIndex nPixels = width * height; + size_t nPixels = width * height; - DenseIndex stridedImageDataSize = 0; + size_t stridedImageDataSize = 0; for (int32_t c = 0; c < nChannels; ++c) { - stridedImageDataSize = std::max(stridedImageDataSize, (DenseIndex)(channelOffsets[c] + (nPixels-1) * channelStrides[c] + 1)); + stridedImageDataSize = std::max(stridedImageDataSize, (size_t)(channelOffsets[c] + (nPixels-1) * channelStrides[c] + 1)); } - if ((DenseIndex)stridedImageData.size() != stridedImageDataSize) { + if (stridedImageData.size() != stridedImageDataSize) { throw runtime_error{tfm::format("UpdateImage IPC packet's data size does not match specified dimensions, offset, and stride. (Expected: %d)", stridedImageDataSize)}; } @@ -224,7 +221,7 @@ IpcPacketUpdateImage IpcPacket::interpretAsUpdateImage() const { result.channelStrides.resize(result.nChannels, 1); payload >> result.x >> result.y >> result.width >> result.height; - DenseIndex nPixels = result.width * result.height; + size_t nPixels = (size_t)result.width * result.height; if (type >= Type::UpdateImageV3) { // custom offset/stride support @@ -241,9 +238,9 @@ IpcPacketUpdateImage IpcPacket::interpretAsUpdateImage() const { result.imageData[i].resize(nPixels); } - DenseIndex stridedImageDataSize = 0; + size_t stridedImageDataSize = 0; for (int32_t c = 0; c < result.nChannels; ++c) { - stridedImageDataSize = std::max(stridedImageDataSize, (DenseIndex)(result.channelOffsets[c] + (nPixels-1) * result.channelStrides[c] + 1)); + stridedImageDataSize = std::max(stridedImageDataSize, (size_t)(result.channelOffsets[c] + (nPixels-1) * result.channelStrides[c] + 1)); } if (payload.remainingBytes() < stridedImageDataSize * sizeof(float)) { @@ -251,7 +248,7 @@ IpcPacketUpdateImage IpcPacket::interpretAsUpdateImage() const { } const float* stridedImageData = (const float*)payload.get(); - gThreadPool->parallelFor(0, nPixels, [&](DenseIndex px) { + gThreadPool->parallelFor(0, nPixels, [&](size_t px) { for (int32_t c = 0; c < result.nChannels; ++c) { result.imageData[c][px] = stridedImageData[result.channelOffsets[c] + px * result.channelStrides[c]]; } diff --git a/src/MultiGraph.cpp b/src/MultiGraph.cpp index f0dd75355..8cfa99946 100644 --- a/src/MultiGraph.cpp +++ b/src/MultiGraph.cpp @@ -39,7 +39,7 @@ void MultiGraph::draw(NVGcontext *ctx) { nvgFill(ctx); - if (mValues.cols() >= 1 && mValues.rows() >= 2) { + if (mValues.size() >= 2) { array colors = {{ Color{255, 0, 0, 200}, Color{0, 255, 0, 200}, @@ -50,13 +50,15 @@ void MultiGraph::draw(NVGcontext *ctx) { // Additive blending nvgGlobalCompositeBlendFunc(ctx, NVGblendFactor::NVG_SRC_ALPHA, NVGblendFactor::NVG_ONE); - for (size_t i = 0; i < (size_t)mValues.cols(); i++) { + size_t nBins = mValues.size() / mNChannels; + + for (size_t i = 0; i < (size_t)mNChannels; i++) { nvgBeginPath(ctx); nvgMoveTo(ctx, m_pos.x(), m_pos.y() + m_size.y()); - - for (size_t j = 0; j < (size_t)mValues.rows(); j++) { - float value = mValues(j, i); - float vx = m_pos.x() + 2 + j * (m_size.x() - 4) / (float)(mValues.rows() - 1); + + for (size_t j = 0; j < (size_t)nBins; j++) { + float value = mValues[j + i * nBins]; + float vx = m_pos.x() + 2 + j * (m_size.x() - 4) / (float)(nBins - 1); float vy = m_pos.y() + (1 - value) * m_size.y(); nvgLineTo(ctx, vx, vy); } @@ -71,11 +73,11 @@ void MultiGraph::draw(NVGcontext *ctx) { if (mZeroBin > 0) { nvgBeginPath(ctx); - nvgRect(ctx, m_pos.x() + 1 + mZeroBin * (m_size.x() - 4) / (float)(mValues.rows() - 1), m_pos.y() + 15, 4, m_size.y() - 15); + nvgRect(ctx, m_pos.x() + 1 + mZeroBin * (m_size.x() - 4) / (float)(nBins - 1), m_pos.y() + 15, 4, m_size.y() - 15); nvgFillColor(ctx, Color(0, 128)); nvgFill(ctx); nvgBeginPath(ctx); - nvgRect(ctx, m_pos.x() + 2 + mZeroBin * (m_size.x() - 4) / (float)(mValues.rows() - 1), m_pos.y() + 15, 2, m_size.y() - 15); + nvgRect(ctx, m_pos.x() + 2 + mZeroBin * (m_size.x() - 4) / (float)(nBins - 1), m_pos.y() + 15, 2, m_size.y() - 15); nvgFillColor(ctx, Color(200, 255)); nvgFill(ctx); } diff --git a/src/UberShader.cpp b/src/UberShader.cpp index ffcc4c985..7d5af6481 100644 --- a/src/UberShader.cpp +++ b/src/UberShader.cpp @@ -4,16 +4,14 @@ #include #include -#include - -using namespace Eigen; +using namespace nanogui; using namespace std; TEV_NAMESPACE_BEGIN -UberShader::UberShader(nanogui::RenderPass* renderPass) { +UberShader::UberShader(RenderPass* renderPass) { try { - mShader = new nanogui::Shader{ + mShader = new Shader{ renderPass, "ubershader", @@ -402,15 +400,15 @@ UberShader::UberShader(nanogui::RenderPass* renderPass) { -1.f, 1.f, }; - mShader->set_buffer("indices", nanogui::VariableType::UInt32, {3*2}, indices); - mShader->set_buffer("position", nanogui::VariableType::Float32, {4, 2}, positions); + mShader->set_buffer("indices", VariableType::UInt32, {3*2}, indices); + mShader->set_buffer("position", VariableType::Float32, {4, 2}, positions); const auto& fcd = colormap::turbo(); - mColorMap = new nanogui::Texture{ - nanogui::Texture::PixelFormat::RGBA, - nanogui::Texture::ComponentFormat::Float32, - nanogui::Vector2i{(int)fcd.size() / 4, 1} + mColorMap = new Texture{ + Texture::PixelFormat::RGBA, + Texture::ComponentFormat::Float32, + Vector2i{(int)fcd.size() / 4, 1} }; mColorMap->upload((uint8_t*)fcd.data()); } @@ -420,7 +418,7 @@ UberShader::~UberShader() { } void UberShader::draw(const Vector2f& pixelSize, const Vector2f& checkerSize) { draw( pixelSize, checkerSize, - nullptr, nanogui::Matrix3f{0.0f}, + nullptr, Matrix3f{0.0f}, 0.0f, 0.0f, 0.0f, false, ETonemap::SRGB ); @@ -429,8 +427,8 @@ void UberShader::draw(const Vector2f& pixelSize, const Vector2f& checkerSize) { void UberShader::draw( const Vector2f& pixelSize, const Vector2f& checkerSize, - nanogui::Texture* textureImage, - const nanogui::Matrix3f& transformImage, + Texture* textureImage, + const Matrix3f& transformImage, float exposure, float offset, float gamma, @@ -440,7 +438,7 @@ void UberShader::draw( draw( pixelSize, checkerSize, textureImage, transformImage, - nullptr, nanogui::Matrix3f{0.0f}, + nullptr, Matrix3f{0.0f}, exposure, offset, gamma, clipToLdr, tonemap, EMetric::Error ); @@ -449,10 +447,10 @@ void UberShader::draw( void UberShader::draw( const Vector2f& pixelSize, const Vector2f& checkerSize, - nanogui::Texture* textureImage, - const nanogui::Matrix3f& transformImage, - nanogui::Texture* textureReference, - const nanogui::Matrix3f& transformReference, + Texture* textureImage, + const Matrix3f& transformImage, + Texture* textureReference, + const Matrix3f& transformReference, float exposure, float offset, float gamma, @@ -480,27 +478,27 @@ void UberShader::draw( mShader->set_uniform("clipToLdr", clipToLdr); mShader->begin(); - mShader->draw_array(nanogui::Shader::PrimitiveType::Triangle, 0, 6, true); + mShader->draw_array(Shader::PrimitiveType::Triangle, 0, 6, true); mShader->end(); } void UberShader::bindCheckerboardData(const Vector2f& pixelSize, const Vector2f& checkerSize) { - mShader->set_uniform("pixelSize", nanogui::Vector2f{pixelSize.x(), pixelSize.y()}); - mShader->set_uniform("checkerSize", nanogui::Vector2f{checkerSize.x(), checkerSize.y()}); + mShader->set_uniform("pixelSize", pixelSize); + mShader->set_uniform("checkerSize", checkerSize); mShader->set_uniform("bgColor", mBackgroundColor); } void UberShader::bindImageData( - nanogui::Texture* textureImage, - const nanogui::Matrix3f& transformImage, + Texture* textureImage, + const Matrix3f& transformImage, float exposure, float offset, float gamma, ETonemap tonemap ) { mShader->set_texture("image", textureImage); - mShader->set_uniform("imageScale", nanogui::Vector2f{transformImage.m[0][0], transformImage.m[1][1]}); - mShader->set_uniform("imageOffset", nanogui::Vector2f{transformImage.m[2][0], transformImage.m[2][1]}); + mShader->set_uniform("imageScale", Vector2f{transformImage.m[0][0], transformImage.m[1][1]}); + mShader->set_uniform("imageOffset", Vector2f{transformImage.m[2][0], transformImage.m[2][1]}); mShader->set_uniform("exposure", exposure); mShader->set_uniform("offset", offset); @@ -511,13 +509,13 @@ void UberShader::bindImageData( } void UberShader::bindReferenceData( - nanogui::Texture* textureReference, - const nanogui::Matrix3f& transformReference, + Texture* textureReference, + const Matrix3f& transformReference, EMetric metric ) { mShader->set_texture("reference", textureReference); - mShader->set_uniform("referenceScale", nanogui::Vector2f{transformReference.m[0][0], transformReference.m[1][1]}); - mShader->set_uniform("referenceOffset", nanogui::Vector2f{transformReference.m[2][0], transformReference.m[2][1]}); + mShader->set_uniform("referenceScale", Vector2f{transformReference.m[0][0], transformReference.m[1][1]}); + mShader->set_uniform("referenceOffset", Vector2f{transformReference.m[2][0], transformReference.m[2][1]}); mShader->set_uniform("metric", static_cast(metric)); } diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index 70d1ef9ec..450181430 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -68,7 +68,7 @@ Task> StbiImageLoader::load(istream& iStream, const if (isHdr) { co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { auto typedData = reinterpret_cast(data); - int baseIdx = i * numChannels; + size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { channels[c].at(i) = typedData[baseIdx + c]; } @@ -76,7 +76,7 @@ Task> StbiImageLoader::load(istream& iStream, const } else { co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { auto typedData = reinterpret_cast(data); - int baseIdx = i * numChannels; + size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == alphaChannelIndex) { channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; From b299d2f318c61187354bd495ae0f98d426efdb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 10 Aug 2021 19:21:58 +0200 Subject: [PATCH 30/83] Nanogui is no longer OGL-exclusive --- src/HelpWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 1f6cdd276..efb5409fc 100644 --- a/src/HelpWindow.cpp +++ b/src/HelpWindow.cpp @@ -167,7 +167,7 @@ HelpWindow::HelpWindow(Widget *parent, bool supportsHdr, function closeC addLibrary(about, "filesystem", "", "Lightweight Path Manipulation Library"); addLibrary(about, "Glad", "", "Multi-Language GL Loader-Generator"); addLibrary(about, "GLFW", "", "OpenGL Desktop Development Library"); - addLibrary(about, "NanoGUI", "", "Small Widget Library for OpenGL"); + addLibrary(about, "NanoGUI", "", "Small GUI Library"); addLibrary(about, "NanoVG", "", "Small Vector Graphics Library"); addLibrary(about, "OpenEXR", "", "High Dynamic-Range (HDR) Image File Format"); addLibrary(about, "stb_image(_write)", "", "Single-Header Library for Loading and Writing Images"); From 1da90e5c64827894f58beebbdb3cdda05d9d02eb Mon Sep 17 00:00:00 2001 From: Tom94 Date: Wed, 11 Aug 2021 10:07:13 +0200 Subject: [PATCH 31/83] Fix compilation on linux --- CMakeLists.txt | 5 +++++ include/tev/Task.h | 1 + 2 files changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 38364769c..99e338607 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,11 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES " endif() endif() +# Coroutines need to be explicitly enabled on g++ +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines") +endif() + set(TEV_LIBS clip IlmImf nanogui ${NANOGUI_EXTRA_LIBS}) if (MSVC) set(TEV_LIBS ${TEV_LIBS} zlibstatic DirectXTex wsock32 ws2_32) diff --git a/include/tev/Task.h b/include/tev/Task.h index a6542a3da..83d7b1318 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -13,6 +13,7 @@ #define COROUTINE_NAMESPACE std #endif +#include #include TEV_NAMESPACE_BEGIN From af52c10ebd0e6abb79dbc58e4c589bb7422f98f7 Mon Sep 17 00:00:00 2001 From: Tom94 Date: Wed, 11 Aug 2021 10:10:51 +0200 Subject: [PATCH 32/83] Make Task much safer to use - Only allows moving Task - Automatically frees coroutine state (fixes memory leaks) - Adds the ability to detach Task execution (letting coroutine destruction become the responsibility of the caller) - Automatically waits for coroutine completion upon Task destruction --- include/tev/Task.h | 80 ++++++++++++++++++++++++++++++---------- include/tev/ThreadPool.h | 4 +- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index 83d7b1318..eba9e9bb3 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -18,7 +18,6 @@ TEV_NAMESPACE_BEGIN -// TODO: replace with std::latch when it's supported everywhere class Latch { public: Latch(int val) : mCounter{val} {} @@ -41,6 +40,7 @@ class Latch { mCv.wait(lock); } } + private: std::atomic mCounter; std::mutex mMutex; @@ -93,8 +93,8 @@ struct TaskPromise : public TaskPromiseBase { onFinalSuspend.emplace_back(std::forward(fun)); } - // The coroutine is about to complete (via co_return or reaching the end of the coroutine body). - // The awaiter returned here defines what happens next + // The coroutine is about to complete (via co_return, reaching the end of the coroutine body, + // or an uncaught exception). The awaiter returned here defines what happens next auto final_suspend() const noexcept { for (auto& f : onFinalSuspend) { f(); @@ -104,12 +104,12 @@ struct TaskPromise : public TaskPromiseBase { bool await_ready() const noexcept { return false; } void await_resume() const noexcept {} - // Returning the parent coroutine has the effect of destroying this coroutine handle - // and continuing execution where the parent co_await'ed us. + // Returning the parent coroutine has the effect of continuing execution where the parent co_await'ed us. COROUTINE_NAMESPACE::coroutine_handle<> await_suspend(COROUTINE_NAMESPACE::coroutine_handle> h) const noexcept { bool isLast = h.promise().latch.countDown(); - if (isLast && h.promise().precursor) { - return h.promise().precursor; + auto precursor = h.promise().precursor; + if (isLast && precursor) { + return precursor; } return COROUTINE_NAMESPACE::noop_coroutine(); @@ -127,6 +127,23 @@ struct Task { // This handle is assigned to when the coroutine itself is suspended (see await_suspend above) COROUTINE_NAMESPACE::coroutine_handle handle; + Task(COROUTINE_NAMESPACE::coroutine_handle handle) : handle{handle} {} + + // No copying allowed! + Task(const Task& other) = delete; + Task(Task&& other) { + handle = other.handle; + other.detach(); + } + + ~Task() { + // Make sure the coroutine finished and is cleaned up + if (handle) { + wait(); + clear(); + } + } + bool await_ready() const noexcept { // No need to suspend if this task has no outstanding work return handle.done(); @@ -137,18 +154,29 @@ struct Task { handle.promise().finally(std::forward(fun)); } - T await_resume() const { - if (handle.promise().eptr) { - std::rethrow_exception(handle.promise().eptr); + T await_resume() { + TEV_ASSERT(handle, "Should not have been able to co_await a detached Task."); + + auto eptr = handle.promise().eptr; + if (eptr) { + clear(); + std::rethrow_exception(eptr); } if constexpr (!std::is_void_v) { // The returned value here is what `co_await our_task` evaluates to - return std::move(handle.promise().data); + T tmp = std::move(handle.promise().data); + clear(); + return tmp; } } bool await_suspend(COROUTINE_NAMESPACE::coroutine_handle<> coroutine) const noexcept { + if (!handle) { + tlog::error() << "Cannot co_await a detached Task."; + std::terminate(); + } + // The coroutine itself is being suspended (async work can beget other async work) // Record the argument as the continuation point when this is resumed later. See // the final_suspend awaiter on the promise_type above for where this gets used @@ -157,17 +185,31 @@ struct Task { } void wait() const { - handle.promise().latch.countDown(); - handle.promise().latch.wait(); + if (!handle) { + throw std::runtime_error{"Cannot wait for a detached Task."}; + } + + if (!handle.promise().latch.countDown()) { + handle.promise().latch.wait(); + } } - T get() const { + T get() { wait(); - if (handle.promise().eptr) { - std::rethrow_exception(handle.promise().eptr); - } - if constexpr (!std::is_void_v) { - return std::move(handle.promise().data); + return await_resume(); + } + + COROUTINE_NAMESPACE::coroutine_handle detach() noexcept { + auto tmp = handle; + handle = nullptr; + return tmp; + } + +private: + void clear() noexcept { + if (handle) { + handle.destroy(); + handle = nullptr; } } }; diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 4c66d5156..3de94cd82 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -41,7 +41,7 @@ class ThreadPool { return res; } - inline auto schedule(int priority) noexcept { + inline auto enqueueCoroutine(int priority) noexcept { class Awaiter { public: Awaiter(ThreadPool* pool, int priority) @@ -87,7 +87,7 @@ class ThreadPool { TEV_ASSERT(taskStart != taskEnd, "Shouldn't not produce tasks with empty range."); tasks.emplace_back([this](Int start, Int end, F body, int priority) -> Task { - co_await schedule(priority); + co_await enqueueCoroutine(priority); for (Int j = start; j < end; ++j) { body(j); } From 35a95a91b03f0a9ee18d6b1abc1078a49ac72fb0 Mon Sep 17 00:00:00 2001 From: Tom94 Date: Wed, 11 Aug 2021 10:11:24 +0200 Subject: [PATCH 33/83] Fix incorrect 2D vector transform in homogeneous coordinates --- include/tev/Common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/tev/Common.h b/include/tev/Common.h index 81b7fe026..d7ede209b 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -123,7 +123,7 @@ namespace nanogui { result.v[i] = accum; } } - return result;// / w; + return result / w; } template From ff6a3cb6cdda89ec8394b424f595cb659ea2b4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Wed, 11 Aug 2021 17:40:52 +0200 Subject: [PATCH 34/83] Fix compile error on Windows std::promise does not support non-default-constructible types (such as Task) on MSVC, so we work around the problem by scheduling image loads slightly differently. --- include/tev/SharedQueue.h | 4 ++-- src/Image.cpp | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/tev/SharedQueue.h b/include/tev/SharedQueue.h index f68dfe67e..adbb947ba 100644 --- a/include/tev/SharedQueue.h +++ b/include/tev/SharedQueue.h @@ -43,11 +43,11 @@ class SharedQueue { return result; } - T tryPop() { + std::optional tryPop() { std::unique_lock lock{mMutex}; if (mRawQueue.empty()) { - throw std::runtime_error{"Could not pop."}; + return {}; } T result = std::move(mRawQueue.front()); diff --git a/src/Image.cpp b/src/Image.cpp index 779ed8c4a..28b433850 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -520,7 +520,8 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele int imageId = Image::drawId(); int loadId = mUnsortedLoadCounter++; - gThreadPool->enqueueTask(coLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { + coLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { + co_await gThreadPool->enqueueCoroutine(-imageId); auto image = co_await tryLoadImage(imageId, path, channelSelector); { @@ -531,7 +532,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele if (publishSortedLoads()) { glfwPostEmptyEvent(); } - }), -imageId); + })(); } bool BackgroundImagesLoader::publishSortedLoads() { From 1e4391067c4279422f154cc18432a3e77986e677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Wed, 11 Aug 2021 17:41:09 +0200 Subject: [PATCH 35/83] std::optional instead of exceptions To avoid spam in the debug console, mostly --- include/tev/Image.h | 3 ++- src/ImageViewer.cpp | 17 +++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index 92c2df387..86fe8976d 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -192,7 +193,7 @@ struct ImageAddition { class BackgroundImagesLoader { public: void enqueue(const filesystem::path& path, const std::string& channelSelector, bool shallSelect); - ImageAddition tryPop() { return mLoadedImages.tryPop(); } + std::optional tryPop() { return mLoadedImages.tryPop(); } bool publishSortedLoads(); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 8b3318fc5..edaf0c914 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -883,13 +883,9 @@ void ImageViewer::draw_contents() { // place where we actually add them to the GUI. Focus the application in case one of the // new images is meant to override the current selection. bool newFocus = false; - try { - while (true) { - auto addition = mImagesLoader->tryPop(); - newFocus |= addition.shallSelect; - addImage(addition.image, addition.shallSelect); - } - } catch (const runtime_error&) { + while (auto addition = mImagesLoader->tryPop()) { + newFocus |= addition->shallSelect; + addImage(addition->image, addition->shallSelect); } if (newFocus) { @@ -898,11 +894,8 @@ void ImageViewer::draw_contents() { // mTaskQueue contains jobs that should be executed on the main thread. It is useful for handling // callbacks from background threads - try { - while (true) { - mTaskQueue.tryPop()(); - } - } catch (const runtime_error&) { + while (auto task = mTaskQueue.tryPop()) { + (*task)(); } for (auto it = begin(mToBump); it != end(mToBump); ) { From 94ef773052059285caa8b8e4993613b91fde7554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 13 Aug 2021 09:11:06 +0200 Subject: [PATCH 36/83] Make ScopeGuard safer by disabling copy --- include/tev/Common.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/tev/Common.h b/include/tev/Common.h index d7ede209b..f4682db10 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -206,6 +206,8 @@ class ScopeGuard { public: ScopeGuard(const T& callback) : mCallback{callback} {} ScopeGuard(T&& callback) : mCallback{std::move(callback)} {} + ScopeGuard(const ScopeGuard& other) = delete; + ScopeGuard(ScopeGuard&& other) { mCallback = std::move(other.mCallback); other.mCallback = {}; } ~ScopeGuard() { mCallback(); } private: T mCallback; From da1fa3615f7abff6b6418387f09871a31e3a49f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 13 Aug 2021 09:11:38 +0200 Subject: [PATCH 37/83] Fix data race & memory leak in task system --- include/tev/Task.h | 77 +++++++++++++++++++++++++++++++++++----- include/tev/ThreadPool.h | 8 ++--- src/Image.cpp | 2 +- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index eba9e9bb3..f2ae4ba57 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -15,6 +15,7 @@ #include #include +#include TEV_NAMESPACE_BEGIN @@ -23,11 +24,17 @@ class Latch { Latch(int val) : mCounter{val} {} bool countDown() noexcept { std::unique_lock lock{mMutex}; - bool result = (--mCounter == 0); - if (result) { + int val = --mCounter; + if (val <= 0) { mCv.notify_all(); + return true; } - return result; + + if (val < 0) { + tlog::warning() << "Latch should never count below zero."; + } + + return false; } void wait() { @@ -75,6 +82,7 @@ template struct TaskPromise : public TaskPromiseBase { COROUTINE_NAMESPACE::coroutine_handle<> precursor; Latch latch{2}; + std::binary_semaphore done{0}; std::exception_ptr eptr; std::vector> onFinalSuspend; @@ -108,6 +116,7 @@ struct TaskPromise : public TaskPromiseBase { COROUTINE_NAMESPACE::coroutine_handle<> await_suspend(COROUTINE_NAMESPACE::coroutine_handle> h) const noexcept { bool isLast = h.promise().latch.countDown(); auto precursor = h.promise().precursor; + h.promise().done.release(); // Allow destroying this coroutine's handle if (isLast && precursor) { return precursor; } @@ -131,14 +140,21 @@ struct Task { // No copying allowed! Task(const Task& other) = delete; + Task& operator=(const Task& other) = delete; + Task(Task&& other) { handle = other.handle; other.detach(); } + Task& operator=(const Task&& other) { + handle = other.handle; + other.detach(); + } ~Task() { // Make sure the coroutine finished and is cleaned up if (handle) { + tlog::warning() << "~Task was waiting for completion. This was likely not intended."; wait(); clear(); } @@ -157,16 +173,19 @@ struct Task { T await_resume() { TEV_ASSERT(handle, "Should not have been able to co_await a detached Task."); + ScopeGuard guard{[this] { + handle.promise().done.acquire(); + clear(); + }}; + auto eptr = handle.promise().eptr; if (eptr) { - clear(); std::rethrow_exception(eptr); } if constexpr (!std::is_void_v) { // The returned value here is what `co_await our_task` evaluates to T tmp = std::move(handle.promise().data); - clear(); return tmp; } } @@ -184,14 +203,16 @@ struct Task { return !handle.promise().latch.countDown(); } - void wait() const { + bool wait() const { if (!handle) { throw std::runtime_error{"Cannot wait for a detached Task."}; } if (!handle.promise().latch.countDown()) { handle.promise().latch.wait(); + return true; } + return false; } T get() { @@ -214,10 +235,50 @@ struct Task { } }; +struct DetachedTask { + struct promise_type { + std::vector> onFinalSuspend; + + template + void finally(F&& fun) { + onFinalSuspend.emplace_back(std::forward(fun)); + } + + DetachedTask get_return_object() noexcept { + return {COROUTINE_NAMESPACE::coroutine_handle::from_promise(*this)}; + } + + COROUTINE_NAMESPACE::suspend_never initial_suspend() const noexcept { return {}; } + COROUTINE_NAMESPACE::suspend_never final_suspend() const noexcept { + for (auto& f : onFinalSuspend) { + f(); + } + return {}; + } + + void return_void() {} + void unhandled_exception() { + try { + std::rethrow_exception(std::current_exception()); + } catch(const std::exception& e) { + tlog::error() << "Unhandled exception in DetachedTask: " << e.what(); + std::terminate(); + } + } + }; + + COROUTINE_NAMESPACE::coroutine_handle handle; + + template + void finally(F&& fun) { + handle.promise().finally(std::forward(fun)); + } +}; + // Ties the lifetime of a lambda coroutine's captures -// to that of the coroutine. +// to that of the task. // Taken from https://stackoverflow.com/a/68630143 -auto coLambda(auto&& executor) { +auto taskLambda(auto&& executor) { return [executor=std::move(executor)](Args&&... args) { using ReturnType = decltype(executor(args...)); // copy the lambda into a new std::function pointer diff --git a/include/tev/ThreadPool.h b/include/tev/ThreadPool.h index 3de94cd82..a503c76b6 100644 --- a/include/tev/ThreadPool.h +++ b/include/tev/ThreadPool.h @@ -84,14 +84,14 @@ class ThreadPool { for (Int i = 0; i < nTasks; ++i) { Int taskStart = start + (range * i / nTasks); Int taskEnd = start + (range * (i+1) / nTasks); - TEV_ASSERT(taskStart != taskEnd, "Shouldn't not produce tasks with empty range."); + TEV_ASSERT(taskStart != taskEnd, "Should not produce tasks with empty range."); - tasks.emplace_back([this](Int start, Int end, F body, int priority) -> Task { - co_await enqueueCoroutine(priority); + tasks.emplace_back([](Int start, Int end, F body, int priority, ThreadPool* pool) -> Task { + co_await pool->enqueueCoroutine(priority); for (Int j = start; j < end; ++j) { body(j); } - }(taskStart, taskEnd, body, priority)); + }(taskStart, taskEnd, body, priority, this)); } for (auto& task : tasks) { diff --git a/src/Image.cpp b/src/Image.cpp index 28b433850..1bdd8a3bb 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -520,7 +520,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele int imageId = Image::drawId(); int loadId = mUnsortedLoadCounter++; - coLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { + taskLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> DetachedTask { co_await gThreadPool->enqueueCoroutine(-imageId); auto image = co_await tryLoadImage(imageId, path, channelSelector); From 62c8a5929eadc2c79be3c1c02327d9f08c3faabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 13 Aug 2021 09:11:46 +0200 Subject: [PATCH 38/83] Ensure parallel compilation on Windows --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 99e338607..b50419b62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,7 +42,7 @@ if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D_CRT_SECURE_NO_WARNINGS") # Parallel build - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP24") # Static build set(CompilerFlags From ec5e6f681f902e5eb3d947d43b0277fa192e824c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Fri, 13 Aug 2021 09:19:10 +0200 Subject: [PATCH 39/83] Fix macos compilation error --- include/tev/Task.h | 9 ++++++--- src/MultiGraph.cpp | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index f2ae4ba57..95e8c566c 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -82,7 +82,7 @@ template struct TaskPromise : public TaskPromiseBase { COROUTINE_NAMESPACE::coroutine_handle<> precursor; Latch latch{2}; - std::binary_semaphore done{0}; + std::atomic done = false; // TODO: use a std::binary_semaphore once that's available on macOS std::exception_ptr eptr; std::vector> onFinalSuspend; @@ -116,7 +116,7 @@ struct TaskPromise : public TaskPromiseBase { COROUTINE_NAMESPACE::coroutine_handle<> await_suspend(COROUTINE_NAMESPACE::coroutine_handle> h) const noexcept { bool isLast = h.promise().latch.countDown(); auto precursor = h.promise().precursor; - h.promise().done.release(); // Allow destroying this coroutine's handle + h.promise().done = true; // Allow destroying this coroutine's handle if (isLast && precursor) { return precursor; } @@ -174,7 +174,10 @@ struct Task { TEV_ASSERT(handle, "Should not have been able to co_await a detached Task."); ScopeGuard guard{[this] { - handle.promise().done.acquire(); + // Spinlock is fine since this will always + // be set in the next couple of instructions + // of the task's executing thread. + while (!handle.promise().done) {} clear(); }}; diff --git a/src/MultiGraph.cpp b/src/MultiGraph.cpp index 8cfa99946..69d6318fa 100644 --- a/src/MultiGraph.cpp +++ b/src/MultiGraph.cpp @@ -8,6 +8,8 @@ #include #include +#include + using namespace nanogui; using namespace std; From 2d15b256c4d74c19f442b87212e6ba44df0eadd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 13 Aug 2021 14:49:33 +0200 Subject: [PATCH 40/83] Attempt to make CI work --- .github/workflows/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f41756398..55b6b79bf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,18 +18,22 @@ jobs: steps: - name: Install dependencies - run: sudo apt-get update && sudo apt-get install cmake xorg-dev libglu1-mesa-dev zlib1g-dev zenity + run: sudo apt-get update && sudo apt-get install cmake gcc-10 g++-10 libglu1-mesa-dev xorg-dev zenity zlib1g-dev - uses: actions/checkout@v1 with: submodules: recursive - name: CMake run: cmake . + shell: bash + env: + CC: gcc-10 + CXX: g++-10 - name: Build run: make -j build_macos: name: Build on macOS - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v1 From 84b683d73135a4e3a0c39b5335d3d2cb7b0ab0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 12:00:06 +0200 Subject: [PATCH 41/83] Avoid need for onFinalSuspend (better lambda lifetime / task detachment procedure) --- include/tev/Task.h | 49 +++++----------------------------------------- src/Image.cpp | 4 ++-- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index 95e8c566c..4ee3dc12e 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -84,7 +84,6 @@ struct TaskPromise : public TaskPromiseBase { Latch latch{2}; std::atomic done = false; // TODO: use a std::binary_semaphore once that's available on macOS std::exception_ptr eptr; - std::vector> onFinalSuspend; future_t get_return_object() noexcept { return {COROUTINE_NAMESPACE::coroutine_handle>::from_promise(*this)}; @@ -96,18 +95,9 @@ struct TaskPromise : public TaskPromiseBase { eptr = std::current_exception(); } - template - void finally(F&& fun) { - onFinalSuspend.emplace_back(std::forward(fun)); - } - // The coroutine is about to complete (via co_return, reaching the end of the coroutine body, // or an uncaught exception). The awaiter returned here defines what happens next auto final_suspend() const noexcept { - for (auto& f : onFinalSuspend) { - f(); - } - struct Awaiter { bool await_ready() const noexcept { return false; } void await_resume() const noexcept {} @@ -165,11 +155,6 @@ struct Task { return handle.done(); } - template - void finally(F&& fun) { - handle.promise().finally(std::forward(fun)); - } - T await_resume() { TEV_ASSERT(handle, "Should not have been able to co_await a detached Task."); @@ -240,24 +225,12 @@ struct Task { struct DetachedTask { struct promise_type { - std::vector> onFinalSuspend; - - template - void finally(F&& fun) { - onFinalSuspend.emplace_back(std::forward(fun)); - } - DetachedTask get_return_object() noexcept { return {COROUTINE_NAMESPACE::coroutine_handle::from_promise(*this)}; } COROUTINE_NAMESPACE::suspend_never initial_suspend() const noexcept { return {}; } - COROUTINE_NAMESPACE::suspend_never final_suspend() const noexcept { - for (auto& f : onFinalSuspend) { - f(); - } - return {}; - } + COROUTINE_NAMESPACE::suspend_never final_suspend() const noexcept { return {}; } void return_void() {} void unhandled_exception() { @@ -271,27 +244,15 @@ struct DetachedTask { }; COROUTINE_NAMESPACE::coroutine_handle handle; - - template - void finally(F&& fun) { - handle.promise().finally(std::forward(fun)); - } }; // Ties the lifetime of a lambda coroutine's captures // to that of the task. // Taken from https://stackoverflow.com/a/68630143 -auto taskLambda(auto&& executor) { - return [executor=std::move(executor)](Args&&... args) { - using ReturnType = decltype(executor(args...)); - // copy the lambda into a new std::function pointer - auto exec = new std::function(executor); - // execute the lambda and save the result - auto result = (*exec)(args...); - // call custom method to save lambda until task ends - result.finally([exec]() { delete exec; }); - return result; - }; +template +DetachedTask invokeTaskDetached(F&& executor, Args&&... args) { + auto exec = std::move(executor); + co_await exec(args...); } TEV_NAMESPACE_END diff --git a/src/Image.cpp b/src/Image.cpp index 1bdd8a3bb..eb3583234 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -520,7 +520,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele int imageId = Image::drawId(); int loadId = mUnsortedLoadCounter++; - taskLambda([imageId, loadId, path, channelSelector, shallSelect, this]() -> DetachedTask { + invokeTaskDetached([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { co_await gThreadPool->enqueueCoroutine(-imageId); auto image = co_await tryLoadImage(imageId, path, channelSelector); @@ -532,7 +532,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele if (publishSortedLoads()) { glfwPostEmptyEvent(); } - })(); + }); } bool BackgroundImagesLoader::publishSortedLoads() { From 8807be6d078a35259a4dcd4ef73d2abd4a2d6ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 12:00:36 +0200 Subject: [PATCH 42/83] Smarter canvas statistics thread pooling with coroutines --- include/tev/ImageCanvas.h | 4 ---- include/tev/Lazy.h | 17 +++++++---------- src/ImageCanvas.cpp | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 68e2e4c4b..e3e72f938 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -170,10 +170,6 @@ class ImageCanvas : public nanogui::Canvas { EMetric mMetric = Error; std::map>>> mMeanValues; - // A custom threadpool is used to ensure progress - // on the global threadpool, even when excessively - // many mean value computations are scheduled. - ThreadPool mMeanValueThreadPool; }; TEV_NAMESPACE_END diff --git a/include/tev/Lazy.h b/include/tev/Lazy.h index 6b54be8f1..33feac283 100644 --- a/include/tev/Lazy.h +++ b/include/tev/Lazy.h @@ -27,6 +27,10 @@ class Lazy { : mThreadPool{threadPool}, mCompute{compute} { } + Lazy(std::future&& future) + : mAsyncValue{std::move(future)} { + } + T get() { if (mIsComputed) { return mValue; @@ -39,6 +43,7 @@ class Lazy { } mIsComputed = true; + mBecameReadyAt = std::chrono::steady_clock::now(); return mValue; } @@ -76,17 +81,9 @@ class Lazy { } if (mThreadPool) { - mAsyncValue = mThreadPool->enqueueTask([this]() { - T result = compute(); - mBecameReadyAt = std::chrono::steady_clock::now(); - return result; - }, priority); + mAsyncValue = mThreadPool->enqueueTask([this]() { return compute(); }, priority); } else { - mAsyncValue = std::async(std::launch::async, [this]() { - T result = compute(); - mBecameReadyAt = std::chrono::steady_clock::now(); - return result; - }); + mAsyncValue = std::async(std::launch::async, [this]() { return compute(); }); } } diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index b79316b9c..69c61e488 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -467,16 +467,16 @@ shared_ptr>> ImageCanvas::canvasStatistics() { auto image = mImage, reference = mReference; auto requestedChannelGroup = mRequestedChannelGroup; auto metric = mMetric; - mMeanValues.insert(make_pair(key, make_shared>>( - [image, reference, requestedChannelGroup, metric, priority]() { - return computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority).get(); - }, - &mMeanValueThreadPool - ))); - - auto val = mMeanValues.at(key); - val->computeAsync(priority); - return val; + + promise> promise; + mMeanValues.insert(make_pair(key, make_shared>>(promise.get_future()))); + + invokeTaskDetached([image, reference, requestedChannelGroup, metric, priority, p=std::move(promise)]() mutable -> Task { + co_await gThreadPool->enqueueCoroutine(priority); + p.set_value(co_await computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority)); + }); + + return mMeanValues.at(key); } vector ImageCanvas::channelsFromImages( From ef7fec9582b25e039d023f04168b87e52f41c6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 12:01:38 +0200 Subject: [PATCH 43/83] Remove outdated comment --- include/tev/Task.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/include/tev/Task.h b/include/tev/Task.h index 4ee3dc12e..209676aa3 100644 --- a/include/tev/Task.h +++ b/include/tev/Task.h @@ -246,9 +246,6 @@ struct DetachedTask { COROUTINE_NAMESPACE::coroutine_handle handle; }; -// Ties the lifetime of a lambda coroutine's captures -// to that of the task. -// Taken from https://stackoverflow.com/a/68630143 template DetachedTask invokeTaskDetached(F&& executor, Args&&... args) { auto exec = std::move(executor); From f917706d8cef138c52cb6a086bd70fe972218a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 12 Oct 2021 16:36:25 +0200 Subject: [PATCH 44/83] Fix broken chromaticities conversion --- include/tev/Image.h | 4 +- src/Image.cpp | 82 ++++++++++++++++++---------------- src/imageio/ExrImageLoader.cpp | 2 - 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index 86fe8976d..a5a16929a 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -36,6 +36,8 @@ struct ImageData { std::vector channelsInLayer(std::string layerName) const; + Task convertToRec709(int priority); + void alphaOperation(const std::function& func); Task multiplyAlpha(int priority); @@ -157,8 +159,6 @@ class Image { std::vector getGroupedChannels(const std::string& layerName) const; - void toRec709(); - filesystem::path mPath; std::string mChannelSelector; diff --git a/src/Image.cpp b/src/Image.cpp index eb3583234..6b897fe8d 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -45,6 +45,46 @@ vector ImageData::channelsInLayer(string layerName) const { return result; } +Task ImageData::convertToRec709(int priority) { + // No need to do anything for identity transforms + if (toRec709 == nanogui::Matrix4f{1.0f}) { + co_return; + } + + vector> tasks; + + for (const auto& layer : layers) { + string layerPrefix = layer.empty() ? "" : (layer + "."); + + Channel* r = nullptr; + Channel* g = nullptr; + Channel* b = nullptr; + + if (!( + (r = mutableChannel(layerPrefix + "R")) && (g = mutableChannel(layerPrefix + "G")) && (b = mutableChannel(layerPrefix + "B")) || + (r = mutableChannel(layerPrefix + "r")) && (g = mutableChannel(layerPrefix + "g")) && (b = mutableChannel(layerPrefix + "b")) + )) { + // No RGB-triplet found + continue; + } + + TEV_ASSERT(r && g && b, "RGB triplet of channels must exist."); + + tasks.emplace_back( + gThreadPool->parallelForAsync(0, r->count(), [r, g, b, this](size_t i) { + auto rgb = toRec709 * Vector3f{r->at(i), g->at(i), b->at(i)}; + r->at(i) = rgb.x(); + g->at(i) = rgb.y(); + b->at(i) = rgb.z(); + }, priority) + ); + } + + for (auto& task : tasks) { + co_await task; + } +} + void ImageData::alphaOperation(const function& func) { for (const auto& layer : layers) { string layerPrefix = layer.empty() ? "" : (layer + "."); @@ -112,9 +152,6 @@ Image::Image(int id, const class path& path, ImageData&& data, const string& cha auto groups = getGroupedChannels(layer); mChannelGroups.insert(end(mChannelGroups), begin(groups), end(groups)); } - - // Convert chromaticities to sRGB / Rec 709 if they aren't already. - toRec709(); } Image::~Image() { @@ -396,42 +433,6 @@ string Image::toString() const { return result + join(localLayers, "\n"); } -void Image::toRec709() { - // No need to do anything for identity transforms - if (mData.toRec709 == nanogui::Matrix4f{1.0f}) { - return; - } - - vector> futures; - - for (const auto& layer : mData.layers) { - string layerPrefix = layer.empty() ? "" : (layer + "."); - - Channel* r = nullptr; - Channel* g = nullptr; - Channel* b = nullptr; - - if (!( - (r = mutableChannel(layerPrefix + "R")) && (g = mutableChannel(layerPrefix + "G")) && (b = mutableChannel(layerPrefix + "B")) || - (r = mutableChannel(layerPrefix + "r")) && (g = mutableChannel(layerPrefix + "g")) && (b = mutableChannel(layerPrefix + "b")) - )) { - // No RGB-triplet found - continue; - } - - TEV_ASSERT(r && g && b, "RGB triplet of channels must exist."); - - gThreadPool->parallelForAsync(0, r->count(), [r, g, b, this](DenseIndex i) { - auto rgb = mData.toRec709 * nanogui::Vector3f{r->at(i), g->at(i), b->at(i)}; - r->at(i) = rgb.x(); - g->at(i) = rgb.y(); - b->at(i) = rgb.z(); - }, futures); - } - - waitAll(futures); -} - Task> tryLoadImage(int imageId, path path, istream& iStream, string channelSelector) { auto handleException = [&](const exception& e) { if (channelSelector.empty()) { @@ -471,6 +472,9 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s co_await data.multiplyAlpha(taskPriority); } + // Convert chromaticities to sRGB / Rec 709 if they aren't already. + co_await data.convertToRec709(taskPriority); + auto image = make_shared(imageId, path, std::move(data), channelSelector); auto end = chrono::system_clock::now(); diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index b86dc0088..a999f6839 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -250,8 +250,6 @@ Task> ExrImageLoader::load(istream& iStream, const p co_await task; } - hasPremultipliedAlpha = true; - // equality comparison for Imf::Chromaticities instances auto chromaEq = [](const Imf::Chromaticities& a, const Imf::Chromaticities& b) { return From 8dceb7ec8d0fe00f20c6b3836674621c0f48d898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 15 Aug 2021 15:01:12 +0200 Subject: [PATCH 45/83] Load & expose data and display windows of EXR files TODO: the image canvas needs to adequately display these in the future --- CMakeLists.txt | 1 + include/tev/Box.h | 49 ++++++++++++++++++++++++++++ include/tev/Channel.h | 22 ++++++------- include/tev/Image.h | 26 ++++++++++++--- src/Box.cpp | 8 +++++ src/Channel.cpp | 38 ++++++++++----------- src/Image.cpp | 4 +-- src/ImageCanvas.cpp | 18 +++++----- src/Task.cpp | 2 -- src/imageio/ClipboardImageLoader.cpp | 3 ++ src/imageio/DdsImageLoader.cpp | 3 ++ src/imageio/EmptyImageLoader.cpp | 3 ++ src/imageio/ExrImageLoader.cpp | 14 +++++--- src/imageio/PfmImageLoader.cpp | 3 ++ src/imageio/StbiImageLoader.cpp | 3 ++ 15 files changed, 144 insertions(+), 53 deletions(-) create mode 100644 include/tev/Box.h create mode 100644 src/Box.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b50419b62..13392dc3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,7 @@ set(TEV_SOURCES include/tev/imageio/StbiImageLoader.h src/imageio/StbiImageLoader.cpp include/tev/imageio/StbiLdrImageSaver.h src/imageio/StbiLdrImageSaver.cpp + include/tev/Box.h src/Box.cpp include/tev/Channel.h src/Channel.cpp include/tev/Common.h src/Common.cpp include/tev/FalseColor.h src/FalseColor.cpp diff --git a/include/tev/Box.h b/include/tev/Box.h new file mode 100644 index 000000000..31b4756fa --- /dev/null +++ b/include/tev/Box.h @@ -0,0 +1,49 @@ +// This file was developed by Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#pragma once + +#include + + +TEV_NAMESPACE_BEGIN + +template +struct Box { + using Vector = nanogui::Array; + + Box(const Vector& min, const Vector& max) : min{min}, max{max} {} + Box(const Vector& max) : Box{Vector{(T)0}, max} {} + Box() : Box{Vector{std::numeric_limits::max()}, Vector{std::numeric_limits::min()}} {} + + // Casting boxes of other types to this one + template + Box(const Box& other) : min{other.min}, max{other.max} {} + + Vector size() const { + return max - min; + } + + bool isValid() const { + bool result = true; + for (uint32_t i = 0; i < N_DIMS; ++i) { + result &= max[i] >= min[i]; + } + return result; + } + + operator bool() const { + return isValid(); + } + + Vector min, max; +}; + +using Box2f = Box; +using Box3f = Box; +using Box4f = Box; +using Box2i = Box; +using Box3i = Box; +using Box4i = Box; + +TEV_NAMESPACE_END diff --git a/include/tev/Channel.h b/include/tev/Channel.h index c8465026f..077b307c9 100644 --- a/include/tev/Channel.h +++ b/include/tev/Channel.h @@ -16,7 +16,7 @@ TEV_NAMESPACE_BEGIN class Channel { public: - Channel(const std::string& name, nanogui::Vector2i size); + Channel(const std::string& name, const nanogui::Vector2i& size); const std::string& name() const { return mName; @@ -34,12 +34,12 @@ class Channel { } float eval(nanogui::Vector2i index) const { - if (index.x() < 0 || index.x() >= mCols || - index.y() < 0 || index.y() >= mRows) { + if (index.x() < 0 || index.x() >= mSize.x() || + index.y() < 0 || index.y() >= mSize.y()) { return 0; } - return mData[index.x() + index.y() * mCols]; + return mData[index.x() + index.y() * mSize.x()]; } float& at(size_t index) { @@ -51,19 +51,19 @@ class Channel { } float& at(nanogui::Vector2i index) { - return at(index.x() + index.y() * mCols); + return at(index.x() + index.y() * mSize.x()); } float at(nanogui::Vector2i index) const { - return at(index.x() + index.y() * mCols); + return at(index.x() + index.y() * mSize.x()); } - size_t count() const { + size_t numPixels() const { return mData.size(); } - nanogui::Vector2i size() const { - return {mCols, mRows}; + const nanogui::Vector2i& size() const { + return mSize; } std::tuple minMaxMean() const { @@ -79,7 +79,7 @@ class Channel { max = f; } } - return {min, max, mean/count()}; + return {min, max, mean/numPixels()}; } Task divideByAsync(const Channel& other, int priority); @@ -101,7 +101,7 @@ class Channel { private: std::string mName; - int mCols, mRows; + nanogui::Vector2i mSize; std::vector mData; }; diff --git a/include/tev/Image.h b/include/tev/Image.h index a5a16929a..c96580a2f 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -26,12 +27,19 @@ struct ImageData { std::vector layers; nanogui::Matrix4f toRec709 = nanogui::Matrix4f{1.0f}; // Identity by default + Box2i dataWindow; + Box2i displayWindow; + nanogui::Vector2i size() const { - return channels.front().size(); + return dataWindow.size(); + } + + nanogui::Vector2i displaySize() const { + return displayWindow.size(); } - size_t count() const { - return channels.front().count(); + size_t numPixels() const { + return channels.front().numPixels(); } std::vector channelsInLayer(std::string layerName) const; @@ -126,8 +134,16 @@ class Image { return mData.size(); } - size_t count() const { - return mData.count(); + const Box2i& dataWindow() const { + return mData.dataWindow; + } + + const Box2i& displayWindow() const { + return mData.displayWindow; + } + + size_t numPixels() const { + return mData.numPixels(); } const std::vector& channelGroups() const { diff --git a/src/Box.cpp b/src/Box.cpp new file mode 100644 index 000000000..4cd9baf37 --- /dev/null +++ b/src/Box.cpp @@ -0,0 +1,8 @@ +// This file was developed by Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#include + +TEV_NAMESPACE_BEGIN + +TEV_NAMESPACE_END diff --git a/src/Channel.cpp b/src/Channel.cpp index bc1c82bf6..c7ce8173f 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -11,15 +11,13 @@ using namespace std; TEV_NAMESPACE_BEGIN -Channel::Channel(const std::string& name, nanogui::Vector2i size) -: mName{name} { - mCols = size.x(); - mRows = size.y(); - mData.resize((size_t)mCols * mRows); +Channel::Channel(const std::string& name, const nanogui::Vector2i& size) +: mName{name}, mSize{size} { + mData.resize((size_t)mSize.x() * mSize.y()); } Task Channel::divideByAsync(const Channel& other, int priority) { - co_await gThreadPool->parallelForAsync(0, other.count(), [&](size_t i) { + co_await gThreadPool->parallelForAsync(0, other.numPixels(), [&](size_t i) { if (other.at(i) != 0) { at(i) /= other.at(i); } else { @@ -29,11 +27,24 @@ Task Channel::divideByAsync(const Channel& other, int priority) { } Task Channel::multiplyWithAsync(const Channel& other, int priority) { - co_await gThreadPool->parallelForAsync(0, other.count(), [&](size_t i) { + co_await gThreadPool->parallelForAsync(0, other.numPixels(), [&](size_t i) { at(i) *= other.at(i); }, priority); } +void Channel::updateTile(int x, int y, int width, int height, const vector& newData) { + if (x < 0 || y < 0 || x + width > size().x() || y + height > size().y()) { + tlog::warning() << "Tile [" << x << "," << y << "," << width << "," << height << "] could not be updated because it does not fit into the channel's size " << size(); + return; + } + + for (int posY = 0; posY < height; ++posY) { + for (int posX = 0; posX < width; ++posX) { + at({x + posX, y + posY}) = newData[posX + posY * width]; + } + } +} + pair Channel::split(const string& channel) { size_t dotPosition = channel.rfind("."); if (dotPosition != string::npos) { @@ -69,17 +80,4 @@ Color Channel::color(string channel) { return Color(1.0f, 1.0f); } -void Channel::updateTile(int x, int y, int width, int height, const vector& newData) { - if (x < 0 || y < 0 || x + width > size().x() || y + height > size().y()) { - tlog::warning() << "Tile [" << x << "," << y << "," << width << "," << height << "] could not be updated because it does not fit into the channel's size " << size(); - return; - } - - for (int posY = 0; posY < height; ++posY) { - for (int posX = 0; posX < width; ++posX) { - at({x + posX, y + posY}) = newData[posX + posY * width]; - } - } -} - TEV_NAMESPACE_END diff --git a/src/Image.cpp b/src/Image.cpp index 6b897fe8d..602416a36 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -135,7 +135,7 @@ void ImageData::ensureValid() { for (const auto& c : channels) { if (c.size() != size()) { throw runtime_error{tfm::format( - "All channels must have the same size as their image. (%s:%dx%d != %dx%d)", + "All channels must have the same size as the data window. (%s:%dx%d != %dx%d)", c.name(), c.size().x(), c.size().y(), size().x(), size().y() )}; } @@ -209,7 +209,7 @@ Texture* Image::texture(const vector& channelNames) { }); auto& texture = mTextures.at(lookup).nanoguiTexture; - auto numPixels = count(); + auto numPixels = this->numPixels(); vector data(numPixels * 4); vector> tasks; diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 69c61e488..c0d15de24 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -321,7 +321,7 @@ std::vector ImageCanvas::getHdrImageData(bool divideAlpha, int priority) } const auto& channels = channelsFromImages(mImage, mReference, mRequestedChannelGroup, mMetric, priority); - auto numPixels = mImage->count(); + auto numPixels = mImage->numPixels(); if (channels.empty()) { return result; @@ -370,7 +370,7 @@ std::vector ImageCanvas::getLdrImageData(bool divideAlpha, int priority) c return result; } - auto numPixels = mImage->count(); + auto numPixels = mImage->numPixels(); auto floatData = getHdrImageData(divideAlpha, priority); // Store as LDR image. @@ -501,7 +501,7 @@ vector ImageCanvas::channelsFromImages( if (!reference) { gThreadPool->parallelFor(0, (int)channelNames.size(), [&](int i) { const auto* chan = image->channel(channelNames[i]); - for (size_t j = 0; j < chan->count(); ++j) { + for (size_t j = 0; j < chan->numPixels(); ++j) { result[i].at(j) = chan->eval(j); } }, priority); @@ -633,15 +633,15 @@ Task> ImageCanvas::computeCanvasStatistics( co_return result; } - auto numElements = image->count(); - std::vector indices(numElements*nChannels); + auto numPixels = image->numPixels(); + std::vector indices(numPixels * nChannels); vector> tasks; for (int i = 0; i < nChannels; ++i) { const auto& channel = flattened[i]; tasks.emplace_back( - gThreadPool->parallelForAsync(0, numElements, [&, i](size_t j) { - indices[j + i * numElements] = valToBin(channel.eval(j)); + gThreadPool->parallelForAsync(0, numPixels, [&, i](size_t j) { + indices[j + i * numPixels] = valToBin(channel.eval(j)); }, priority) ); } @@ -651,8 +651,8 @@ Task> ImageCanvas::computeCanvasStatistics( } co_await gThreadPool->parallelForAsync(0, nChannels, [&](int i) { - for (size_t j = 0; j < numElements; ++j) { - result->histogram[indices[j + i * numElements] + i * NUM_BINS] += alphaChannel ? alphaChannel->eval(j) : 1; + for (size_t j = 0; j < numPixels; ++j) { + result->histogram[indices[j + i * numPixels] + i * NUM_BINS] += alphaChannel ? alphaChannel->eval(j) : 1; } }, priority); diff --git a/src/Task.cpp b/src/Task.cpp index 106791772..5029f9de9 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -3,8 +3,6 @@ #include -using namespace std; - TEV_NAMESPACE_BEGIN TEV_NAMESPACE_END diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index 7ea90e216..c8965b4eb 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -119,6 +119,9 @@ Task> ClipboardImageLoader::load(istream& iStream, c // within a topmost root layer. result.layers.emplace_back(""); + // Clipboard images do not have non-trivial data and display windows. + result.dataWindow = result.displayWindow = result.channels.front().size(); + co_return {result, false}; } diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index 272bcc88c..dc0d82d2c 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -265,6 +265,9 @@ Task> DdsImageLoader::load(istream& iStream, const p // within a topmost root layer. result.layers.emplace_back(""); + // DDS images do not have non-trivial data and display windows. + result.dataWindow = result.displayWindow = result.channels.front().size(); + co_return {result, scratchImage.GetMetadata().IsPMAlpha()}; } diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index 29a5e8af0..07d06a554 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -61,6 +61,9 @@ Task> EmptyImageLoader::load(istream& iStream, const result.layers.emplace_back(layer); } + // Empty images currently do not support custom data and display windows + result.dataWindow = result.displayWindow = result.channels.front().size(); + co_return {result, true}; } diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index a999f6839..befab5930 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -183,13 +183,19 @@ Task> ExrImageLoader::load(istream& iStream, const p l_foundPart: Imf::InputPart file{multiPartFile, partIdx}; - Imath::Box2i dw = file.header().dataWindow(); - Vector2i size = {dw.max.x - dw.min.x + 1 , dw.max.y - dw.min.y + 1}; + Imath::Box2i dataWindow = file.header().dataWindow(); + Imath::Box2i displayWindow = file.header().dataWindow(); + Vector2i size = {dataWindow.max.x - dataWindow.min.x + 1 , dataWindow.max.y - dataWindow.min.y + 1}; if (size.x() == 0 || size.y() == 0) { throw invalid_argument{"EXR image has zero pixels."}; } + // EXR's display- and data windows have inclusive upper ends while tev's upper ends are exclusive. + // This allows easy conversion from window to size. Hence the +1. + result.dataWindow = {{dataWindow.min.x, dataWindow.min.y }, {dataWindow.max.x+1, dataWindow.max.y+1 }}; + result.displayWindow = {{displayWindow.min.x, displayWindow.min.y}, {displayWindow.max.x+1, displayWindow.max.y+1}}; + // Allocate raw channels on the heap, because it'll be references // by nested parallel for coroutine. auto rawChannels = std::make_unique>(); @@ -231,11 +237,11 @@ Task> ExrImageLoader::load(istream& iStream, const p }, priority); for (size_t i = 0; i < rawChannels->size(); ++i) { - rawChannels->at(i).registerWith(frameBuffer, dw); + rawChannels->at(i).registerWith(frameBuffer, dataWindow); } file.setFrameBuffer(frameBuffer); - file.readPixels(dw.min.y, dw.max.y); + file.readPixels(dataWindow.min.y, dataWindow.max.y); for (const auto& rawChannel : *rawChannels) { result.channels.emplace_back(Channel{rawChannel.name(), size}); diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index bafd8832c..28d1c3179 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -110,6 +110,9 @@ Task> PfmImageLoader::load(istream& iStream, const p // within a topmost root layer. result.layers.emplace_back(""); + // PFM images do not have custom data and display windows. + result.dataWindow = result.displayWindow = result.channels.front().size(); + co_return {result, false}; } diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index 450181430..356a86cb8 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -107,6 +107,9 @@ Task> StbiImageLoader::load(istream& iStream, const // within a topmost root layer. result.layers.emplace_back(""); + // STBI-loaded images do not have custom data and display windows. + result.dataWindow = result.displayWindow = result.channels.front().size(); + co_return {result, false}; } From 86a0bb61e239707b64f8431677cc82ffd8e572ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 15 Aug 2021 16:29:42 +0200 Subject: [PATCH 46/83] Image positioning based on display window rather than data window --- include/tev/Box.h | 4 ++++ include/tev/Image.h | 4 ++++ src/ImageCanvas.cpp | 8 ++++---- src/imageio/ExrImageLoader.cpp | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/include/tev/Box.h b/include/tev/Box.h index 31b4756fa..50352863e 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -24,6 +24,10 @@ struct Box { return max - min; } + Vector middle() const { + return (min + max) / (T)2; + } + bool isValid() const { bool result = true; for (uint32_t i = 0; i < N_DIMS; ++i) { diff --git a/include/tev/Image.h b/include/tev/Image.h index c96580a2f..051a414c7 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -142,6 +142,10 @@ class Image { return mData.displayWindow; } + nanogui::Vector2f centerDisplayOffset() const { + return Box2f{dataWindow()}.middle() - Box2f{displayWindow()}.middle(); + } + size_t numPixels() const { return mData.numPixels(); } diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index c0d15de24..8ee9b4996 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -305,8 +305,8 @@ float ImageCanvas::applyMetric(float image, float reference, EMetric metric) { } void ImageCanvas::fitImageToScreen(const Image& image) { - Vector2f nanoguiImageSize = Vector2f{image.size()} / mPixelRatio; - mTransform = Matrix3f::scale(Vector2f{m_size} / min(nanoguiImageSize.x(), nanoguiImageSize.y())); + Vector2f nanoguiImageSize = Vector2f{image.displayWindow().size()} / mPixelRatio; + mTransform = Matrix3f::scale(Vector2f{min(m_size.x() / nanoguiImageSize.x(), m_size.y() / nanoguiImageSize.y())}); } void ImageCanvas::resetTransform() { @@ -701,7 +701,7 @@ Matrix3f ImageCanvas::transform(const Image* image) { Matrix3f::scale(Vector2f{2.0f / m_size.x(), -2.0f / m_size.y()}) * mTransform * Matrix3f::scale(Vector2f{1.0f / mPixelRatio}) * - Matrix3f::translate(pixelOffset(image->size())) * + Matrix3f::translate(image->centerDisplayOffset() + pixelOffset(image->size())) * Matrix3f::scale(Vector2f{image->size()}) * Matrix3f::translate(Vector2f{-0.5f}); } @@ -716,7 +716,7 @@ Matrix3f ImageCanvas::textureToNanogui(const Image* image) { Matrix3f::translate(0.5f * Vector2f{m_size}) * mTransform * Matrix3f::scale(Vector2f{1.0f / mPixelRatio}) * - Matrix3f::translate(-0.5f * Vector2f{image->size()} + pixelOffset(image->size())); + Matrix3f::translate(-0.5f * Vector2f{image->size()} + image->centerDisplayOffset() + pixelOffset(image->size())); } TEV_NAMESPACE_END diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index befab5930..9631fa039 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -184,7 +184,7 @@ Task> ExrImageLoader::load(istream& iStream, const p Imf::InputPart file{multiPartFile, partIdx}; Imath::Box2i dataWindow = file.header().dataWindow(); - Imath::Box2i displayWindow = file.header().dataWindow(); + Imath::Box2i displayWindow = file.header().displayWindow(); Vector2i size = {dataWindow.max.x - dataWindow.min.x + 1 , dataWindow.max.y - dataWindow.min.y + 1}; if (size.x() == 0 || size.y() == 0) { From f348e23cdb7685313d56f983f36b918df46ab50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 15 Aug 2021 22:23:17 +0200 Subject: [PATCH 47/83] Fix regression in background color picker --- include/tev/ImageCanvas.h | 2 +- src/ImageViewer.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index e3e72f938..6d04fb862 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -100,7 +100,7 @@ class ImageCanvas : public nanogui::Canvas { return applyMetric(value, reference, mMetric); } - const nanogui::Color& backgroundColor() { + auto backgroundColor() { return mShader->backgroundColor(); } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index edaf0c914..a01d376bb 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -163,14 +163,14 @@ ImageViewer::ImageViewer(const shared_ptr& imagesLoader, popup->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 10}); new Label{popup, "Background Color"}; - auto colorwheel = new ColorWheel{popup, mImageCanvas->background_color()}; + auto colorwheel = new ColorWheel{popup, mImageCanvas->backgroundColor()}; colorwheel->set_color(popupBtn->background_color()); new Label{popup, "Background Alpha"}; auto bgAlphaSlider = new Slider{popup}; bgAlphaSlider->set_range({0.0f, 1.0f}); bgAlphaSlider->set_callback([this](float value) { - auto col = mImageCanvas->background_color(); + auto col = mImageCanvas->backgroundColor(); mImageCanvas->setBackgroundColor(Color{ col.r(), col.g(), From f99f66aa03ed832c869843622e3aa92147d6e0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 15 Aug 2021 22:24:17 +0200 Subject: [PATCH 48/83] Add correctly positioned display- and data window overlay TODO: needs to be neater --- include/tev/Box.h | 4 +- include/tev/ImageCanvas.h | 5 + src/ImageCanvas.cpp | 252 ++++++++++++++++++++++++-------------- 3 files changed, 170 insertions(+), 91 deletions(-) diff --git a/include/tev/Box.h b/include/tev/Box.h index 50352863e..9dfe79913 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -36,8 +36,8 @@ struct Box { return result; } - operator bool() const { - return isValid(); + bool operator==(const Box& other) const { + return min == other.min && max == other.max; } Vector min, max; diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 6d04fb862..304a2aa64 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -143,12 +143,17 @@ class ImageCanvas : public nanogui::Canvas { int priority ); + void drawPixelValuesAsText(NVGcontext *ctx); + void drawCoordinateSystem(NVGcontext *ctx); + void drawEdgeShadows(NVGcontext *ctx); + nanogui::Vector2f pixelOffset(const nanogui::Vector2i& size) const; // Assembles the transform from canonical space to // the [-1, 1] square for the current image. nanogui::Matrix3f transform(const Image* image); nanogui::Matrix3f textureToNanogui(const Image* image); + nanogui::Matrix3f displayWindowToNanogui(const Image* image); float mPixelRatio = 1; float mExposure = 0; diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 8ee9b4996..176a07bb8 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -94,108 +94,171 @@ void ImageCanvas::draw_contents() { ); } -void ImageCanvas::draw(NVGcontext *ctx) { - Canvas::draw(ctx); - - if (mImage) { - auto texToNano = textureToNanogui(mImage.get()); - auto nanoToTex = inverse(texToNano); +void ImageCanvas::drawPixelValuesAsText(NVGcontext* ctx) { + TEV_ASSERT(mImage, "Can only draw pixel values if there exists an image."); - Vector2f pixelSize = texToNano * Vector2f{1.0f} - texToNano * Vector2f{0.0f}; + auto texToNano = textureToNanogui(mImage.get()); + auto nanoToTex = inverse(texToNano); - Vector2f topLeft = (nanoToTex * Vector2f{0.0f}); - Vector2f bottomRight = (nanoToTex * Vector2f{m_size}); + Vector2f pixelSize = texToNano * Vector2f{1.0f} - texToNano * Vector2f{0.0f}; - Vector2i startIndices = Vector2i{ - static_cast(floor(topLeft.x())), - static_cast(floor(topLeft.y())), - }; + Vector2f topLeft = (nanoToTex * Vector2f{0.0f}); + Vector2f bottomRight = (nanoToTex * Vector2f{m_size}); - Vector2i endIndices = Vector2i{ - static_cast(ceil(bottomRight.x())), - static_cast(ceil(bottomRight.y())), - }; + Vector2i startIndices = Vector2i{ + static_cast(floor(topLeft.x())), + static_cast(floor(topLeft.y())), + }; - if (pixelSize.x() > 50 && pixelSize.x() < 1024) { - vector channels = mImage->channelsInGroup(mRequestedChannelGroup); - // Remove duplicates - channels.erase(unique(begin(channels), end(channels)), end(channels)); + Vector2i endIndices = Vector2i{ + static_cast(ceil(bottomRight.x())), + static_cast(ceil(bottomRight.y())), + }; - vector colors; - for (const auto& channel : channels) { - colors.emplace_back(Channel::color(channel)); - } + if (pixelSize.x() > 50 && pixelSize.x() < 1024) { + vector channels = mImage->channelsInGroup(mRequestedChannelGroup); + // Remove duplicates + channels.erase(unique(begin(channels), end(channels)), end(channels)); - float fontSize = pixelSize.x() / 6; - if (colors.size() > 4) { - fontSize *= 4.0f / colors.size(); - } - float fontAlpha = min(min(1.0f, (pixelSize.x() - 50) / 30), (1024 - pixelSize.x()) / 256); - - nvgFontSize(ctx, fontSize); - nvgFontFace(ctx, "sans"); - nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); - - auto* glfwWindow = screen()->glfw_window(); - bool altHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_ALT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_ALT); - - Vector2i cur; - vector values; - for (cur.y() = startIndices.y(); cur.y() < endIndices.y(); ++cur.y()) { - for (cur.x() = startIndices.x(); cur.x() < endIndices.x(); ++cur.x()) { - Vector2i nano = Vector2i{texToNano * (Vector2f{cur} + Vector2f{0.5f})}; - getValuesAtNanoPos(nano, values, channels); - - TEV_ASSERT(values.size() >= colors.size(), "Can not have more values than channels."); - - for (size_t i = 0; i < colors.size(); ++i) { - string str; - Vector2f pos; - - if (altHeld) { - float tonemappedValue = Channel::tail(channels[i]) == "A" ? values[i] : toSRGB(values[i]); - unsigned char discretizedValue = (char)(tonemappedValue * 255 + 0.5f); - str = tfm::format("%02X", discretizedValue); - - pos = Vector2f{ - m_pos.x() + nano.x() + (i - 0.5f * (colors.size() - 1)) * fontSize * 0.88f, - (float)m_pos.y() + nano.y(), - }; - } else { - str = tfm::format("%.4f", values[i]); - - pos = Vector2f{ - (float)m_pos.x() + nano.x(), - m_pos.y() + nano.y() + (i - 0.5f * (colors.size() - 1)) * fontSize, - }; - } + vector colors; + for (const auto& channel : channels) { + colors.emplace_back(Channel::color(channel)); + } - Color col = colors[i]; - nvgFillColor(ctx, Color(col.r(), col.g(), col.b(), fontAlpha)); - drawTextWithShadow(ctx, pos.x(), pos.y(), str, fontAlpha); + float fontSize = pixelSize.x() / 6; + if (colors.size() > 4) { + fontSize *= 4.0f / colors.size(); + } + float fontAlpha = min(min(1.0f, (pixelSize.x() - 50) / 30), (1024 - pixelSize.x()) / 256); + + nvgFontSize(ctx, fontSize); + nvgFontFace(ctx, "sans"); + nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); + + auto* glfwWindow = screen()->glfw_window(); + bool altHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_ALT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_ALT); + + Vector2i cur; + vector values; + for (cur.y() = startIndices.y(); cur.y() < endIndices.y(); ++cur.y()) { + for (cur.x() = startIndices.x(); cur.x() < endIndices.x(); ++cur.x()) { + Vector2i nano = Vector2i{texToNano * (Vector2f{cur} + Vector2f{0.5f})}; + getValuesAtNanoPos(nano, values, channels); + + TEV_ASSERT(values.size() >= colors.size(), "Can not have more values than channels."); + + for (size_t i = 0; i < colors.size(); ++i) { + string str; + Vector2f pos; + + if (altHeld) { + float tonemappedValue = Channel::tail(channels[i]) == "A" ? values[i] : toSRGB(values[i]); + unsigned char discretizedValue = (char)(tonemappedValue * 255 + 0.5f); + str = tfm::format("%02X", discretizedValue); + + pos = Vector2f{ + m_pos.x() + nano.x() + (i - 0.5f * (colors.size() - 1)) * fontSize * 0.88f, + (float)m_pos.y() + nano.y(), + }; + } else { + str = tfm::format("%.4f", values[i]); + + pos = Vector2f{ + (float)m_pos.x() + nano.x(), + m_pos.y() + nano.y() + (i - 0.5f * (colors.size() - 1)) * fontSize, + }; } + + Color col = colors[i]; + nvgFillColor(ctx, Color(col.r(), col.g(), col.b(), fontAlpha)); + drawTextWithShadow(ctx, pos.x(), pos.y(), str, fontAlpha); } } } } +} - // If we're not in fullscreen mode draw an inner drop shadow. (adapted from Window) - if (m_pos.x() != 0) { - int ds = m_theme->m_window_drop_shadow_size, cr = m_theme->m_window_corner_radius; - NVGpaint shadowPaint = nvgBoxGradient( - ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y(), cr * 2, ds * 2, - m_theme->m_transparent, m_theme->m_drop_shadow +void ImageCanvas::drawCoordinateSystem(NVGcontext* ctx) { + TEV_ASSERT(mImage, "Can only draw coordinate system if there exists an image."); + + auto displayWindowToNano = displayWindowToNanogui(mImage.get()); + + auto drawWindow = [&](const Box2i& window, const Color& color, const std::string& name) { + Vector2i topLeft = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.min.x(), (float)window.min.y()}}; + Vector2i topRight = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.max.x(), (float)window.min.y()}}; + Vector2i bottomLeft = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.min.x(), (float)window.max.y()}}; + Vector2i bottomRight = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.max.x(), (float)window.max.y()}}; + + NVGpaint shadowPaint = nvgLinearGradient(ctx, + m_pos.x(), m_pos.y(), m_pos.x(), m_pos.y()+m_size.y(), + color, color ); + NVGcolor regularTextColor = Color(150, 255);// : Color(190, 255); + NVGcolor hightlightedTextColor = Color(190, 255); + + float fontSize = 30; + float strokeWidth = 3.0f; + nvgSave(ctx); - nvgResetScissor(ctx); + nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM); nvgBeginPath(ctx); - nvgRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y()); - nvgRoundedRect(ctx, m_pos.x() + ds, m_pos.y() + ds, m_size.x() - 2 * ds, m_size.y() - 2 * ds, cr); - nvgPathWinding(ctx, NVG_HOLE); - nvgFillPaint(ctx, shadowPaint); - nvgFill(ctx); + nvgMoveTo(ctx, bottomLeft.x(), bottomLeft.y()); + nvgLineTo(ctx, topLeft.x(), topLeft.y()); + nvgLineTo(ctx, topRight.x(), topRight.y()); + nvgLineTo(ctx, bottomRight.x(), bottomRight.y()); + nvgLineTo(ctx, bottomLeft.x(), bottomLeft.y()); + nvgStrokeWidth(ctx, strokeWidth); + nvgStrokePaint(ctx, shadowPaint); + nvgStroke(ctx); + + nvgFontFace(ctx, "sans-bold"); + nvgFontSize(ctx, fontSize); + nvgFillColor(ctx, color); + // drawTextWithShadow(ctx, topLeft.x(), topLeft.y() - 20, name, 1.0f); + nvgText(ctx, topLeft.x() - strokeWidth/2 - 1.0f, topLeft.y(), name.c_str(), NULL); + nvgRestore(ctx); + + }; + + drawWindow(mImage->displayWindow(), Color(0.7f, 0.4f, 0.4f, 1.0f), "Display window"); + drawWindow(mImage->dataWindow(), Color(0.35f, 0.35f, 0.8f, 1.0f), "Data window"); +} + +void ImageCanvas::drawEdgeShadows(NVGcontext* ctx) { + int ds = m_theme->m_window_drop_shadow_size, cr = m_theme->m_window_corner_radius; + NVGpaint shadowPaint = nvgBoxGradient( + ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y(), cr * 2, ds * 2, + m_theme->m_transparent, m_theme->m_drop_shadow + ); + + nvgSave(ctx); + nvgResetScissor(ctx); + nvgBeginPath(ctx); + nvgRect(ctx, m_pos.x(), m_pos.y(), m_size.x(), m_size.y()); + nvgRoundedRect(ctx, m_pos.x() + ds, m_pos.y() + ds, m_size.x() - 2 * ds, m_size.y() - 2 * ds, cr); + nvgPathWinding(ctx, NVG_HOLE); + nvgFillPaint(ctx, shadowPaint); + nvgFill(ctx); + nvgRestore(ctx); +} + +void ImageCanvas::draw(NVGcontext* ctx) { + Canvas::draw(ctx); + + if (mImage) { + drawPixelValuesAsText(ctx); + + // If the coordinate system is in any sort of way non-trivial, draw it! + if (mImage->dataWindow() != mImage->displayWindow() || mImage->displayWindow().min != Vector2i{0}) { + drawCoordinateSystem(ctx); + } + } + + // If we're not in fullscreen mode draw an inner drop shadow. (adapted from Window) + if (m_pos.x() != 0) { + drawEdgeShadows(ctx); } } @@ -220,8 +283,8 @@ float ImageCanvas::applyExposureAndOffset(float value) const { return pow(2.0f, mExposure) * value + mOffset; } -Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i mousePos) { - Vector2f imagePos = inverse(textureToNanogui(&image)) * Vector2f{mousePos}; +Vector2i ImageCanvas::getImageCoords(const Image& image, Vector2i nanoPos) { + Vector2f imagePos = inverse(textureToNanogui(&image)) * Vector2f{nanoPos}; return { static_cast(floor(imagePos.x())), static_cast(floor(imagePos.y())), @@ -684,10 +747,11 @@ Vector2f ImageCanvas::pixelOffset(const Vector2i& size) const { // axes are implicitly shifted by half a pixel due to the centering operation. // Additionally, add 0.1111111 such that our final position is almost never 0 // modulo our pixel ratio, which again avoids aligned pixel boundaries with texels. - return Vector2f{ - size.x() % 2 == 0 ? 0.5f : 0.0f, - size.y() % 2 == 0 ? -0.5f : 0.0f, - } + Vector2f{0.1111111f}; + // return Vector2f{ + // size.x() % 2 == 0 ? 0.5f : 0.0f, + // size.y() % 2 == 0 ? -0.5f : 0.0f, + // } + Vector2f{0.1111111f}; + return Vector2f{0.1111111f}; } Matrix3f ImageCanvas::transform(const Image* image) { @@ -719,4 +783,14 @@ Matrix3f ImageCanvas::textureToNanogui(const Image* image) { Matrix3f::translate(-0.5f * Vector2f{image->size()} + image->centerDisplayOffset() + pixelOffset(image->size())); } +Matrix3f ImageCanvas::displayWindowToNanogui(const Image* image) { + if (!image) { + return Matrix3f::scale(Vector2f{1.0f}); + } + + // Shift texture coordinates by the data coordinate offset. + // It's that simple. + return textureToNanogui(image) * Matrix3f::translate(-image->dataWindow().min); +} + TEV_NAMESPACE_END From cd4fb6e09c84f36b77b7f7fbcca93989d9c2a800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 08:56:04 +0200 Subject: [PATCH 49/83] Simplify image loader code (automate away a bunch of responsibilities) # Conflicts: # include/tev/Image.h # src/imageio/ExrImageLoader.cpp --- include/tev/Image.h | 3 +- include/tev/imageio/ClipboardImageLoader.h | 2 +- include/tev/imageio/DdsImageLoader.h | 2 +- include/tev/imageio/EmptyImageLoader.h | 2 +- include/tev/imageio/ExrImageLoader.h | 2 +- include/tev/imageio/ImageLoader.h | 2 +- include/tev/imageio/PfmImageLoader.h | 2 +- include/tev/imageio/StbiImageLoader.h | 2 +- src/Image.cpp | 77 ++++++++++++++++++---- src/imageio/ClipboardImageLoader.cpp | 35 ++-------- src/imageio/DdsImageLoader.cpp | 35 ++-------- src/imageio/EmptyImageLoader.cpp | 13 +--- src/imageio/ExrImageLoader.cpp | 26 +++++--- src/imageio/PfmImageLoader.cpp | 31 ++------- src/imageio/StbiImageLoader.cpp | 35 ++-------- 15 files changed, 119 insertions(+), 150 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index 051a414c7..673fe4461 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -26,6 +26,7 @@ struct ImageData { std::vector channels; std::vector layers; nanogui::Matrix4f toRec709 = nanogui::Matrix4f{1.0f}; // Identity by default + bool hasPremultipliedAlpha; Box2i dataWindow; Box2i displayWindow; @@ -51,7 +52,7 @@ struct ImageData { Task multiplyAlpha(int priority); Task unmultiplyAlpha(int priority); - void ensureValid(); + Task ensureValid(const std::string& channelSelector, int taskPriority); bool hasChannel(const std::string& channelName) const { return channel(channelName) != nullptr; diff --git a/include/tev/imageio/ClipboardImageLoader.h b/include/tev/imageio/ClipboardImageLoader.h index eeb303da3..4763d2e1a 100644 --- a/include/tev/imageio/ClipboardImageLoader.h +++ b/include/tev/imageio/ClipboardImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ClipboardImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "clipboard"; diff --git a/include/tev/imageio/DdsImageLoader.h b/include/tev/imageio/DdsImageLoader.h index 219594455..5e9e54fb6 100644 --- a/include/tev/imageio/DdsImageLoader.h +++ b/include/tev/imageio/DdsImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class DdsImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "DDS"; diff --git a/include/tev/imageio/EmptyImageLoader.h b/include/tev/imageio/EmptyImageLoader.h index d7870595a..9fec55639 100644 --- a/include/tev/imageio/EmptyImageLoader.h +++ b/include/tev/imageio/EmptyImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class EmptyImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "IPC"; diff --git a/include/tev/imageio/ExrImageLoader.h b/include/tev/imageio/ExrImageLoader.h index 770ce9b47..898384910 100644 --- a/include/tev/imageio/ExrImageLoader.h +++ b/include/tev/imageio/ExrImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ExrImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "OpenEXR"; diff --git a/include/tev/imageio/ImageLoader.h b/include/tev/imageio/ImageLoader.h index 6dec1a92e..b90c086b2 100644 --- a/include/tev/imageio/ImageLoader.h +++ b/include/tev/imageio/ImageLoader.h @@ -22,7 +22,7 @@ class ImageLoader { virtual bool canLoadFile(std::istream& iStream) const = 0; // Return loaded image data as well as whether that data has the alpha channel pre-multiplied or not. - virtual Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; + virtual Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; virtual std::string name() const = 0; diff --git a/include/tev/imageio/PfmImageLoader.h b/include/tev/imageio/PfmImageLoader.h index 124756f2e..ca17c7ae4 100644 --- a/include/tev/imageio/PfmImageLoader.h +++ b/include/tev/imageio/PfmImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class PfmImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "PFM"; diff --git a/include/tev/imageio/StbiImageLoader.h b/include/tev/imageio/StbiImageLoader.h index 369ca3bb2..215d87917 100644 --- a/include/tev/imageio/StbiImageLoader.h +++ b/include/tev/imageio/StbiImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class StbiImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "STBI"; diff --git a/src/Image.cpp b/src/Image.cpp index 602416a36..300908aca 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -104,6 +104,10 @@ void ImageData::alphaOperation(const function& f } Task ImageData::multiplyAlpha(int priority) { + if (hasPremultipliedAlpha) { + throw runtime_error{"Can't multiply with alpha twice."}; + } + vector> tasks; alphaOperation([&] (Channel& target, const Channel& alpha) { tasks.emplace_back(target.multiplyWithAsync(alpha, priority)); @@ -111,9 +115,15 @@ Task ImageData::multiplyAlpha(int priority) { for (auto& task : tasks) { co_await task; } + + hasPremultipliedAlpha = true; } Task ImageData::unmultiplyAlpha(int priority) { + if (!hasPremultipliedAlpha) { + throw runtime_error{"Can't divide by alpha twice."}; + } + vector> tasks; alphaOperation([&] (Channel& target, const Channel& alpha) { tasks.emplace_back(target.divideByAsync(alpha, priority)); @@ -121,17 +131,24 @@ Task ImageData::unmultiplyAlpha(int priority) { for (auto& task : tasks) { co_await task; } -} -void ImageData::ensureValid() { - if (layers.empty()) { - throw runtime_error{"Images must have at least one layer."}; - } + hasPremultipliedAlpha = false; +} +Task ImageData::ensureValid(const string& channelSelector, int taskPriority) { if (channels.empty()) { throw runtime_error{"Images must have at least one channel."}; } + // No data window? Default to the channel size + if (!dataWindow.isValid()) { + dataWindow = channels.front().size(); + } + + if (!displayWindow.isValid()) { + displayWindow = channels.front().size(); + } + for (const auto& c : channels) { if (c.size() != size()) { throw runtime_error{tfm::format( @@ -140,6 +157,43 @@ void ImageData::ensureValid() { )}; } } + + if (!channelSelector.empty()) { + vector> matches; + for (size_t i = 0; i < channels.size(); ++i) { + size_t matchId; + if (matchesFuzzy(channels[i].name(), channelSelector, &matchId)) { + matches.emplace_back(matchId, i); + } + } + + sort(begin(matches), end(matches)); + + // Prune and sort channels by the channel selector + vector tmp = move(channels); + channels.clear(); + + for (const auto& match : matches) { + channels.emplace_back(move(tmp[match.second])); + } + } + + if (layers.empty()) { + set layerNames; + for (auto& c : channels) { + layerNames.insert(Channel::head(c.name())); + } + + for (const string& l : layerNames) { + layers.emplace_back(l); + } + } + + if (!hasPremultipliedAlpha) { + co_await multiplyAlpha(taskPriority); + } + + TEV_ASSERT(hasPremultipliedAlpha, "tev assumes an internal pre-multiplied-alpha representation."); } atomic Image::sId(0); @@ -148,8 +202,8 @@ Image::Image(int id, const class path& path, ImageData&& data, const string& cha : mPath{path}, mChannelSelector{channelSelector}, mData{std::move(data)}, mId{id} { mName = channelSelector.empty() ? path.str() : tfm::format("%s:%s", path, channelSelector); - for (const auto& layer : mData.layers) { - auto groups = getGroupedChannels(layer); + for (const auto& l : mData.layers) { + auto groups = getGroupedChannels(l); mChannelGroups.insert(end(mChannelGroups), begin(groups), end(groups)); } } @@ -464,13 +518,8 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s int taskPriority = -imageId; loadMethod = imageLoader->name(); - auto [data, hasPremultipliedAlpha] = co_await imageLoader->load(iStream, path, channelSelector, taskPriority); - data.ensureValid(); - - // We assume an internal pre-multiplied-alpha representation - if (!hasPremultipliedAlpha) { - co_await data.multiplyAlpha(taskPriority); - } + auto data = co_await imageLoader->load(iStream, path, channelSelector, taskPriority); + co_await data.ensureValid(channelSelector, taskPriority); // Convert chromaticities to sRGB / Rec 709 if they aren't already. co_await data.convertToRec709(taskPriority); diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index c8965b4eb..c159c4aff 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -23,7 +23,7 @@ bool ClipboardImageLoader::canLoadFile(istream& iStream) const { return result; } -Task> ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; char magic[4]; @@ -58,7 +58,7 @@ Task> ClipboardImageLoader::load(istream& iStream, c auto numBytes = (size_t)numBytesPerRow * size.y(); int alphaChannelIndex = 3; - vector channels = makeNChannels(numChannels, size); + result.channels = makeNChannels(numChannels, size); vector data(numBytes); iStream.read(reinterpret_cast(data.data()), numBytes); @@ -89,40 +89,19 @@ Task> ClipboardImageLoader::load(istream& iStream, c for (int c = numChannels-1; c >= 0; --c) { unsigned char val = data[baseIdx + shifts[c]]; if (c == alphaChannelIndex) { - channels[c].at({x, y}) = val / 255.0f; + result.channels[c].at({x, y}) = val / 255.0f; } else { - float alpha = premultipliedAlpha ? channels[alphaChannelIndex].at({x, y}) : 1.0f; + float alpha = premultipliedAlpha ? result.channels[alphaChannelIndex].at({x, y}) : 1.0f; float alphaFactor = alpha == 0 ? 0 : (1.0f / alpha); - channels[c].at({x, y}) = toLinear(val / 255.0f * alphaFactor); + result.channels[c].at({x, y}) = toLinear(val / 255.0f * alphaFactor); } } } }, priority); - vector> matches; - for (size_t i = 0; i < channels.size(); ++i) { - size_t matchId; - if (matchesFuzzy(channels[i].name(), channelSelector, &matchId)) { - matches.emplace_back(matchId, i); - } - } - - if (!channelSelector.empty()) { - sort(begin(matches), end(matches)); - } - - for (const auto& match : matches) { - result.channels.emplace_back(move(channels[match.second])); - } - - // The clipboard can not contain layers, so all channels simply reside - // within a topmost root layer. - result.layers.emplace_back(""); - - // Clipboard images do not have non-trivial data and display windows. - result.dataWindow = result.displayWindow = result.channels.front().size(); + result.hasPremultipliedAlpha = false; - co_return {result, false}; + co_return result; } TEV_NAMESPACE_END diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index dc0d82d2c..d79c305b5 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -153,7 +153,7 @@ static int getDxgiChannelCount(DXGI_FORMAT fmt) { } } -Task> DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { // COM must be initialized on the thread executing load(). if (CoInitializeEx(nullptr, COINIT_MULTITHREADED) != S_OK) { throw invalid_argument{"Failed to initialize COM."}; @@ -208,7 +208,7 @@ Task> DdsImageLoader::load(istream& iStream, const p std::swap(scratchImage, convertedImage); } - vector channels = makeNChannels(numChannels, { (int)metadata.width, (int)metadata.height }); + result.channels = makeNChannels(numChannels, { (int)metadata.width, (int)metadata.height }); auto numPixels = (size_t)metadata.width * metadata.height; if (numPixels == 0) { @@ -224,7 +224,7 @@ Task> DdsImageLoader::load(istream& iStream, const p co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { - channels[c].at(i) = typedData[baseIdx + c]; + result.channels[c].at(i) = typedData[baseIdx + c]; } }, priority); } else { @@ -237,38 +237,17 @@ Task> DdsImageLoader::load(istream& iStream, const p size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == 3) { - channels[c].at(i) = typedData[baseIdx + c]; + result.channels[c].at(i) = typedData[baseIdx + c]; } else { - channels[c].at(i) = toLinear(typedData[baseIdx + c]); + result.channels[c].at(i) = toLinear(typedData[baseIdx + c]); } } }, priority); } - vector> matches; - for (size_t i = 0; i < channels.size(); ++i) { - size_t matchId; - if (matchesFuzzy(channels[i].name(), channelSelector, &matchId)) { - matches.emplace_back(matchId, i); - } - } - - if (!channelSelector.empty()) { - sort(begin(matches), end(matches)); - } - - for (const auto& match : matches) { - result.channels.emplace_back(move(channels[match.second])); - } - - // DDS can not contain layers, so all channels simply reside - // within a topmost root layer. - result.layers.emplace_back(""); - - // DDS images do not have non-trivial data and display windows. - result.dataWindow = result.displayWindow = result.channels.front().size(); + result.hasPremultipliedAlpha = scratchImage.GetMetadata().IsPMAlpha(); - co_return {result, scratchImage.GetMetadata().IsPMAlpha()}; + co_return result; } TEV_NAMESPACE_END diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index 07d06a554..432351a17 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -22,7 +22,7 @@ bool EmptyImageLoader::canLoadFile(istream& iStream) const { return result; } -Task> EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { +Task EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { ImageData result; string magic; @@ -39,7 +39,6 @@ Task> EmptyImageLoader::load(istream& iStream, const throw invalid_argument{"Image has zero pixels."}; } - set layerNames; for (int i = 0; i < nChannels; ++i) { // The following lines decode strings by prefix length. // The reason for using sthis encoding is to allow arbitrary characters, @@ -54,17 +53,11 @@ Task> EmptyImageLoader::load(istream& iStream, const result.channels.emplace_back(Channel{channelName, size}); result.channels.back().setZero(); - layerNames.insert(Channel::head(channelName)); } - for (const string& layer : layerNames) { - result.layers.emplace_back(layer); - } - - // Empty images currently do not support custom data and display windows - result.dataWindow = result.displayWindow = result.channels.front().size(); + result.hasPremultipliedAlpha = true; - co_return {result, true}; + co_return result; } TEV_NAMESPACE_END diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 9631fa039..63cbb3e32 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -155,7 +155,7 @@ class RawChannel { vector mData; }; -Task> ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { +Task ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { ImageData result; StdIStream stdIStream{iStream, path.str().c_str()}; @@ -196,13 +196,26 @@ Task> ExrImageLoader::load(istream& iStream, const p result.dataWindow = {{dataWindow.min.x, dataWindow.min.y }, {dataWindow.max.x+1, dataWindow.max.y+1 }}; result.displayWindow = {{displayWindow.min.x, displayWindow.min.y}, {displayWindow.max.x+1, displayWindow.max.y+1}}; + if (!result.dataWindow.isValid()) { + throw invalid_argument{tfm::format( + "EXR image has invalid data window: [%d,%d] - [%d,%d]", + result.dataWindow.min.x(), result.dataWindow.min.y(), result.dataWindow.max.x(), result.dataWindow.max.y() + )}; + } + + if (!result.displayWindow.isValid()) { + throw invalid_argument{tfm::format( + "EXR image has invalid display window: [%d,%d] - [%d,%d]", + result.displayWindow.min.x(), result.displayWindow.min.y(), result.displayWindow.max.x(), result.displayWindow.max.y() + )}; + } + // Allocate raw channels on the heap, because it'll be references // by nested parallel for coroutine. auto rawChannels = std::make_unique>(); Imf::FrameBuffer frameBuffer; const Imf::ChannelList& imfChannels = file.header().channels(); - set layerNames; using match_t = pair; vector matches; @@ -210,7 +223,6 @@ Task> ExrImageLoader::load(istream& iStream, const p size_t matchId; if (matchesFuzzy(c.name(), channelSelector, &matchId)) { matches.emplace_back(matchId, c); - layerNames.insert(Channel::head(c.name())); } } @@ -228,10 +240,6 @@ Task> ExrImageLoader::load(istream& iStream, const p throw invalid_argument{tfm::format("No channels match '%s'.", channelSelector)}; } - for (const string& layer : layerNames) { - result.layers.emplace_back(layer); - } - co_await gThreadPool->parallelForAsync(0, (int)rawChannels->size(), [c = rawChannels.get(), size](int i) { c->at(i).resize((size_t)size.x() * size.y()); }, priority); @@ -281,7 +289,9 @@ Task> ExrImageLoader::load(istream& iStream, const p } } - co_return {result, true}; + result.hasPremultipliedAlpha = true; + + co_return result; } TEV_NAMESPACE_END diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index 28d1c3179..7e9698080 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -21,7 +21,7 @@ bool PfmImageLoader::canLoadFile(istream& iStream) const { return result; } -Task> PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; string magic; @@ -48,7 +48,7 @@ Task> PfmImageLoader::load(istream& iStream, const p bool isPfmLittleEndian = scale < 0; scale = abs(scale); - vector channels = makeNChannels(numChannels, size); + result.channels = makeNChannels(numChannels, size); auto numPixels = (size_t)size.x() * size.y(); if (numPixels == 0) { @@ -85,35 +85,14 @@ Task> PfmImageLoader::load(istream& iStream, const p } // Flip image vertically due to PFM format - channels[c].at({x, size.y() - (int)y - 1}) = scale * val; + result.channels[c].at({x, size.y() - (int)y - 1}) = scale * val; } } }, priority); - vector> matches; - for (size_t i = 0; i < channels.size(); ++i) { - size_t matchId; - if (matchesFuzzy(channels[i].name(), channelSelector, &matchId)) { - matches.emplace_back(matchId, i); - } - } - - if (!channelSelector.empty()) { - sort(begin(matches), end(matches)); - } - - for (const auto& match : matches) { - result.channels.emplace_back(move(channels[match.second])); - } - - // PFM can not contain layers, so all channels simply reside - // within a topmost root layer. - result.layers.emplace_back(""); - - // PFM images do not have custom data and display windows. - result.dataWindow = result.displayWindow = result.channels.front().size(); + result.hasPremultipliedAlpha = false; - co_return {result, false}; + co_return result; } TEV_NAMESPACE_END diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index 356a86cb8..8fd5139ba 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -18,7 +18,7 @@ bool StbiImageLoader::canLoadFile(istream&) const { return true; } -Task> StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { ImageData result; static const stbi_io_callbacks callbacks = { @@ -61,7 +61,7 @@ Task> StbiImageLoader::load(istream& iStream, const ScopeGuard dataGuard{[data] { stbi_image_free(data); }}; - auto channels = makeNChannels(numChannels, size); + result.channels = makeNChannels(numChannels, size); int alphaChannelIndex = 3; auto numPixels = (size_t)size.x() * size.y(); @@ -70,7 +70,7 @@ Task> StbiImageLoader::load(istream& iStream, const auto typedData = reinterpret_cast(data); size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { - channels[c].at(i) = typedData[baseIdx + c]; + result.channels[c].at(i) = typedData[baseIdx + c]; } }, priority); } else { @@ -79,38 +79,17 @@ Task> StbiImageLoader::load(istream& iStream, const size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == alphaChannelIndex) { - channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; + result.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; } else { - channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); + result.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); } } }, priority); } - vector> matches; - for (size_t i = 0; i < channels.size(); ++i) { - size_t matchId; - if (matchesFuzzy(channels.at(i).name(), channelSelector, &matchId)) { - matches.emplace_back(matchId, i); - } - } - - if (!channelSelector.empty()) { - sort(begin(matches), end(matches)); - } - - for (const auto& match : matches) { - result.channels.emplace_back(move(channels.at(match.second))); - } - - // STBI can not load layers, so all channels simply reside - // within a topmost root layer. - result.layers.emplace_back(""); - - // STBI-loaded images do not have custom data and display windows. - result.dataWindow = result.displayWindow = result.channels.front().size(); + result.hasPremultipliedAlpha = false; - co_return {result, false}; + co_return result; } TEV_NAMESPACE_END From 87751b75bcd7ce7b2146fe122111e4edc6dadb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 10:41:34 +0200 Subject: [PATCH 50/83] Neatify display and data windows --- include/tev/Box.h | 4 ++ include/tev/Image.h | 4 +- src/Image.cpp | 13 ++++- src/ImageCanvas.cpp | 114 +++++++++++++++++++++++++++++++------------- 4 files changed, 99 insertions(+), 36 deletions(-) diff --git a/include/tev/Box.h b/include/tev/Box.h index 9dfe79913..1ec830d0b 100644 --- a/include/tev/Box.h +++ b/include/tev/Box.h @@ -40,6 +40,10 @@ struct Box { return min == other.min && max == other.max; } + Box inflate(T amount) const { + return {min - Vector{amount}, max + Vector{amount}}; + } + Vector min, max; }; diff --git a/include/tev/Image.h b/include/tev/Image.h index 673fe4461..2425e443a 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -143,8 +143,8 @@ class Image { return mData.displayWindow; } - nanogui::Vector2f centerDisplayOffset() const { - return Box2f{dataWindow()}.middle() - Box2f{displayWindow()}.middle(); + nanogui::Vector2f centerDisplayOffset(const Box2i& displayWindow) const { + return Box2f{dataWindow()}.middle() - Box2f{displayWindow}.middle(); } size_t numPixels() const { diff --git a/src/Image.cpp b/src/Image.cpp index 300908aca..2ebd983b7 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -470,7 +470,15 @@ void Image::updateChannel(const string& channelName, int x, int y, int width, in } string Image::toString() const { - string result = tfm::format("Path: %s\n\nResolution: (%d, %d)\n\nChannels:\n", mName, size().x(), size().y()); + stringstream sstream; + sstream << "Path: " << mName << "\n\n"; + sstream << "Resolution: (" << size().x() << ", " << size().y() << ")\n"; + if (displayWindow() != dataWindow() || displayWindow().min != Vector2i{0}) { + sstream << "Display window: (" << displayWindow().min.x() << ", " << displayWindow().min.y() << ")(" << displayWindow().max.x() << ", " << displayWindow().max.y() << ")\n"; + sstream << "Data window: (" << dataWindow().min.x() << ", " << dataWindow().min.y() << ")(" << dataWindow().max.x() << ", " << dataWindow().max.y() << ")\n"; + } + + sstream << "\nChannels:\n"; auto localLayers = mData.layers; transform(begin(localLayers), end(localLayers), begin(localLayers), [this](string layer) { @@ -484,7 +492,8 @@ string Image::toString() const { return layer + ": " + join(channels, ","); }); - return result + join(localLayers, "\n"); + sstream << join(localLayers, "\n"); + return sstream.str(); } Task> tryLoadImage(int imageId, path path, istream& iStream, string channelSelector) { diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 176a07bb8..c37f045a0 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -183,47 +183,92 @@ void ImageCanvas::drawCoordinateSystem(NVGcontext* ctx) { auto displayWindowToNano = displayWindowToNanogui(mImage.get()); - auto drawWindow = [&](const Box2i& window, const Color& color, const std::string& name) { - Vector2i topLeft = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.min.x(), (float)window.min.y()}}; - Vector2i topRight = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.max.x(), (float)window.min.y()}}; - Vector2i bottomLeft = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.min.x(), (float)window.max.y()}}; - Vector2i bottomRight = m_pos + Vector2i{displayWindowToNano * Vector2f{(float)window.max.x(), (float)window.max.y()}}; - - NVGpaint shadowPaint = nvgLinearGradient(ctx, - m_pos.x(), m_pos.y(), m_pos.x(), m_pos.y()+m_size.y(), - color, color - ); - - NVGcolor regularTextColor = Color(150, 255);// : Color(190, 255); - NVGcolor hightlightedTextColor = Color(190, 255); + enum DrawFlags { + Label = 1, + Region = 2, + }; - float fontSize = 30; + auto drawWindow = [&](Box2f window, Color color, bool top, bool right, const std::string& name, DrawFlags flags) { + float fontSize = 20; float strokeWidth = 3.0f; + Vector2i topLeft = m_pos + Vector2i{displayWindowToNano * Vector2f{window.min.x(), window.min.y()}}; + Vector2i topRight = m_pos + Vector2i{displayWindowToNano * Vector2f{window.max.x(), window.min.y()}}; + Vector2i bottomLeft = m_pos + Vector2i{displayWindowToNano * Vector2f{window.min.x(), window.max.y()}}; + Vector2i bottomRight = m_pos + Vector2i{displayWindowToNano * Vector2f{window.max.x(), window.max.y()}}; + nvgSave(ctx); - nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM); - nvgBeginPath(ctx); - nvgMoveTo(ctx, bottomLeft.x(), bottomLeft.y()); - nvgLineTo(ctx, topLeft.x(), topLeft.y()); - nvgLineTo(ctx, topRight.x(), topRight.y()); - nvgLineTo(ctx, bottomRight.x(), bottomRight.y()); - nvgLineTo(ctx, bottomLeft.x(), bottomLeft.y()); - nvgStrokeWidth(ctx, strokeWidth); - nvgStrokePaint(ctx, shadowPaint); - nvgStroke(ctx); nvgFontFace(ctx, "sans-bold"); nvgFontSize(ctx, fontSize); - nvgFillColor(ctx, color); - // drawTextWithShadow(ctx, topLeft.x(), topLeft.y() - 20, name, 1.0f); - nvgText(ctx, topLeft.x() - strokeWidth/2 - 1.0f, topLeft.y(), name.c_str(), NULL); + nvgTextAlign(ctx, (right ? NVG_ALIGN_RIGHT : NVG_ALIGN_LEFT) | (top ? NVG_ALIGN_BOTTOM : NVG_ALIGN_TOP)); + float textWidth = nvgTextBounds(ctx, 0, 0, name.c_str(), nullptr, nullptr); + float textAlpha = max(min(1.0f, (((topRight.x() - topLeft.x()) / textWidth) - 2.0f)), 0.0f); + float regionAlpha = max(min(1.0f, (((topRight.x() - topLeft.x()) / textWidth) - 1.5f) * 2), 0.0f); + + Color textColor = Color(190, 255); + textColor.a() = textAlpha; + + if (flags & Region) { + color.a() = regionAlpha; + + nvgBeginPath(ctx); + nvgMoveTo(ctx, bottomLeft.x(), bottomLeft.y()); + nvgLineTo(ctx, topLeft.x(), topLeft.y()); + nvgLineTo(ctx, topRight.x(), topRight.y()); + nvgLineTo(ctx, bottomRight.x(), bottomRight.y()); + nvgLineTo(ctx, bottomLeft.x(), bottomLeft.y()); + nvgStrokeWidth(ctx, strokeWidth); + nvgStrokeColor(ctx, color); + nvgStroke(ctx); + } + + if (flags & Label) { + color.a() = textAlpha; + + nvgBeginPath(ctx); + nvgFillColor(ctx, color); + + float cornerRadius = fontSize / 3; + float topLeftCornerRadius = top && right ? cornerRadius : 0; + float topRightCornerRadius = top && !right ? cornerRadius : 0; + float bottomLeftCornerRadius = !top && right ? cornerRadius : 0; + float bottomRightCornerRadius = !top && !right ? cornerRadius : 0; + + nvgRoundedRectVarying( + ctx, right ? (topRight.x() - textWidth - 4*strokeWidth) : topLeft.x() - strokeWidth/2, topLeft.y() - (top ? fontSize : 0), textWidth + 4*strokeWidth, fontSize, + topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius + ); + nvgFill(ctx); + + nvgFillColor(ctx, textColor); + nvgText(ctx, right ? (topRight.x() - 2*strokeWidth) : (topLeft.x() + 2*strokeWidth) - strokeWidth/2, topLeft.y(), name.c_str(), NULL); + } nvgRestore(ctx); + }; + + Color imageColor = Color(0.35f, 0.35f, 0.8f, 1.0f); + Color referenceColor = Color(0.7f, 0.4f, 0.4f, 1.0f); + + auto draw = [&](DrawFlags flags) { + if (mReference) { + if (mReference->dataWindow() != mImage->dataWindow()) { + drawWindow(mReference->dataWindow(), referenceColor, mReference->displayWindow().min.y() > mReference->dataWindow().min.y(), true, "Reference data window", flags); + } + if (mReference->displayWindow() != mImage->displayWindow()) { + drawWindow(mReference->displayWindow(), referenceColor, mReference->displayWindow().min.y() <= mReference->dataWindow().min.y(), true, "Reference display window", flags); + } + } + + drawWindow(mImage->dataWindow(), imageColor, mImage->displayWindow().min.y() > mImage->dataWindow().min.y(), false, "Data window", flags); + drawWindow(mImage->displayWindow(), Color(0.3f, 1.0f), mImage->displayWindow().min.y() <= mImage->dataWindow().min.y(), false, "Display window", flags); }; - drawWindow(mImage->displayWindow(), Color(0.7f, 0.4f, 0.4f, 1.0f), "Display window"); - drawWindow(mImage->dataWindow(), Color(0.35f, 0.35f, 0.8f, 1.0f), "Data window"); + // Draw all labels after the regions to ensure no occlusion + draw(Region); + draw(Label); } void ImageCanvas::drawEdgeShadows(NVGcontext* ctx) { @@ -251,7 +296,8 @@ void ImageCanvas::draw(NVGcontext* ctx) { drawPixelValuesAsText(ctx); // If the coordinate system is in any sort of way non-trivial, draw it! - if (mImage->dataWindow() != mImage->displayWindow() || mImage->displayWindow().min != Vector2i{0}) { + if (mImage->dataWindow() != mImage->displayWindow() || mImage->displayWindow().min != Vector2i{0} || + mReference && (mReference->dataWindow() != mImage->dataWindow() || mReference->displayWindow() != mImage->displayWindow())) { drawCoordinateSystem(ctx); } } @@ -759,13 +805,15 @@ Matrix3f ImageCanvas::transform(const Image* image) { return Matrix3f::scale(Vector2f{1.0f}); } + TEV_ASSERT(mImage, "Coordinates are relative to the currently selected image's display window. So must have an image selected."); + // Center image, scale to pixel space, translate to desired position, // then rescale to the [-1, 1] square for drawing. return Matrix3f::scale(Vector2f{2.0f / m_size.x(), -2.0f / m_size.y()}) * mTransform * Matrix3f::scale(Vector2f{1.0f / mPixelRatio}) * - Matrix3f::translate(image->centerDisplayOffset() + pixelOffset(image->size())) * + Matrix3f::translate(image->centerDisplayOffset(mImage->displayWindow()) + pixelOffset(image->size())) * Matrix3f::scale(Vector2f{image->size()}) * Matrix3f::translate(Vector2f{-0.5f}); } @@ -775,12 +823,14 @@ Matrix3f ImageCanvas::textureToNanogui(const Image* image) { return Matrix3f::scale(Vector2f{1.0f}); } + TEV_ASSERT(mImage, "Coordinates are relative to the currently selected image's display window. So must have an image selected."); + // Move origin to centre of image, scale pixels, apply our transform, move origin back to top-left. return Matrix3f::translate(0.5f * Vector2f{m_size}) * mTransform * Matrix3f::scale(Vector2f{1.0f / mPixelRatio}) * - Matrix3f::translate(-0.5f * Vector2f{image->size()} + image->centerDisplayOffset() + pixelOffset(image->size())); + Matrix3f::translate(-0.5f * Vector2f{image->size()} + image->centerDisplayOffset(mImage->displayWindow()) + pixelOffset(image->size())); } Matrix3f ImageCanvas::displayWindowToNanogui(const Image* image) { From 882c23df81ce9606db5f69d960a4f588eeb5036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 12:40:30 +0200 Subject: [PATCH 51/83] Allow image loaders to return more than a single image Preparation for multi-part EXR images # Conflicts: # src/Image.cpp # src/imageio/ExrImageLoader.cpp --- include/tev/Image.h | 16 ++++--- include/tev/imageio/ClipboardImageLoader.h | 2 +- include/tev/imageio/DdsImageLoader.h | 2 +- include/tev/imageio/EmptyImageLoader.h | 2 +- include/tev/imageio/ExrImageLoader.h | 2 +- include/tev/imageio/ImageLoader.h | 2 +- include/tev/imageio/PfmImageLoader.h | 2 +- include/tev/imageio/StbiImageLoader.h | 2 +- src/Image.cpp | 49 +++++++++++----------- src/ImageViewer.cpp | 23 ++++++---- src/imageio/ClipboardImageLoader.cpp | 15 +++---- src/imageio/DdsImageLoader.cpp | 16 +++---- src/imageio/EmptyImageLoader.cpp | 11 ++--- src/imageio/ExrImageLoader.cpp | 25 +++++------ src/imageio/PfmImageLoader.cpp | 11 ++--- src/imageio/StbiImageLoader.cpp | 15 +++---- src/main.cpp | 7 ++-- 17 files changed, 110 insertions(+), 92 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index 2425e443a..fe916ef13 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -23,6 +23,10 @@ TEV_NAMESPACE_BEGIN class ImageLoader; struct ImageData { + ImageData() = default; + ImageData(const ImageData&) = delete; + ImageData(ImageData&&) = default; + std::vector channels; std::vector layers; nanogui::Matrix4f toRec709 = nanogui::Matrix4f{1.0f}; // Identity by default @@ -100,7 +104,7 @@ struct ImageTexture { class Image { public: - Image(int id, const filesystem::path& path, ImageData&& data, const std::string& channelSelector); + Image(const filesystem::path& path, ImageData&& data, const std::string& channelSelector); virtual ~Image(); const filesystem::path& path() const { @@ -194,15 +198,15 @@ class Image { int mId; }; -Task> tryLoadImage(int imageId, filesystem::path path, std::istream& iStream, std::string channelSelector); -Task> tryLoadImage(filesystem::path path, std::istream& iStream, std::string channelSelector); -Task> tryLoadImage(int imageId, filesystem::path path, std::string channelSelector); -Task> tryLoadImage(filesystem::path path, std::string channelSelector); +Task>> tryLoadImage(int imageId, filesystem::path path, std::istream& iStream, std::string channelSelector); +Task>> tryLoadImage(filesystem::path path, std::istream& iStream, std::string channelSelector); +Task>> tryLoadImage(int imageId, filesystem::path path, std::string channelSelector); +Task>> tryLoadImage(filesystem::path path, std::string channelSelector); struct ImageAddition { int loadId; bool shallSelect; - std::shared_ptr image; + std::vector> images; struct Comparator { bool operator()(const ImageAddition& a, const ImageAddition& b) { diff --git a/include/tev/imageio/ClipboardImageLoader.h b/include/tev/imageio/ClipboardImageLoader.h index 4763d2e1a..e7daef306 100644 --- a/include/tev/imageio/ClipboardImageLoader.h +++ b/include/tev/imageio/ClipboardImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ClipboardImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "clipboard"; diff --git a/include/tev/imageio/DdsImageLoader.h b/include/tev/imageio/DdsImageLoader.h index 5e9e54fb6..ff0f7e504 100644 --- a/include/tev/imageio/DdsImageLoader.h +++ b/include/tev/imageio/DdsImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class DdsImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "DDS"; diff --git a/include/tev/imageio/EmptyImageLoader.h b/include/tev/imageio/EmptyImageLoader.h index 9fec55639..5640df502 100644 --- a/include/tev/imageio/EmptyImageLoader.h +++ b/include/tev/imageio/EmptyImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class EmptyImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "IPC"; diff --git a/include/tev/imageio/ExrImageLoader.h b/include/tev/imageio/ExrImageLoader.h index 898384910..e531d77e0 100644 --- a/include/tev/imageio/ExrImageLoader.h +++ b/include/tev/imageio/ExrImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class ExrImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "OpenEXR"; diff --git a/include/tev/imageio/ImageLoader.h b/include/tev/imageio/ImageLoader.h index b90c086b2..b2dfd3740 100644 --- a/include/tev/imageio/ImageLoader.h +++ b/include/tev/imageio/ImageLoader.h @@ -22,7 +22,7 @@ class ImageLoader { virtual bool canLoadFile(std::istream& iStream) const = 0; // Return loaded image data as well as whether that data has the alpha channel pre-multiplied or not. - virtual Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; + virtual Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const = 0; virtual std::string name() const = 0; diff --git a/include/tev/imageio/PfmImageLoader.h b/include/tev/imageio/PfmImageLoader.h index ca17c7ae4..3f223ed26 100644 --- a/include/tev/imageio/PfmImageLoader.h +++ b/include/tev/imageio/PfmImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class PfmImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "PFM"; diff --git a/include/tev/imageio/StbiImageLoader.h b/include/tev/imageio/StbiImageLoader.h index 215d87917..a8ea61660 100644 --- a/include/tev/imageio/StbiImageLoader.h +++ b/include/tev/imageio/StbiImageLoader.h @@ -13,7 +13,7 @@ TEV_NAMESPACE_BEGIN class StbiImageLoader : public ImageLoader { public: bool canLoadFile(std::istream& iStream) const override; - Task load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; std::string name() const override { return "STBI"; diff --git a/src/Image.cpp b/src/Image.cpp index 2ebd983b7..0a0e6b28c 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -198,8 +198,8 @@ Task ImageData::ensureValid(const string& channelSelector, int taskPriorit atomic Image::sId(0); -Image::Image(int id, const class path& path, ImageData&& data, const string& channelSelector) -: mPath{path}, mChannelSelector{channelSelector}, mData{std::move(data)}, mId{id} { +Image::Image(const class path& path, ImageData&& data, const string& channelSelector) +: mPath{path}, mChannelSelector{channelSelector}, mData{std::move(data)}, mId{Image::drawId()} { mName = channelSelector.empty() ? path.str() : tfm::format("%s:%s", path, channelSelector); for (const auto& l : mData.layers) { @@ -496,7 +496,7 @@ string Image::toString() const { return sstream.str(); } -Task> tryLoadImage(int imageId, path path, istream& iStream, string channelSelector) { +Task>> tryLoadImage(int taskPriority, path path, istream& iStream, string channelSelector) { auto handleException = [&](const exception& e) { if (channelSelector.empty()) { tlog::error() << tfm::format("Could not load '%s'. %s", path, e.what()); @@ -524,23 +524,22 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s if (useLoader) { // Earlier images should be prioritized when loading. - int taskPriority = -imageId; - loadMethod = imageLoader->name(); - auto data = co_await imageLoader->load(iStream, path, channelSelector, taskPriority); - co_await data.ensureValid(channelSelector, taskPriority); - - // Convert chromaticities to sRGB / Rec 709 if they aren't already. - co_await data.convertToRec709(taskPriority); + auto imageData = co_await imageLoader->load(iStream, path, channelSelector, taskPriority); - auto image = make_shared(imageId, path, std::move(data), channelSelector); + vector> images; + for (auto& i : imageData) { + co_await i.ensureValid(channelSelector, taskPriority); + co_await i.convertToRec709(taskPriority); + images.emplace_back(make_shared(path, std::move(i), channelSelector)); + } auto end = chrono::system_clock::now(); chrono::duration elapsedSeconds = end - start; - tlog::success() << tfm::format("Loaded '%s' via %s after %.3f seconds.", image->name(), loadMethod, elapsedSeconds.count()); + tlog::success() << tfm::format("Loaded '%s' via %s after %.3f seconds.", path, loadMethod, elapsedSeconds.count()); - co_return image; + co_return images; } } @@ -555,14 +554,14 @@ Task> tryLoadImage(int imageId, path path, istream& iStream, s handleException(e); } - co_return nullptr; + co_return {}; } -Task> tryLoadImage(path path, istream& iStream, string channelSelector) { - co_return co_await tryLoadImage(Image::drawId(), path, iStream, channelSelector); +Task>> tryLoadImage(path path, istream& iStream, string channelSelector) { + co_return co_await tryLoadImage(-Image::drawId(), path, iStream, channelSelector); } -Task> tryLoadImage(int imageId, path path, string channelSelector) { +Task>> tryLoadImage(int taskPriority, path path, string channelSelector) { try { path = path.make_absolute(); } catch (const runtime_error&) { @@ -571,24 +570,24 @@ Task> tryLoadImage(int imageId, path path, string channelSelec } ifstream fileStream{nativeString(path), ios_base::binary}; - co_return co_await tryLoadImage(imageId, path, fileStream, channelSelector); + co_return co_await tryLoadImage(taskPriority, path, fileStream, channelSelector); } -Task> tryLoadImage(path path, string channelSelector) { +Task>> tryLoadImage(path path, string channelSelector) { co_return co_await tryLoadImage(Image::drawId(), path, channelSelector); } void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { - int imageId = Image::drawId(); int loadId = mUnsortedLoadCounter++; + invokeTaskDetached([loadId, path, channelSelector, shallSelect, this]() -> Task { + int taskPriority = -Image::drawId(); - invokeTaskDetached([imageId, loadId, path, channelSelector, shallSelect, this]() -> Task { - co_await gThreadPool->enqueueCoroutine(-imageId); - auto image = co_await tryLoadImage(imageId, path, channelSelector); + co_await gThreadPool->enqueueCoroutine(taskPriority); + auto images = co_await tryLoadImage(taskPriority, path, channelSelector); { std::lock_guard lock{mPendingLoadedImagesMutex}; - mPendingLoadedImages.push({ loadId, shallSelect, image }); + mPendingLoadedImages.push({ loadId, shallSelect, images }); } if (publishSortedLoads()) { @@ -604,7 +603,7 @@ bool BackgroundImagesLoader::publishSortedLoads() { ++mLoadCounter; // null image pointers indicate failed loads. These shouldn't be pushed. - if (mPendingLoadedImages.top().image) { + if (!mPendingLoadedImages.top().images.empty()) { mLoadedImages.push(std::move(mPendingLoadedImages.top())); } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index a01d376bb..9f50733bc 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -758,11 +758,13 @@ bool ImageViewer::keyboard_event(int key, int scancode, int action, int modifier << string(clipImage.data(), clipImage.spec().bytes_per_row * clipImage.spec().height) ; - auto image = tryLoadImage(tfm::format("clipboard (%d)", ++mClipboardIndex), imageStream, "").get(); - if (image) { - addImage(image, true); - } else { + auto images = tryLoadImage(tfm::format("clipboard (%d)", ++mClipboardIndex), imageStream, "").get(); + if (images.empty()) { tlog::error() << "Failed to load image from clipboard data."; + } else { + for (auto& image : images) { + addImage(image, true); + } } } } @@ -885,7 +887,9 @@ void ImageViewer::draw_contents() { bool newFocus = false; while (auto addition = mImagesLoader->tryPop()) { newFocus |= addition->shallSelect; - addImage(addition->image, addition->shallSelect); + for (auto& image : addition->images) { + addImage(image, addition->shallSelect); + } } if (newFocus) { @@ -1135,10 +1139,13 @@ void ImageViewer::reloadImage(shared_ptr image, bool shallSelect) { int referenceId = imageId(mCurrentReference); - auto newImage = tryLoadImage(image->path(), image->channelSelector()).get(); - if (newImage) { + auto newImages = tryLoadImage(image->path(), image->channelSelector()).get(); + if (!newImages.empty()) { removeImage(image); - insertImage(newImage, id, shallSelect); + insertImage(newImages.front(), id, shallSelect); + if (newImages.size() > 1) { + tlog::warning() << "Ambiguous image reload."; + } } if (referenceId != -1) { diff --git a/src/imageio/ClipboardImageLoader.cpp b/src/imageio/ClipboardImageLoader.cpp index c159c4aff..1a5188cb3 100644 --- a/src/imageio/ClipboardImageLoader.cpp +++ b/src/imageio/ClipboardImageLoader.cpp @@ -23,8 +23,9 @@ bool ClipboardImageLoader::canLoadFile(istream& iStream) const { return result; } -Task ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { - ImageData result; +Task> ClipboardImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { + vector result(1); + ImageData& resultData = result.front(); char magic[4]; clip::image_spec spec; @@ -58,7 +59,7 @@ Task ClipboardImageLoader::load(istream& iStream, const path&, const auto numBytes = (size_t)numBytesPerRow * size.y(); int alphaChannelIndex = 3; - result.channels = makeNChannels(numChannels, size); + resultData.channels = makeNChannels(numChannels, size); vector data(numBytes); iStream.read(reinterpret_cast(data.data()), numBytes); @@ -89,17 +90,17 @@ Task ClipboardImageLoader::load(istream& iStream, const path&, const for (int c = numChannels-1; c >= 0; --c) { unsigned char val = data[baseIdx + shifts[c]]; if (c == alphaChannelIndex) { - result.channels[c].at({x, y}) = val / 255.0f; + resultData.channels[c].at({x, y}) = val / 255.0f; } else { - float alpha = premultipliedAlpha ? result.channels[alphaChannelIndex].at({x, y}) : 1.0f; + float alpha = premultipliedAlpha ? resultData.channels[alphaChannelIndex].at({x, y}) : 1.0f; float alphaFactor = alpha == 0 ? 0 : (1.0f / alpha); - result.channels[c].at({x, y}) = toLinear(val / 255.0f * alphaFactor); + resultData.channels[c].at({x, y}) = toLinear(val / 255.0f * alphaFactor); } } } }, priority); - result.hasPremultipliedAlpha = false; + resultData.hasPremultipliedAlpha = false; co_return result; } diff --git a/src/imageio/DdsImageLoader.cpp b/src/imageio/DdsImageLoader.cpp index d79c305b5..edd41921d 100644 --- a/src/imageio/DdsImageLoader.cpp +++ b/src/imageio/DdsImageLoader.cpp @@ -153,14 +153,16 @@ static int getDxgiChannelCount(DXGI_FORMAT fmt) { } } -Task DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { +Task> DdsImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { // COM must be initialized on the thread executing load(). if (CoInitializeEx(nullptr, COINIT_MULTITHREADED) != S_OK) { throw invalid_argument{"Failed to initialize COM."}; } ScopeGuard comScopeGuard{ []() { CoUninitialize(); } }; - ImageData result; + vector result(1); + ImageData& resultData = result.front(); + iStream.seekg(0, iStream.end); size_t dataSize = iStream.tellg(); iStream.seekg(0, iStream.beg); @@ -208,7 +210,7 @@ Task DdsImageLoader::load(istream& iStream, const path&, const string std::swap(scratchImage, convertedImage); } - result.channels = makeNChannels(numChannels, { (int)metadata.width, (int)metadata.height }); + resultData.channels = makeNChannels(numChannels, { (int)metadata.width, (int)metadata.height }); auto numPixels = (size_t)metadata.width * metadata.height; if (numPixels == 0) { @@ -224,7 +226,7 @@ Task DdsImageLoader::load(istream& iStream, const path&, const string co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { - result.channels[c].at(i) = typedData[baseIdx + c]; + resultData.channels[c].at(i) = typedData[baseIdx + c]; } }, priority); } else { @@ -237,15 +239,15 @@ Task DdsImageLoader::load(istream& iStream, const path&, const string size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == 3) { - result.channels[c].at(i) = typedData[baseIdx + c]; + resultData.channels[c].at(i) = typedData[baseIdx + c]; } else { - result.channels[c].at(i) = toLinear(typedData[baseIdx + c]); + resultData.channels[c].at(i) = toLinear(typedData[baseIdx + c]); } } }, priority); } - result.hasPremultipliedAlpha = scratchImage.GetMetadata().IsPMAlpha(); + resultData.hasPremultipliedAlpha = scratchImage.GetMetadata().IsPMAlpha(); co_return result; } diff --git a/src/imageio/EmptyImageLoader.cpp b/src/imageio/EmptyImageLoader.cpp index 432351a17..9bc1a81ed 100644 --- a/src/imageio/EmptyImageLoader.cpp +++ b/src/imageio/EmptyImageLoader.cpp @@ -22,8 +22,9 @@ bool EmptyImageLoader::canLoadFile(istream& iStream) const { return result; } -Task EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { - ImageData result; +Task> EmptyImageLoader::load(istream& iStream, const path&, const string&, int priority) const { + vector result(1); + ImageData& data = result.front(); string magic; Vector2i size; @@ -51,11 +52,11 @@ Task EmptyImageLoader::load(istream& iStream, const path&, const stri string channelName = channelNameData.data(); - result.channels.emplace_back(Channel{channelName, size}); - result.channels.back().setZero(); + data.channels.emplace_back(Channel{channelName, size}); + data.channels.back().setZero(); } - result.hasPremultipliedAlpha = true; + data.hasPremultipliedAlpha = true; co_return result; } diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 63cbb3e32..dd67b6f76 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -155,8 +155,9 @@ class RawChannel { vector mData; }; -Task ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { - ImageData result; +Task> ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { + vector result(1); + ImageData& data = result.front(); StdIStream stdIStream{iStream, path.str().c_str()}; Imf::MultiPartInputFile multiPartFile{stdIStream}; @@ -193,20 +194,20 @@ Task ExrImageLoader::load(istream& iStream, const path& path, const s // EXR's display- and data windows have inclusive upper ends while tev's upper ends are exclusive. // This allows easy conversion from window to size. Hence the +1. - result.dataWindow = {{dataWindow.min.x, dataWindow.min.y }, {dataWindow.max.x+1, dataWindow.max.y+1 }}; - result.displayWindow = {{displayWindow.min.x, displayWindow.min.y}, {displayWindow.max.x+1, displayWindow.max.y+1}}; + data.dataWindow = {{dataWindow.min.x, dataWindow.min.y }, {dataWindow.max.x+1, dataWindow.max.y+1 }}; + data.displayWindow = {{displayWindow.min.x, displayWindow.min.y}, {displayWindow.max.x+1, displayWindow.max.y+1}}; - if (!result.dataWindow.isValid()) { + if (!data.dataWindow.isValid()) { throw invalid_argument{tfm::format( "EXR image has invalid data window: [%d,%d] - [%d,%d]", - result.dataWindow.min.x(), result.dataWindow.min.y(), result.dataWindow.max.x(), result.dataWindow.max.y() + data.dataWindow.min.x(), data.dataWindow.min.y(), data.dataWindow.max.x(), data.dataWindow.max.y() )}; } - if (!result.displayWindow.isValid()) { + if (!data.displayWindow.isValid()) { throw invalid_argument{tfm::format( "EXR image has invalid display window: [%d,%d] - [%d,%d]", - result.displayWindow.min.x(), result.displayWindow.min.y(), result.displayWindow.max.x(), result.displayWindow.max.y() + data.displayWindow.min.x(), data.displayWindow.min.y(), data.displayWindow.max.x(), data.displayWindow.max.y() )}; } @@ -252,12 +253,12 @@ Task ExrImageLoader::load(istream& iStream, const path& path, const s file.readPixels(dataWindow.min.y, dataWindow.max.y); for (const auto& rawChannel : *rawChannels) { - result.channels.emplace_back(Channel{rawChannel.name(), size}); + data.channels.emplace_back(Channel{rawChannel.name(), size}); } vector> tasks; for (size_t i = 0; i < rawChannels->size(); ++i) { - tasks.emplace_back(rawChannels->at(i).copyTo(result.channels[i], priority)); + tasks.emplace_back(rawChannels->at(i).copyTo(data.channels[i], priority)); } for (auto& task : tasks) { @@ -284,12 +285,12 @@ Task ExrImageLoader::load(istream& iStream, const path& path, const s Imath::M44f M = Imf::RGBtoXYZ(chroma, 1) * Imf::XYZtoRGB(rec709, 1); for (int m = 0; m < 4; ++m) { for (int n = 0; n < 4; ++n) { - result.toRec709.m[m][n] = M.x[m][n]; + data.toRec709.m[m][n] = M.x[m][n]; } } } - result.hasPremultipliedAlpha = true; + data.hasPremultipliedAlpha = true; co_return result; } diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index 7e9698080..3b211d1a9 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -21,8 +21,9 @@ bool PfmImageLoader::canLoadFile(istream& iStream) const { return result; } -Task PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { - ImageData result; +Task> PfmImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { + vector result(1); + ImageData& resultData = result.front(); string magic; Vector2i size; @@ -48,7 +49,7 @@ Task PfmImageLoader::load(istream& iStream, const path&, const string bool isPfmLittleEndian = scale < 0; scale = abs(scale); - result.channels = makeNChannels(numChannels, size); + resultData.channels = makeNChannels(numChannels, size); auto numPixels = (size_t)size.x() * size.y(); if (numPixels == 0) { @@ -85,12 +86,12 @@ Task PfmImageLoader::load(istream& iStream, const path&, const string } // Flip image vertically due to PFM format - result.channels[c].at({x, size.y() - (int)y - 1}) = scale * val; + resultData.channels[c].at({x, size.y() - (int)y - 1}) = scale * val; } } }, priority); - result.hasPremultipliedAlpha = false; + resultData.hasPremultipliedAlpha = false; co_return result; } diff --git a/src/imageio/StbiImageLoader.cpp b/src/imageio/StbiImageLoader.cpp index 8fd5139ba..cca60f789 100644 --- a/src/imageio/StbiImageLoader.cpp +++ b/src/imageio/StbiImageLoader.cpp @@ -18,8 +18,9 @@ bool StbiImageLoader::canLoadFile(istream&) const { return true; } -Task StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { - ImageData result; +Task> StbiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { + vector result(1); + ImageData& resultData = result.front(); static const stbi_io_callbacks callbacks = { // Read @@ -61,7 +62,7 @@ Task StbiImageLoader::load(istream& iStream, const path&, const strin ScopeGuard dataGuard{[data] { stbi_image_free(data); }}; - result.channels = makeNChannels(numChannels, size); + resultData.channels = makeNChannels(numChannels, size); int alphaChannelIndex = 3; auto numPixels = (size_t)size.x() * size.y(); @@ -70,7 +71,7 @@ Task StbiImageLoader::load(istream& iStream, const path&, const strin auto typedData = reinterpret_cast(data); size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { - result.channels[c].at(i) = typedData[baseIdx + c]; + resultData.channels[c].at(i) = typedData[baseIdx + c]; } }, priority); } else { @@ -79,15 +80,15 @@ Task StbiImageLoader::load(istream& iStream, const path&, const strin size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { if (c == alphaChannelIndex) { - result.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; + resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; } else { - result.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); + resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); } } }, priority); } - result.hasPremultipliedAlpha = false; + resultData.hasPremultipliedAlpha = false; co_return result; } diff --git a/src/main.cpp b/src/main.cpp index 899784aae..94cc94147 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -114,9 +114,10 @@ void handleIpcPacket(const IpcPacket& packet, const std::shared_ptraddImage(image, info.grabFocus); + auto images = tryLoadImage(imageString, imageStream, "").get(); + if (!images.empty()) { + sImageViewer->addImage(images.front(), info.grabFocus); + TEV_ASSERT(images.size() == 1, "IPC CreateImage should never create more than 1 image at once."); } }); From 49cbc7df40353d91de65983c15f51ab50e57119a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 13:35:43 +0200 Subject: [PATCH 52/83] When loading a multi-part EXR file, load all parts with matching selector as separate images # Conflicts: # src/Image.cpp # src/imageio/ExrImageLoader.cpp --- include/tev/Image.h | 2 + src/Image.cpp | 17 ++- src/imageio/ExrImageLoader.cpp | 228 ++++++++++++++++++--------------- 3 files changed, 145 insertions(+), 102 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index fe916ef13..e4fbd68d5 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -35,6 +35,8 @@ struct ImageData { Box2i dataWindow; Box2i displayWindow; + std::string partName; + nanogui::Vector2i size() const { return dataWindow.size(); } diff --git a/src/Image.cpp b/src/Image.cpp index 0a0e6b28c..78531bece 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -71,7 +71,7 @@ Task ImageData::convertToRec709(int priority) { TEV_ASSERT(r && g && b, "RGB triplet of channels must exist."); tasks.emplace_back( - gThreadPool->parallelForAsync(0, r->count(), [r, g, b, this](size_t i) { + gThreadPool->parallelForAsync(0, r->numPixels(), [r, g, b, this](size_t i) { auto rgb = toRec709 * Vector3f{r->at(i), g->at(i), b->at(i)}; r->at(i) = rgb.x(); g->at(i) = rgb.y(); @@ -531,7 +531,20 @@ Task>> tryLoadImage(int taskPriority, path path, istrea for (auto& i : imageData) { co_await i.ensureValid(channelSelector, taskPriority); co_await i.convertToRec709(taskPriority); - images.emplace_back(make_shared(path, std::move(i), channelSelector)); + + // If multiple image "parts" were loaded and they have names, + // ensure that these names are present in the channel selector. + string localChannelSelector = channelSelector; + if (!i.partName.empty()) { + auto selectorParts = split(channelSelector, ","); + if (channelSelector.empty()) { + localChannelSelector = i.partName; + } else if (find(begin(selectorParts), end(selectorParts), i.partName) == end(selectorParts)) { + localChannelSelector = join(vector{i.partName, channelSelector}, ","); + } + } + + images.emplace_back(make_shared(path, std::move(i), localChannelSelector)); } auto end = chrono::system_clock::now(); diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index dd67b6f76..683570c44 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -91,17 +91,16 @@ bool ExrImageLoader::canLoadFile(istream& iStream) const { // Helper class for dealing with the raw channels loaded from an exr file. class RawChannel { public: - RawChannel(string name, Imf::Channel imfChannel) - : mName(name), mImfChannel(imfChannel) { - } + RawChannel(size_t partId, string name, string imfName, Imf::Channel imfChannel, const Vector2i& size) + : mPartId{partId}, mName{name}, mImfName{imfName}, mImfChannel{imfChannel}, mSize{size} {} - void resize(size_t size) { - mData.resize(size * bytesPerPixel()); + void resize() { + mData.resize((size_t)mSize.x() * mSize.y() * bytesPerPixel()); } void registerWith(Imf::FrameBuffer& frameBuffer, const Imath::Box2i& dw) { int width = dw.max.x - dw.min.x + 1; - frameBuffer.insert(mName.c_str(), Imf::Slice( + frameBuffer.insert(mImfName.c_str(), Imf::Slice( mImfChannel.type, mData.data() - (dw.min.x + dw.min.y * width) * bytesPerPixel(), bytesPerPixel(), bytesPerPixel() * (width/mImfChannel.xSampling), @@ -135,10 +134,18 @@ class RawChannel { } } + size_t partId() const { + return mPartId; + } + const string& name() const { return mName; } + const Vector2i& size() const { + return mSize; + } + private: int bytesPerPixel() const { switch (mImfChannel.type) { @@ -150,14 +157,16 @@ class RawChannel { } } + size_t mPartId; string mName; + string mImfName; Imf::Channel mImfChannel; + Vector2i mSize; vector mData; }; Task> ExrImageLoader::load(istream& iStream, const path& path, const string& channelSelector, int priority) const { - vector result(1); - ImageData& data = result.front(); + vector result; StdIStream stdIStream{iStream, path.str().c_str()}; Imf::MultiPartInputFile multiPartFile{stdIStream}; @@ -167,130 +176,149 @@ Task> ExrImageLoader::load(istream& iStream, const path& path, throw invalid_argument{"EXR image does not contain any parts."}; } - // Find the first part containing a channel that matches the given channelSubstr. - int partIdx = 0; - for (int i = 0; i < numParts; ++i) { - Imf::InputPart part{multiPartFile, i}; + vector parts; + vector frameBuffers; + + vector rawChannels; + + // Load all parts that match the channel selector + for (int partIdx = 0; partIdx < numParts; ++partIdx) { + Imf::InputPart part{multiPartFile, partIdx}; const Imf::ChannelList& imfChannels = part.header().channels(); + auto channelName = [&](Imf::ChannelList::ConstIterator c) { + string name = c.name(); + if (part.header().hasName()) { + name = part.header().name() + "."s + name; + } + return name; + }; + + bool matched = false; for (Imf::ChannelList::ConstIterator c = imfChannels.begin(); c != imfChannels.end(); ++c) { - if (matchesFuzzy(c.name(), channelSelector)) { - partIdx = i; - goto l_foundPart; + matched |= matchesFuzzy(channelName(c), channelSelector); + if (matched) { + break; } } - } -l_foundPart: - - Imf::InputPart file{multiPartFile, partIdx}; - Imath::Box2i dataWindow = file.header().dataWindow(); - Imath::Box2i displayWindow = file.header().displayWindow(); - Vector2i size = {dataWindow.max.x - dataWindow.min.x + 1 , dataWindow.max.y - dataWindow.min.y + 1}; - - if (size.x() == 0 || size.y() == 0) { - throw invalid_argument{"EXR image has zero pixels."}; - } - - // EXR's display- and data windows have inclusive upper ends while tev's upper ends are exclusive. - // This allows easy conversion from window to size. Hence the +1. - data.dataWindow = {{dataWindow.min.x, dataWindow.min.y }, {dataWindow.max.x+1, dataWindow.max.y+1 }}; - data.displayWindow = {{displayWindow.min.x, displayWindow.min.y}, {displayWindow.max.x+1, displayWindow.max.y+1}}; - - if (!data.dataWindow.isValid()) { - throw invalid_argument{tfm::format( - "EXR image has invalid data window: [%d,%d] - [%d,%d]", - data.dataWindow.min.x(), data.dataWindow.min.y(), data.dataWindow.max.x(), data.dataWindow.max.y() - )}; - } - - if (!data.displayWindow.isValid()) { - throw invalid_argument{tfm::format( - "EXR image has invalid display window: [%d,%d] - [%d,%d]", - data.displayWindow.min.x(), data.displayWindow.min.y(), data.displayWindow.max.x(), data.displayWindow.max.y() - )}; - } - // Allocate raw channels on the heap, because it'll be references - // by nested parallel for coroutine. - auto rawChannels = std::make_unique>(); - Imf::FrameBuffer frameBuffer; + if (!matched) { + continue; + } - const Imf::ChannelList& imfChannels = file.header().channels(); + Imath::Box2i dataWindow = part.header().dataWindow(); + Vector2i size = {dataWindow.max.x - dataWindow.min.x + 1 , dataWindow.max.y - dataWindow.min.y + 1}; - using match_t = pair; - vector matches; - for (Imf::ChannelList::ConstIterator c = imfChannels.begin(); c != imfChannels.end(); ++c) { - size_t matchId; - if (matchesFuzzy(c.name(), channelSelector, &matchId)) { - matches.emplace_back(matchId, c); + if (size.x() == 0 || size.y() == 0) { + tlog::warning() << "EXR part '" << part.header().name() << "' has zero pixels."; + continue; } - } - // Sort matched channels by matched component of the selector, if one exists. - if (!channelSelector.empty()) { - sort(begin(matches), end(matches), [](const match_t& m1, const match_t& m2) { return m1.first < m2.first; }); - } + for (Imf::ChannelList::ConstIterator c = imfChannels.begin(); c != imfChannels.end(); ++c) { + string name = channelName(c); + if (matchesFuzzy(name, channelSelector)) { + rawChannels.emplace_back(parts.size(), name, c.name(), c.channel(), size); + } + } - for (const auto& match : matches) { - const auto& c = match.second; - rawChannels->emplace_back(c.name(), c.channel()); + parts.emplace_back(part); + frameBuffers.emplace_back(); } - if (rawChannels->empty()) { + if (rawChannels.empty()) { throw invalid_argument{tfm::format("No channels match '%s'.", channelSelector)}; } - co_await gThreadPool->parallelForAsync(0, (int)rawChannels->size(), [c = rawChannels.get(), size](int i) { - c->at(i).resize((size_t)size.x() * size.y()); + co_await gThreadPool->parallelForAsync(0, (int)rawChannels.size(), [&](int i) { + rawChannels.at(i).resize(); }, priority); - for (size_t i = 0; i < rawChannels->size(); ++i) { - rawChannels->at(i).registerWith(frameBuffer, dataWindow); + for (auto& rawChannel : rawChannels) { + size_t partId = rawChannel.partId(); + rawChannel.registerWith(frameBuffers.at(partId), parts.at(partId).header().dataWindow()); } - file.setFrameBuffer(frameBuffer); - file.readPixels(dataWindow.min.y, dataWindow.max.y); + // No need for a parallel for loop, because OpenEXR parallelizes internally + for (size_t partIdx = 0; partIdx < parts.size(); ++partIdx) { + auto& part = parts.at(partIdx); - for (const auto& rawChannel : *rawChannels) { - data.channels.emplace_back(Channel{rawChannel.name(), size}); - } + result.emplace_back(); + ImageData& data = result.back(); - vector> tasks; - for (size_t i = 0; i < rawChannels->size(); ++i) { - tasks.emplace_back(rawChannels->at(i).copyTo(data.channels[i], priority)); - } + Imath::Box2i dataWindow = part.header().dataWindow(); + Imath::Box2i displayWindow = part.header().displayWindow(); - for (auto& task : tasks) { - co_await task; - } + // EXR's display- and data windows have inclusive upper ends while tev's upper ends are exclusive. + // This allows easy conversion from window to size. Hence the +1. + data.dataWindow = {{dataWindow.min.x, dataWindow.min.y }, {dataWindow.max.x+1, dataWindow.max.y+1 }}; + data.displayWindow = {{displayWindow.min.x, displayWindow.min.y}, {displayWindow.max.x+1, displayWindow.max.y+1}}; - // equality comparison for Imf::Chromaticities instances - auto chromaEq = [](const Imf::Chromaticities& a, const Imf::Chromaticities& b) { - return - (a.red - b.red).length2() + (a.green - b.green).length2() + - (a.blue - b.blue).length2() + (a.white - b.white).length2() < 1e-6f; - }; + if (!data.dataWindow.isValid()) { + throw invalid_argument{tfm::format( + "EXR image has invalid data window: [%d,%d] - [%d,%d]", + data.dataWindow.min.x(), data.dataWindow.min.y(), data.dataWindow.max.x(), data.dataWindow.max.y() + )}; + } - Imf::Chromaticities rec709; // default rec709 (sRGB) primaries + if (!data.displayWindow.isValid()) { + throw invalid_argument{tfm::format( + "EXR image has invalid display window: [%d,%d] - [%d,%d]", + data.displayWindow.min.x(), data.displayWindow.min.y(), data.displayWindow.max.x(), data.displayWindow.max.y() + )}; + } - // Check if there is a chromaticity header entry and if so, - // expose it to the image data for later conversion to sRGB/Rec709. - Imf::Chromaticities chroma; - if (Imf::hasChromaticities(file.header())) { - chroma = Imf::chromaticities(file.header()); - } + part.setFrameBuffer(frameBuffers.at(partIdx)); + part.readPixels(dataWindow.min.y, dataWindow.max.y); + + data.hasPremultipliedAlpha = true; + if (part.header().hasName()) { + data.partName = part.header().name(); + } + + // equality comparison for Imf::Chromaticities instances + auto chromaEq = [](const Imf::Chromaticities& a, const Imf::Chromaticities& b) { + return + (a.red - b.red).length2() + (a.green - b.green).length2() + + (a.blue - b.blue).length2() + (a.white - b.white).length2() < 1e-6f; + }; - if (!chromaEq(chroma, rec709)) { - Imath::M44f M = Imf::RGBtoXYZ(chroma, 1) * Imf::XYZtoRGB(rec709, 1); - for (int m = 0; m < 4; ++m) { - for (int n = 0; n < 4; ++n) { - data.toRec709.m[m][n] = M.x[m][n]; + Imf::Chromaticities rec709; // default rec709 (sRGB) primaries + + // Check if there is a chromaticity header entry and if so, + // expose it to the image data for later conversion to sRGB/Rec709. + Imf::Chromaticities chroma; + if (Imf::hasChromaticities(part.header())) { + chroma = Imf::chromaticities(part.header()); + } + + if (!chromaEq(chroma, rec709)) { + Imath::M44f M = Imf::RGBtoXYZ(chroma, 1) * Imf::XYZtoRGB(rec709, 1); + for (int m = 0; m < 4; ++m) { + for (int n = 0; n < 4; ++n) { + data.toRec709.m[m][n] = M.x[m][n]; + } } } } - data.hasPremultipliedAlpha = true; + vector channelMapping; + for (size_t i = 0; i < rawChannels.size(); ++i) { + auto& rawChannel = rawChannels.at(i); + auto& data = result.at(rawChannel.partId()); + channelMapping.emplace_back(data.channels.size()); + data.channels.emplace_back(Channel{rawChannel.name(), rawChannel.size()}); + } + + vector> tasks; + for (size_t i = 0; i < rawChannels.size(); ++i) { + auto& rawChannel = rawChannels.at(i); + tasks.emplace_back(rawChannel.copyTo(result.at(rawChannel.partId()).channels.at(channelMapping.at(i)), priority)); + } + + for (auto& task : tasks) { + co_await task; + } co_return result; } From 06eb53d4969b9b677efef78f1c7235e14b6fc7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 16 Aug 2021 16:09:40 +0200 Subject: [PATCH 53/83] C++17/20 algorithms simplification --- include/tev/Common.h | 6 ------ include/tev/Image.h | 14 ++------------ include/tev/SharedQueue.h | 10 +++++----- src/Common.cpp | 4 ++-- src/Image.cpp | 14 +++++++------- src/ImageViewer.cpp | 6 +++--- src/ThreadPool.cpp | 12 ++++++------ src/imageio/ExrImageLoader.cpp | 18 ++++++------------ src/imageio/ImageLoader.cpp | 2 +- src/imageio/ImageSaver.cpp | 4 ++-- 10 files changed, 34 insertions(+), 56 deletions(-) diff --git a/include/tev/Common.h b/include/tev/Common.h index f4682db10..b79743cf3 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -223,12 +223,6 @@ class SharedScopeGuard { std::shared_ptr> mSharedPtr; }; -template -T clamp(T value, T min, T max) { - TEV_ASSERT(max >= min, "Minimum (%f) may not be larger than maximum (%f).", min, max); - return std::max(std::min(value, max), min); -} - template T round(T value, T decimals) { auto precision = std::pow(static_cast(10), decimals); diff --git a/include/tev/Image.h b/include/tev/Image.h index e4fbd68d5..829b3b91e 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -65,12 +65,7 @@ struct ImageData { } const Channel* channel(const std::string& channelName) const { - auto it = std::find_if( - std::begin(channels), - std::end(channels), - [&channelName](const Channel& c) { return c.name() == channelName; } - ); - + auto it = std::ranges::find_if(channels, [&channelName](const Channel& c) { return c.name() == channelName; }); if (it != std::end(channels)) { return &(*it); } else { @@ -79,12 +74,7 @@ struct ImageData { } Channel* mutableChannel(const std::string& channelName) { - auto it = std::find_if( - std::begin(channels), - std::end(channels), - [&channelName](const Channel& c) { return c.name() == channelName; } - ); - + auto it = std::ranges::find_if(channels, [&channelName](const Channel& c) { return c.name() == channelName; }); if (it != std::end(channels)) { return &(*it); } else { diff --git a/include/tev/SharedQueue.h b/include/tev/SharedQueue.h index adbb947ba..a9dc9339f 100644 --- a/include/tev/SharedQueue.h +++ b/include/tev/SharedQueue.h @@ -15,23 +15,23 @@ template class SharedQueue { public: bool empty() const { - std::lock_guard lock{mMutex}; + std::lock_guard lock{mMutex}; return mRawQueue.empty(); } size_t size() const { - std::lock_guard lock{mMutex}; + std::lock_guard lock{mMutex}; return mRawQueue.size(); } void push(T newElem) { - std::lock_guard lock{mMutex}; + std::lock_guard lock{mMutex}; mRawQueue.push_back(newElem); mDataCondition.notify_one(); } T waitAndPop() { - std::unique_lock lock{mMutex}; + std::unique_lock lock{mMutex}; while (mRawQueue.empty()) { mDataCondition.wait(lock); @@ -44,7 +44,7 @@ class SharedQueue { } std::optional tryPop() { - std::unique_lock lock{mMutex}; + std::unique_lock lock{mMutex}; if (mRawQueue.empty()) { return {}; diff --git a/src/Common.cpp b/src/Common.cpp index 2b65f5d24..2534088d6 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -62,12 +62,12 @@ vector split(string text, const string& delim) { } string toLower(string str) { - transform(begin(str), end(str), begin(str), [](unsigned char c) { return (char)tolower(c); }); + ranges::transform(str, begin(str), [](unsigned char c) { return (char)tolower(c); }); return str; } string toUpper(string str) { - transform(begin(str), end(str), begin(str), [](unsigned char c) { return (char)toupper(c); }); + ranges::transform(str, begin(str), [](unsigned char c) { return (char)toupper(c); }); return str; } diff --git a/src/Image.cpp b/src/Image.cpp index 78531bece..476abc663 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -325,7 +325,7 @@ vector Image::getGroupedChannels(const string& layerName) const { auto channelTails = channels; // Remove duplicates channelTails.erase(unique(begin(channelTails), end(channelTails)), end(channelTails)); - transform(begin(channelTails), end(channelTails), begin(channelTails), Channel::tail); + ranges::transform(channelTails, begin(channelTails), Channel::tail); string channelsString = join(channelTails, ","); string name; @@ -345,7 +345,7 @@ vector Image::getGroupedChannels(const string& layerName) const { vector allChannels = mData.channelsInLayer(layerName); - auto alphaIt = find(begin(allChannels), end(allChannels), alphaChannelName); + auto alphaIt = ranges::find(allChannels, alphaChannelName); bool hasAlpha = alphaIt != end(allChannels); if (hasAlpha) { allChannels.erase(alphaIt); @@ -357,7 +357,7 @@ vector Image::getGroupedChannels(const string& layerName) const { vector groupChannels; for (const string& channel : group) { string name = layerPrefix + channel; - auto it = find(begin(allChannels), end(allChannels), name); + auto it = ranges::find(allChannels, name); if (it != end(allChannels)) { groupChannels.emplace_back(name); allChannels.erase(it); @@ -436,7 +436,7 @@ void Image::updateChannel(const string& channelName, int x, int y, int width, in // Update textures that are cached for this channel for (auto& kv : mTextures) { auto& imageTexture = kv.second; - if (find(begin(imageTexture.channels), end(imageTexture.channels), channelName) == end(imageTexture.channels)) { + if (ranges::find(imageTexture.channels, channelName) == end(imageTexture.channels)) { continue; } @@ -481,9 +481,9 @@ string Image::toString() const { sstream << "\nChannels:\n"; auto localLayers = mData.layers; - transform(begin(localLayers), end(localLayers), begin(localLayers), [this](string layer) { + ranges::transform(localLayers, begin(localLayers), [this](string layer) { auto channels = mData.channelsInLayer(layer); - transform(begin(channels), end(channels), begin(channels), [](string channel) { + ranges::transform(channels, begin(channels), [](string channel) { return Channel::tail(channel); }); if (layer.empty()) { @@ -539,7 +539,7 @@ Task>> tryLoadImage(int taskPriority, path path, istrea auto selectorParts = split(channelSelector, ","); if (channelSelector.empty()) { localChannelSelector = i.partName; - } else if (find(begin(selectorParts), end(selectorParts), i.partName) == end(selectorParts)) { + } else if (ranges::find(selectorParts, i.partName) == end(selectorParts)) { localChannelSelector = join(vector{i.partName, channelSelector}, ","); } } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 9f50733bc..327f50782 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -1771,7 +1771,7 @@ void ImageViewer::updateTitle() { channels.erase(unique(begin(channels), end(channels)), end(channels)); auto channelTails = channels; - transform(begin(channelTails), end(channelTails), begin(channelTails), Channel::tail); + ranges::transform(channelTails, begin(channelTails), Channel::tail); caption = mCurrentImage->shortName(); caption += " – "s + mCurrentGroup; @@ -1825,12 +1825,12 @@ int ImageViewer::groupId(const string& groupName) const { } int ImageViewer::imageId(const shared_ptr& image) const { - auto pos = static_cast(distance(begin(mImages), find(begin(mImages), end(mImages), image))); + auto pos = static_cast(distance(begin(mImages), ranges::find(mImages, image))); return pos >= mImages.size() ? -1 : (int)pos; } int ImageViewer::imageId(const string& imageName) const { - auto pos = static_cast(distance(begin(mImages), find_if(begin(mImages), end(mImages), [&](const shared_ptr& image) { + auto pos = static_cast(distance(begin(mImages), ranges::find_if(mImages, [&](const shared_ptr& image) { return image->name() == imageName; }))); return pos >= mImages.size() ? -1 : (int)pos; diff --git a/src/ThreadPool.cpp b/src/ThreadPool.cpp index 3b6fb1524..378816f7b 100644 --- a/src/ThreadPool.cpp +++ b/src/ThreadPool.cpp @@ -30,7 +30,7 @@ void ThreadPool::startThreads(size_t num) { for (size_t i = mThreads.size(); i < mNumThreads; ++i) { mThreads.emplace_back([this, i] { while (true) { - unique_lock lock{mTaskQueueMutex}; + unique_lock lock{mTaskQueueMutex}; // look for a work item while (i < mNumThreads && mTaskQueue.empty()) { @@ -53,7 +53,7 @@ void ThreadPool::startThreads(size_t num) { mNumTasksInSystem--; { - unique_lock localLock{mSystemBusyMutex}; + unique_lock localLock{mSystemBusyMutex}; if (mNumTasksInSystem == 0) { mSystemBusyCondition.notify_all(); @@ -68,7 +68,7 @@ void ThreadPool::shutdownThreads(size_t num) { auto numToClose = min(num, mNumThreads); { - lock_guard lock{mTaskQueueMutex}; + lock_guard lock{mTaskQueueMutex}; mNumThreads -= numToClose; } @@ -81,7 +81,7 @@ void ThreadPool::shutdownThreads(size_t num) { } void ThreadPool::waitUntilFinished() { - unique_lock lock{mSystemBusyMutex}; + unique_lock lock{mSystemBusyMutex}; if (mNumTasksInSystem == 0) { return; @@ -91,7 +91,7 @@ void ThreadPool::waitUntilFinished() { } void ThreadPool::waitUntilFinishedFor(const chrono::microseconds Duration) { - unique_lock lock{mSystemBusyMutex}; + unique_lock lock{mSystemBusyMutex}; if (mNumTasksInSystem == 0) { return; @@ -101,7 +101,7 @@ void ThreadPool::waitUntilFinishedFor(const chrono::microseconds Duration) { } void ThreadPool::flushQueue() { - lock_guard lock{mTaskQueueMutex}; + lock_guard lock{mTaskQueueMutex}; mNumTasksInSystem -= mTaskQueue.size(); while (!mTaskQueue.empty()) { diff --git a/src/imageio/ExrImageLoader.cpp b/src/imageio/ExrImageLoader.cpp index 683570c44..0bc2b218c 100644 --- a/src/imageio/ExrImageLoader.cpp +++ b/src/imageio/ExrImageLoader.cpp @@ -195,18 +195,6 @@ Task> ExrImageLoader::load(istream& iStream, const path& path, return name; }; - bool matched = false; - for (Imf::ChannelList::ConstIterator c = imfChannels.begin(); c != imfChannels.end(); ++c) { - matched |= matchesFuzzy(channelName(c), channelSelector); - if (matched) { - break; - } - } - - if (!matched) { - continue; - } - Imath::Box2i dataWindow = part.header().dataWindow(); Vector2i size = {dataWindow.max.x - dataWindow.min.x + 1 , dataWindow.max.y - dataWindow.min.y + 1}; @@ -215,13 +203,19 @@ Task> ExrImageLoader::load(istream& iStream, const path& path, continue; } + bool matched = false; for (Imf::ChannelList::ConstIterator c = imfChannels.begin(); c != imfChannels.end(); ++c) { string name = channelName(c); if (matchesFuzzy(name, channelSelector)) { rawChannels.emplace_back(parts.size(), name, c.name(), c.channel(), size); + matched = true; } } + if (!matched) { + continue; + } + parts.emplace_back(part); frameBuffers.emplace_back(); } diff --git a/src/imageio/ImageLoader.cpp b/src/imageio/ImageLoader.cpp index e6eea56d9..9d9f395f2 100644 --- a/src/imageio/ImageLoader.cpp +++ b/src/imageio/ImageLoader.cpp @@ -30,7 +30,7 @@ const vector>& ImageLoader::getLoaders() { return imageLoaders; }; - static const vector> imageLoaders = makeLoaders(); + static const vector imageLoaders = makeLoaders(); return imageLoaders; } diff --git a/src/imageio/ImageSaver.cpp b/src/imageio/ImageSaver.cpp index 86c1c5438..e765b5f9f 100644 --- a/src/imageio/ImageSaver.cpp +++ b/src/imageio/ImageSaver.cpp @@ -22,8 +22,8 @@ const vector>& ImageSaver::getSavers() { return imageSavers; }; - static const vector> imageSavers = makeSavers(); + static const vector imageSavers = makeSavers(); return imageSavers; } -TEV_NAMESPACE_END \ No newline at end of file +TEV_NAMESPACE_END From e3b8d0ff7ea131affc7cfb9b27805c1836a8d056 Mon Sep 17 00:00:00 2001 From: Tom94 Date: Tue, 17 Aug 2021 09:07:08 +0200 Subject: [PATCH 54/83] Fix incorrect task priority when image loading --- src/Image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Image.cpp b/src/Image.cpp index 476abc663..9db3e6824 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -587,7 +587,7 @@ Task>> tryLoadImage(int taskPriority, path path, string } Task>> tryLoadImage(path path, string channelSelector) { - co_return co_await tryLoadImage(Image::drawId(), path, channelSelector); + co_return co_await tryLoadImage(-Image::drawId(), path, channelSelector); } void BackgroundImagesLoader::enqueue(const path& path, const string& channelSelector, bool shallSelect) { From 5c96fd45d5c829fbac0a18e02e977a29000266cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 24 Aug 2021 10:37:47 +0200 Subject: [PATCH 55/83] Fix undesired lag when opening an image through IPC --- include/tev/Common.h | 1 + src/Image.cpp | 2 +- src/main.cpp | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/include/tev/Common.h b/include/tev/Common.h index b79743cf3..8fe7134c4 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -283,6 +283,7 @@ void toggleConsole(); // Implemented in main.cpp void scheduleToMainThread(const std::function& fun); +void redrawWindow(); enum ETonemap : int { SRGB = 0, diff --git a/src/Image.cpp b/src/Image.cpp index 9db3e6824..195bb3ee9 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -604,7 +604,7 @@ void BackgroundImagesLoader::enqueue(const path& path, const string& channelSele } if (publishSortedLoads()) { - glfwPostEmptyEvent(); + redrawWindow(); } }); } diff --git a/src/main.cpp b/src/main.cpp index 94cc94147..39d4669b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,6 +46,12 @@ void scheduleToMainThread(const std::function& fun) { } } +void redrawWindow() { + if (sImageViewer) { + sImageViewer->redraw(); + } +} + void handleIpcPacket(const IpcPacket& packet, const std::shared_ptr& imagesLoader) { switch (packet.type()) { case IpcPacket::OpenImage: From 6a918863a22d4d031cae48e939dd4cb32f9207a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 24 Aug 2021 10:39:35 +0200 Subject: [PATCH 56/83] Allow secondary tev instances to become primary ...if the primary instance is closed. --- include/tev/Ipc.h | 6 ++ src/Ipc.cpp | 210 +++++++++++++++++++++++++--------------------- src/main.cpp | 21 +++-- 3 files changed, 135 insertions(+), 102 deletions(-) diff --git a/include/tev/Ipc.h b/include/tev/Ipc.h index 63404eb2f..ee14b1f8b 100644 --- a/include/tev/Ipc.h +++ b/include/tev/Ipc.h @@ -231,6 +231,8 @@ class Ipc { return mIsPrimaryInstance; } + bool attemptToBecomePrimaryInstance(); + void sendToPrimaryInstance(const IpcPacket& message); void receiveFromSecondaryInstance(std::function callback); @@ -267,6 +269,10 @@ class Ipc { }; std::list mSocketConnections; + + std::string mIp; + std::string mPort; + std::string mLockName; }; TEV_NAMESPACE_END diff --git a/src/Ipc.cpp b/src/Ipc.cpp index 6cc8f18cd..05224330c 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -302,42 +302,13 @@ static int closeSocket(Ipc::socket_t socket) { } Ipc::Ipc(const string& hostname) { - const string lockName = ".tev-lock."s + hostname; + mLockName = ".tev-lock."s + hostname; auto parts = split(hostname, ":"); - const string& ip = parts.front(); - const string& port = parts.back(); + mIp = parts.front(); + mPort = parts.back(); try { - // Lock file -#ifdef _WIN32 - //Make sure at most one instance of the tool is running - mInstanceMutex = CreateMutex(NULL, TRUE, lockName.c_str()); - - if (!mInstanceMutex) { - throw runtime_error{tfm::format("Could not obtain global mutex: %s", errorString(lastError()))}; - } - - mIsPrimaryInstance = GetLastError() != ERROR_ALREADY_EXISTS; - if (!mIsPrimaryInstance) { - // No need to keep the handle to the existing mutex if we're not the primary instance. - ReleaseMutex(mInstanceMutex); - CloseHandle(mInstanceMutex); - } -#else - mLockFile = homeDirectory() / lockName; - - mLockFileDescriptor = open(mLockFile.str().c_str(), O_RDWR | O_CREAT, 0666); - if (mLockFileDescriptor == -1) { - throw runtime_error{tfm::format("Could not create lock file: %s", errorString(lastError()))}; - } - - mIsPrimaryInstance = !flock(mLockFileDescriptor, LOCK_EX | LOCK_NB); - if (!mIsPrimaryInstance) { - close(mLockFileDescriptor); - } -#endif - // Networking #ifdef _WIN32 // FIXME: only do this once if multiple Ipc objects are created. @@ -351,79 +322,48 @@ Ipc::Ipc(const string& hostname) { signal(SIGPIPE, SIG_IGN); #endif - // If we're the primary instance, create a server. Otherwise, create a client. - if (mIsPrimaryInstance) { - mSocketFd = socket(AF_INET, SOCK_STREAM, 0); - if (mSocketFd == INVALID_SOCKET) { - throw runtime_error{tfm::format("socket() call failed: %s", errorString(lastSocketError()))}; - } - - makeSocketNonBlocking(mSocketFd); - - // Avoid address in use error that occurs if we quit with a client connected. - int t = 1; - if (setsockopt(mSocketFd, SOL_SOCKET, SO_REUSEADDR, (const char*)&t, sizeof(int)) == SOCKET_ERROR) { - throw runtime_error{tfm::format("setsockopt() call failed: %s", errorString(lastSocketError()))}; - } - - struct sockaddr_in addr; - addr.sin_family = AF_INET; - addr.sin_port = htons((uint16_t)atoi(port.c_str())); - -#ifdef _WIN32 - InetPton(AF_INET, ip.c_str(), &addr.sin_addr); -#else - inet_aton(ip.c_str(), &addr.sin_addr); -#endif + if (attemptToBecomePrimaryInstance()) { + return; + } - if (::bind(mSocketFd, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { - throw runtime_error{tfm::format("bind() call failed: %s", errorString(lastSocketError()))}; - } + // If we're not the primary instance, try to connect to it as a client + struct addrinfo hints = {}, *addrinfo; + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + int err = getaddrinfo(mIp.c_str(), mPort.c_str(), &hints, &addrinfo); + if (err != 0) { + throw runtime_error{tfm::format("getaddrinfo() failed: %s", gai_strerror(err))}; + } - if (listen(mSocketFd, 5) == SOCKET_ERROR) { - throw runtime_error{tfm::format("listen() call failed: %s", errorString(lastSocketError()))}; - } + ScopeGuard addrinfoGuard{[addrinfo] { freeaddrinfo(addrinfo); }}; - tlog::success() << "Initialized IPC, listening on " << ip << ":" << port; - } else { - struct addrinfo hints = {}, *addrinfo; - hints.ai_family = PF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - int err = getaddrinfo(ip.c_str(), port.c_str(), &hints, &addrinfo); - if (err != 0) { - throw runtime_error{tfm::format("getaddrinfo() failed: %s", gai_strerror(err))}; + mSocketFd = INVALID_SOCKET; + for (struct addrinfo* ptr = addrinfo; ptr; ptr = ptr->ai_next) { + mSocketFd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol); + if (mSocketFd == INVALID_SOCKET) { + tlog::warning() << tfm::format("socket() failed: %s", errorString(lastSocketError())); + continue; } - ScopeGuard addrinfoGuard{[addrinfo] { freeaddrinfo(addrinfo); }}; - - mSocketFd = INVALID_SOCKET; - for (struct addrinfo* ptr = addrinfo; ptr; ptr = ptr->ai_next) { - mSocketFd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol); - if (mSocketFd == INVALID_SOCKET) { - tlog::warning() << tfm::format("socket() failed: %s", errorString(lastSocketError())); - continue; + if (connect(mSocketFd, ptr->ai_addr, (int)ptr->ai_addrlen) == SOCKET_ERROR) { + int errorId = lastSocketError(); + if (errorId == SocketError::ConnRefused) { + throw runtime_error{"Connection to primary instance refused"}; + } else { + tlog::warning() << tfm::format("connect() failed: %s", errorString(errorId)); } - if (connect(mSocketFd, ptr->ai_addr, (int)ptr->ai_addrlen) == SOCKET_ERROR) { - int errorId = lastSocketError(); - if (errorId == SocketError::ConnRefused) { - throw runtime_error{"Connection to primary instance refused"}; - } else { - tlog::warning() << tfm::format("connect() failed: %s", errorString(errorId)); - } - - closeSocket(mSocketFd); - mSocketFd = INVALID_SOCKET; - continue; - } - - tlog::success() << "Connected to primary instance at " << ip << ":" << port; - break; // success + closeSocket(mSocketFd); + mSocketFd = INVALID_SOCKET; + continue; } - if (mSocketFd == INVALID_SOCKET) { - throw runtime_error{"Unable to connect to primary instance."}; - } + tlog::success() << "Connected to primary instance at " << mIp << ":" << mPort; + break; // success + } + + if (mSocketFd == INVALID_SOCKET) { + throw runtime_error{"Unable to connect to primary instance."}; } } catch (const runtime_error& e) { tlog::warning() << "Could not initialize IPC; assuming primary instance. " << e.what(); @@ -462,6 +402,84 @@ Ipc::~Ipc() { #endif } +bool Ipc::attemptToBecomePrimaryInstance() { +#ifdef _WIN32 + // Make sure at most one instance of tev is running + mInstanceMutex = CreateMutex(NULL, TRUE, mLockName.c_str()); + + if (!mInstanceMutex) { + throw runtime_error{tfm::format("Could not obtain global mutex: %s", errorString(lastError()))}; + } + + mIsPrimaryInstance = GetLastError() != ERROR_ALREADY_EXISTS; + if (!mIsPrimaryInstance) { + // No need to keep the handle to the existing mutex if we're not the primary instance. + ReleaseMutex(mInstanceMutex); + CloseHandle(mInstanceMutex); + } +#else + mLockFile = homeDirectory() / mLockName; + + mLockFileDescriptor = open(mLockFile.str().c_str(), O_RDWR | O_CREAT, 0666); + if (mLockFileDescriptor == -1) { + throw runtime_error{tfm::format("Could not create lock file: %s", errorString(lastError()))}; + } + + mIsPrimaryInstance = !flock(mLockFileDescriptor, LOCK_EX | LOCK_NB); + if (!mIsPrimaryInstance) { + close(mLockFileDescriptor); + } +#endif + + if (!mIsPrimaryInstance) { + return false; + } + + // Managed to become primary instance + + // If we were previously a secondary instance connected with the primary instance, disconnect + if (mSocketFd != INVALID_SOCKET) { + if (closeSocket(mSocketFd) == SOCKET_ERROR) { + tlog::warning() << "Error closing socket upon becoming primary instance " << mSocketFd << ": " << errorString(lastSocketError()); + } + } + + // Set up primary instance network server + mSocketFd = socket(AF_INET, SOCK_STREAM, 0); + if (mSocketFd == INVALID_SOCKET) { + throw runtime_error{tfm::format("socket() call failed: %s", errorString(lastSocketError()))}; + } + + makeSocketNonBlocking(mSocketFd); + + // Avoid address in use error that occurs if we quit with a client connected. + int t = 1; + if (setsockopt(mSocketFd, SOL_SOCKET, SO_REUSEADDR, (const char*)&t, sizeof(int)) == SOCKET_ERROR) { + throw runtime_error{tfm::format("setsockopt() call failed: %s", errorString(lastSocketError()))}; + } + + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)atoi(mPort.c_str())); + +#ifdef _WIN32 + InetPton(AF_INET, mIp.c_str(), &addr.sin_addr); +#else + inet_aton(mIp.c_str(), &addr.sin_addr); +#endif + + if (::bind(mSocketFd, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { + throw runtime_error{tfm::format("bind() call failed: %s", errorString(lastSocketError()))}; + } + + if (listen(mSocketFd, 5) == SOCKET_ERROR) { + throw runtime_error{tfm::format("listen() call failed: %s", errorString(lastSocketError()))}; + } + + tlog::success() << "Initialized IPC, listening on " << mIp << ":" << mPort; + return true; +} + void Ipc::sendToPrimaryInstance(const IpcPacket& message) { if (mIsPrimaryInstance) { throw runtime_error{"Must be a secondary instance to send to the primary instance."}; diff --git a/src/main.cpp b/src/main.cpp index 39d4669b7..1c6699f52 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -341,7 +341,7 @@ int mainFunc(const vector& arguments) { imagesLoader->enqueue(imageFile, channelSelector, false); } - this_thread::sleep_for(chrono::milliseconds{100}); + this_thread::sleep_for(100ms); } }}; @@ -355,10 +355,17 @@ int mainFunc(const vector& arguments) { // a user starts another instance of tev while one is already running. Note, that this // behavior can be overridden by the -n flag, so not _all_ secondary instances send their // paths to the primary instance. - thread ipcThread; - if (ipc->isPrimaryInstance()) { - ipcThread = thread{[&]() { + thread ipcThread = thread{[&]() { + try { while (!shallShutdown) { + // Attempt to become primary instance in case the primary instance + // got closed at some point. Attempt this with a reasonably low frequency + // to not hog CPU/OS resources. + if (!ipc->isPrimaryInstance() && !ipc->attemptToBecomePrimaryInstance()) { + this_thread::sleep_for(100ms); + continue; + } + ipc->receiveFromSecondaryInstance([&](const IpcPacket& packet) { try { handleIpcPacket(packet, imagesLoader); @@ -369,8 +376,10 @@ int mainFunc(const vector& arguments) { this_thread::sleep_for(chrono::milliseconds{10}); } - }}; - } + } catch (const runtime_error& e) { + tlog::warning() << "Uncaught exception in IPC thread: " << e.what(); + } + }}; // Load images passed via command line in the background prior to // creating our main application such that they are not stalled From 37be03466175cab2fee99dd5219aaef450996b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 24 Aug 2021 10:40:04 +0200 Subject: [PATCH 57/83] When opening tev with no arguments, spawn a new instance --- src/main.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 1c6699f52..49e1a8db9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -290,9 +290,14 @@ int mainFunc(const vector& arguments) { const string hostname = hostnameFlag ? get(hostnameFlag) : "127.0.0.1:14158"; auto ipc = make_shared(hostname); + // If we don't have any images to load, create new windows regardless of flag. + // (In this case, the user likely wants to open a new instance of tev rather + // than focusing the existing one.) + bool newWindow = newWindowFlag || !imageFiles; + // If we're not the primary instance and did not request to open a new window, // simply send the to-be-opened images to the primary instance. - if (!ipc->isPrimaryInstance() && !newWindowFlag) { + if (!ipc->isPrimaryInstance() && !newWindow) { string channelSelector; for (auto imageFile : get(imageFiles)) { if (!imageFile.empty() && imageFile[0] == ':') { From c471f9bab688a00400d83032bbd418ad6f333ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 24 Aug 2021 10:40:16 +0200 Subject: [PATCH 58/83] chrono::milliseconds -> _ms --- src/ImageViewer.cpp | 2 +- src/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 327f50782..983e7a0e5 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -907,7 +907,7 @@ void ImageViewer::draw_contents() { bool isShown = image == mCurrentImage || image == mCurrentReference; // If the image is no longer shown, bump ID immediately. Otherwise, wait until canvas statistics were ready for over 200 ms. - if (!isShown || (std::chrono::steady_clock::now() - mImageCanvas->canvasStatistics()->becameReadyAt()) > std::chrono::milliseconds{200}) { + if (!isShown || (std::chrono::steady_clock::now() - mImageCanvas->canvasStatistics()->becameReadyAt()) > 200ms) { image->bumpId(); auto localIt = it; ++it; diff --git a/src/main.cpp b/src/main.cpp index 49e1a8db9..9aa2497db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -379,7 +379,7 @@ int mainFunc(const vector& arguments) { } }); - this_thread::sleep_for(chrono::milliseconds{10}); + this_thread::sleep_for(10ms); } } catch (const runtime_error& e) { tlog::warning() << "Uncaught exception in IPC thread: " << e.what(); From fd2f75b4e0c1f5a984377e6501774359019a40e1 Mon Sep 17 00:00:00 2001 From: Tom94 Date: Tue, 24 Aug 2021 13:43:50 +0200 Subject: [PATCH 59/83] Fix socket error during IPC initialization --- src/Ipc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ipc.cpp b/src/Ipc.cpp index 05224330c..e289e7488 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -301,7 +301,7 @@ static int closeSocket(Ipc::socket_t socket) { #endif } -Ipc::Ipc(const string& hostname) { +Ipc::Ipc(const string& hostname) : mSocketFd{INVALID_SOCKET} { mLockName = ".tev-lock."s + hostname; auto parts = split(hostname, ":"); From d6ea35c9f8db20f882c512d8bd7c099a1468ea0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 9 Oct 2021 10:44:21 +0200 Subject: [PATCH 60/83] Revert usage of ranges to restore macOS support --- include/tev/Image.h | 14 ++++++++++++-- src/Common.cpp | 4 ++-- src/Image.cpp | 14 +++++++------- src/ImageCanvas.cpp | 2 +- src/ImageViewer.cpp | 6 +++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index 829b3b91e..e4fbd68d5 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -65,7 +65,12 @@ struct ImageData { } const Channel* channel(const std::string& channelName) const { - auto it = std::ranges::find_if(channels, [&channelName](const Channel& c) { return c.name() == channelName; }); + auto it = std::find_if( + std::begin(channels), + std::end(channels), + [&channelName](const Channel& c) { return c.name() == channelName; } + ); + if (it != std::end(channels)) { return &(*it); } else { @@ -74,7 +79,12 @@ struct ImageData { } Channel* mutableChannel(const std::string& channelName) { - auto it = std::ranges::find_if(channels, [&channelName](const Channel& c) { return c.name() == channelName; }); + auto it = std::find_if( + std::begin(channels), + std::end(channels), + [&channelName](const Channel& c) { return c.name() == channelName; } + ); + if (it != std::end(channels)) { return &(*it); } else { diff --git a/src/Common.cpp b/src/Common.cpp index 2534088d6..2b65f5d24 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -62,12 +62,12 @@ vector split(string text, const string& delim) { } string toLower(string str) { - ranges::transform(str, begin(str), [](unsigned char c) { return (char)tolower(c); }); + transform(begin(str), end(str), begin(str), [](unsigned char c) { return (char)tolower(c); }); return str; } string toUpper(string str) { - ranges::transform(str, begin(str), [](unsigned char c) { return (char)toupper(c); }); + transform(begin(str), end(str), begin(str), [](unsigned char c) { return (char)toupper(c); }); return str; } diff --git a/src/Image.cpp b/src/Image.cpp index 195bb3ee9..6ca59a1cd 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -325,7 +325,7 @@ vector Image::getGroupedChannels(const string& layerName) const { auto channelTails = channels; // Remove duplicates channelTails.erase(unique(begin(channelTails), end(channelTails)), end(channelTails)); - ranges::transform(channelTails, begin(channelTails), Channel::tail); + transform(begin(channelTails), end(channelTails), begin(channelTails), Channel::tail); string channelsString = join(channelTails, ","); string name; @@ -345,7 +345,7 @@ vector Image::getGroupedChannels(const string& layerName) const { vector allChannels = mData.channelsInLayer(layerName); - auto alphaIt = ranges::find(allChannels, alphaChannelName); + auto alphaIt = find(begin(allChannels), end(allChannels), alphaChannelName); bool hasAlpha = alphaIt != end(allChannels); if (hasAlpha) { allChannels.erase(alphaIt); @@ -357,7 +357,7 @@ vector Image::getGroupedChannels(const string& layerName) const { vector groupChannels; for (const string& channel : group) { string name = layerPrefix + channel; - auto it = ranges::find(allChannels, name); + auto it = find(begin(allChannels), end(allChannels), name); if (it != end(allChannels)) { groupChannels.emplace_back(name); allChannels.erase(it); @@ -436,7 +436,7 @@ void Image::updateChannel(const string& channelName, int x, int y, int width, in // Update textures that are cached for this channel for (auto& kv : mTextures) { auto& imageTexture = kv.second; - if (ranges::find(imageTexture.channels, channelName) == end(imageTexture.channels)) { + if (find(begin(imageTexture.channels), end(imageTexture.channels), channelName) == end(imageTexture.channels)) { continue; } @@ -481,9 +481,9 @@ string Image::toString() const { sstream << "\nChannels:\n"; auto localLayers = mData.layers; - ranges::transform(localLayers, begin(localLayers), [this](string layer) { + transform(begin(localLayers), end(localLayers), begin(localLayers), [this](string layer) { auto channels = mData.channelsInLayer(layer); - ranges::transform(channels, begin(channels), [](string channel) { + transform(begin(channels), end(channels), begin(channels), [](string channel) { return Channel::tail(channel); }); if (layer.empty()) { @@ -539,7 +539,7 @@ Task>> tryLoadImage(int taskPriority, path path, istrea auto selectorParts = split(channelSelector, ","); if (channelSelector.empty()) { localChannelSelector = i.partName; - } else if (ranges::find(selectorParts, i.partName) == end(selectorParts)) { + } else if (find(begin(selectorParts), end(selectorParts), i.partName) == end(selectorParts)) { localChannelSelector = join(vector{i.partName, channelSelector}, ","); } } diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index c37f045a0..5789c96fa 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -297,7 +297,7 @@ void ImageCanvas::draw(NVGcontext* ctx) { // If the coordinate system is in any sort of way non-trivial, draw it! if (mImage->dataWindow() != mImage->displayWindow() || mImage->displayWindow().min != Vector2i{0} || - mReference && (mReference->dataWindow() != mImage->dataWindow() || mReference->displayWindow() != mImage->displayWindow())) { + (mReference && (mReference->dataWindow() != mImage->dataWindow() || mReference->displayWindow() != mImage->displayWindow()))) { drawCoordinateSystem(ctx); } } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 983e7a0e5..ee26b141e 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -1771,7 +1771,7 @@ void ImageViewer::updateTitle() { channels.erase(unique(begin(channels), end(channels)), end(channels)); auto channelTails = channels; - ranges::transform(channelTails, begin(channelTails), Channel::tail); + transform(begin(channelTails), end(channelTails), begin(channelTails), Channel::tail); caption = mCurrentImage->shortName(); caption += " – "s + mCurrentGroup; @@ -1825,12 +1825,12 @@ int ImageViewer::groupId(const string& groupName) const { } int ImageViewer::imageId(const shared_ptr& image) const { - auto pos = static_cast(distance(begin(mImages), ranges::find(mImages, image))); + auto pos = static_cast(distance(begin(mImages), find(begin(mImages), end(mImages), image))); return pos >= mImages.size() ? -1 : (int)pos; } int ImageViewer::imageId(const string& imageName) const { - auto pos = static_cast(distance(begin(mImages), ranges::find_if(mImages, [&](const shared_ptr& image) { + auto pos = static_cast(distance(begin(mImages), find_if(begin(mImages), end(mImages), [&](const shared_ptr& image) { return image->name() == imageName; }))); return pos >= mImages.size() ? -1 : (int)pos; From 07769d7eddbbc2dcf9c64e1019cfe8ea2582ae96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sat, 9 Oct 2021 11:02:14 +0200 Subject: [PATCH 61/83] Fix incorrect initializer braces --- src/ImageViewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index ee26b141e..70a5db728 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -963,7 +963,7 @@ void ImageViewer::draw_contents() { ); } } else { - mHistogram->setNChannels({1}); + mHistogram->setNChannels(1); mHistogram->setValues({0.0f}); mHistogram->setMinimum(0); mHistogram->setMean(0); From 6b16de9caafc5758314166526da205500f29a0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sun, 24 Oct 2021 11:22:21 +0200 Subject: [PATCH 62/83] Fix warnings --- include/tev/Common.h | 4 ++-- src/Image.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/tev/Common.h b/include/tev/Common.h index 8fe7134c4..c99e03e10 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -128,8 +128,8 @@ namespace nanogui { template bool operator==(const Matrix& a, const Matrix& b) { - for (int m = 0; m < Size; ++m) { - for (int n = 0; n < Size; ++n) { + for (size_t m = 0; m < Size; ++m) { + for (size_t n = 0; n < Size; ++n) { if (a.m[m][n] != b.m[m][n]) { return false; } diff --git a/src/Image.cpp b/src/Image.cpp index 6ca59a1cd..3daa14393 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -61,8 +61,8 @@ Task ImageData::convertToRec709(int priority) { Channel* b = nullptr; if (!( - (r = mutableChannel(layerPrefix + "R")) && (g = mutableChannel(layerPrefix + "G")) && (b = mutableChannel(layerPrefix + "B")) || - (r = mutableChannel(layerPrefix + "r")) && (g = mutableChannel(layerPrefix + "g")) && (b = mutableChannel(layerPrefix + "b")) + ((r = mutableChannel(layerPrefix + "R")) && (g = mutableChannel(layerPrefix + "G")) && (b = mutableChannel(layerPrefix + "B"))) || + ((r = mutableChannel(layerPrefix + "r")) && (g = mutableChannel(layerPrefix + "g")) && (b = mutableChannel(layerPrefix + "b"))) )) { // No RGB-triplet found continue; From 42e0cbfa525ccda2added1f16494ef7677d12a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sun, 24 Oct 2021 11:29:46 +0200 Subject: [PATCH 63/83] Fold conversion to rec709 colors into ImageData::ensureValid() --- src/Image.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Image.cpp b/src/Image.cpp index 3daa14393..63e74a8c4 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -47,7 +47,7 @@ vector ImageData::channelsInLayer(string layerName) const { Task ImageData::convertToRec709(int priority) { // No need to do anything for identity transforms - if (toRec709 == nanogui::Matrix4f{1.0f}) { + if (toRec709 == Matrix4f{1.0f}) { co_return; } @@ -83,6 +83,10 @@ Task ImageData::convertToRec709(int priority) { for (auto& task : tasks) { co_await task; } + + // Since the image data is now in Rec709 space, + // converting to Rec709 is the identity transform. + toRec709 = Matrix4f{1.0f}; } void ImageData::alphaOperation(const function& func) { @@ -193,7 +197,10 @@ Task ImageData::ensureValid(const string& channelSelector, int taskPriorit co_await multiplyAlpha(taskPriority); } + co_await convertToRec709(taskPriority); + TEV_ASSERT(hasPremultipliedAlpha, "tev assumes an internal pre-multiplied-alpha representation."); + TEV_ASSERT(toRec709 == Matrix4f{1.0f}, "tev assumes an images to be internally represented in sRGB/Rec709 space."); } atomic Image::sId(0); @@ -530,7 +537,6 @@ Task>> tryLoadImage(int taskPriority, path path, istrea vector> images; for (auto& i : imageData) { co_await i.ensureValid(channelSelector, taskPriority); - co_await i.convertToRec709(taskPriority); // If multiple image "parts" were loaded and they have names, // ensure that these names are present in the channel selector. From 96d89484ec1ba2d5c3e8ae8c0ed3ef4785cc87ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sun, 7 Nov 2021 11:08:56 +0100 Subject: [PATCH 64/83] Reflect the C++20 requirement in both CMakeLists.txt as well as the README --- CMakeLists.txt | 1 + README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 13392dc3c..d9da54361 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ if (APPLE) endif() set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) if (MSVC) # Disable annoying secure CRT warnings diff --git a/README.md b/README.md index 241437ba9..4c42c8a08 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ brew install --cask tev ## Building tev -All that is required for building __tev__ is a C++17-compatible compiler. Begin by cloning this repository and all its submodules using the following command: +All that is required for building __tev__ is a C++20-compatible compiler. Begin by cloning this repository and all its submodules using the following command: ```sh $ git clone --recursive https://github.com/Tom94/tev ``` @@ -131,7 +131,7 @@ $ make install ### Windows -On Windows, install [CMake](https://cmake.org/download/), open the included GUI application, and point it to the root directory of __tev__. CMake will then generate [Visual Studio](https://www.visualstudio.com/) project files for compiling __tev__. Make sure you select at least Visual Studio 2017 or higher! +On Windows, install [CMake](https://cmake.org/download/), open the included GUI application, and point it to the root directory of __tev__. CMake will then generate [Visual Studio](https://www.visualstudio.com/) project files for compiling __tev__. Make sure you select at least Visual Studio 2019 or higher! ## License From 86771e1490fb8be952914224b0d725ef5951c735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Sun, 7 Nov 2021 11:38:07 +0100 Subject: [PATCH 65/83] Check system endianness through C++20 header --- include/tev/Common.h | 5 ----- src/imageio/PfmImageLoader.cpp | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/include/tev/Common.h b/include/tev/Common.h index c99e03e10..91730ad45 100644 --- a/include/tev/Common.h +++ b/include/tev/Common.h @@ -170,11 +170,6 @@ inline float swapBytes(float value) { return result; } -inline bool isSystemLittleEndian() { - uint16_t beef = 0xbeef; - return *reinterpret_cast(&beef) == 0xef; -} - inline int codePointLength(char first) { if ((first & 0xf8) == 0xf0) { return 4; diff --git a/src/imageio/PfmImageLoader.cpp b/src/imageio/PfmImageLoader.cpp index 3b211d1a9..0993642f9 100644 --- a/src/imageio/PfmImageLoader.cpp +++ b/src/imageio/PfmImageLoader.cpp @@ -4,6 +4,8 @@ #include #include +#include + using namespace filesystem; using namespace nanogui; using namespace std; @@ -71,7 +73,7 @@ Task> PfmImageLoader::load(istream& iStream, const path&, cons } // Reverse bytes of every float if endianness does not match up with system - const bool shallSwapBytes = isSystemLittleEndian() != isPfmLittleEndian; + const bool shallSwapBytes = (std::endian::native == std::endian::little) != isPfmLittleEndian; co_await gThreadPool->parallelForAsync(0, size.y(), [&](int y) { for (int x = 0; x < size.x(); ++x) { From 50f0958683e033d3d16eebea59026c74824d2d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 15 Nov 2021 15:21:46 +0100 Subject: [PATCH 66/83] Update automated release build scripts on Windows --- scripts/create-exe.bat | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/scripts/create-exe.bat b/scripts/create-exe.bat index 9d7ae8faf..ab4b3f052 100644 --- a/scripts/create-exe.bat +++ b/scripts/create-exe.bat @@ -3,31 +3,21 @@ set cwd=%cd% cd /D %~dp0 -set DevCmd="C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat" +set DevCmd="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" set MSBuildOptions=/v:m /p:Configuration=Release set BuildDir64="build-exe-64" -set BuildDir32="build-exe-32" call %DevCmd% echo Building 64-bit tev... mkdir %BuildDir64% cd %BuildDir64% -cmake -DTEV_DEPLOY=1 -G "Visual Studio 15 2017 Win64" ..\.. +cmake -DTEV_DEPLOY=1 -G "Visual Studio 16 2019" ..\.. msbuild %MSBuildOptions% tev.sln move "Release\tev.exe" "..\..\tev.exe" cd .. rmdir /S /Q %BuildDir64% -echo Building 32-bit tev... -mkdir %BuildDir32% -cd %BuildDir32% -cmake -DTEV_DEPLOY=1 -G "Visual Studio 15 2017" ..\.. -msbuild %MSBuildOptions% tev.sln -move "Release\tev.exe" "..\..\tev-32bit.exe" -cd .. -rmdir /S /Q %BuildDir32% - echo Returning to original directory. cd /D %cwd% pause From 6fe81dcc4b802f3b88783302ffe170158eb7ffcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 15 Nov 2021 16:17:38 +0100 Subject: [PATCH 67/83] Bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d9da54361..8aaab08d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ project( DESCRIPTION "High dynamic range (HDR) image comparison tool for graphics people. With an emphasis on OpenEXR images." LANGUAGES C CXX ) -set(TEV_VERSION "1.18") +set(TEV_VERSION "1.19") if (NOT TEV_DEPLOY) set(TEV_VERSION "${TEV_VERSION}dev") From 42ca2d6b911265714b2d87a26ab77c889a051adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 23 Nov 2021 22:47:08 +0100 Subject: [PATCH 68/83] Update macOS binary generation scripts --- scripts/create-dmg-backwards.sh | 14 ++++++++------ scripts/create-dmg.sh | 12 +++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/scripts/create-dmg-backwards.sh b/scripts/create-dmg-backwards.sh index b8e724fc6..6bf669c77 100755 --- a/scripts/create-dmg-backwards.sh +++ b/scripts/create-dmg-backwards.sh @@ -6,19 +6,21 @@ echo "Building backwards-compatible tev..." BUILD_DIR="build-dmg" -mkdir $BUILD_DIR && cd $BUILD_DIR -MACOSX_DEPLOYMENT_TARGET=10.10 +mkdir $BUILD_DIR +cd $BUILD_DIR +MACOSX_DEPLOYMENT_TARGET=10.14 cmake \ - -DCMAKE_OSX_SYSROOT=/Users/tom94/Projects/MacOSX-SDKs/MacOSX10.10.sdk/ \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=10.10 \ + -DCMAKE_OSX_SYSROOT=/Users/tom94/Projects/MacOSX-SDKs/MacOSX10.14.sdk/ \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \ -DTEV_DEPLOY=1 \ ../.. make -j cd .. echo "Creating dmg..." -RESULT="../tev-pre-mojave.dmg" -test -f $RESULT && rm $RESULT +RESULT="../tev-pre-catalina.dmg" +test -f $RESULT +rm $RESULT ./create-dmg/create-dmg --window-size 500 300 --icon-size 96 --volname "tev Installer" --app-drop-link 360 105 --icon tev.app 130 105 $RESULT $BUILD_DIR/tev.app echo "Removing temporary build dir..." diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh index da9b35904..5e1851791 100755 --- a/scripts/create-dmg.sh +++ b/scripts/create-dmg.sh @@ -6,10 +6,11 @@ echo "Building tev..." BUILD_DIR="build-dmg" -mkdir $BUILD_DIR && cd $BUILD_DIR -MACOSX_DEPLOYMENT_TARGET=10.14 +mkdir $BUILD_DIR +cd $BUILD_DIR +MACOSX_DEPLOYMENT_TARGET=10.15 cmake \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ -DTEV_DEPLOY=1 \ ../.. make -j @@ -17,8 +18,9 @@ cd .. echo "Creating dmg..." RESULT="../tev.dmg" -test -f $RESULT && rm $RESULT -./create-dmg/create-dmg --window-size 500 300 --icon-size 96 --volname "tev Mojave Installer" --app-drop-link 360 105 --icon tev.app 130 105 $RESULT $BUILD_DIR/tev.app +test -f $RESULT +rm $RESULT +./create-dmg/create-dmg --window-size 500 300 --icon-size 96 --volname "tev Installer" --app-drop-link 360 105 --icon tev.app 130 105 $RESULT $BUILD_DIR/tev.app echo "Removing temporary build dir..." rm -rf $BUILD_DIR From 43280da2cd08e6a806a48bb2466bbdfea9588011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 23 Nov 2021 22:47:39 +0100 Subject: [PATCH 69/83] Fix memory leak of color histograms --- include/tev/Image.h | 11 +++++++++++ include/tev/ImageCanvas.h | 5 ++++- src/Image.cpp | 4 ++++ src/ImageCanvas.cpp | 27 +++++++++++++++++++++++---- src/ImageViewer.cpp | 2 +- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/include/tev/Image.h b/include/tev/Image.h index e4fbd68d5..f817a4c58 100644 --- a/include/tev/Image.h +++ b/include/tev/Image.h @@ -166,7 +166,12 @@ class Image { } void bumpId() { + int oldId = mId; mId = sId++; + + if (mStaleIdCallback) { + mStaleIdCallback(oldId); + } } static int drawId() { @@ -175,6 +180,10 @@ class Image { void updateChannel(const std::string& channelName, int x, int y, int width, int height, const std::vector& data); + void setStaleIdCallback(const std::function& callback) { + mStaleIdCallback = callback; + } + std::string toString() const; private: @@ -197,6 +206,8 @@ class Image { std::vector mChannelGroups; + std::function mStaleIdCallback; + int mId; }; diff --git a/include/tev/ImageCanvas.h b/include/tev/ImageCanvas.h index 304a2aa64..785f3e12e 100644 --- a/include/tev/ImageCanvas.h +++ b/include/tev/ImageCanvas.h @@ -126,6 +126,8 @@ class ImageCanvas : public nanogui::Canvas { std::shared_ptr>> canvasStatistics(); + void purgeCanvasStatistics(int imageId); + private: static std::vector channelsFromImages( std::shared_ptr image, @@ -174,7 +176,8 @@ class ImageCanvas : public nanogui::Canvas { ETonemap mTonemap = SRGB; EMetric mMetric = Error; - std::map>>> mMeanValues; + std::map>>> mCanvasStatistics; + std::map> mImageIdToCanvasStatisticsKey; }; TEV_NAMESPACE_END diff --git a/src/Image.cpp b/src/Image.cpp index 63e74a8c4..bba51c4b3 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -220,6 +220,10 @@ Image::~Image() { // hits zero there. This is required, because OpenGL calls must always happen // on the main thread. scheduleToMainThread([textures = std::move(mTextures)] {}); + + if (mStaleIdCallback) { + mStaleIdCallback(mId); + } } string Image::shortName() const { diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 5789c96fa..00b95cd4b 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -564,8 +564,8 @@ shared_ptr>> ImageCanvas::canvasStatistics() { tfm::format("%d-%s-%d-%d", mImage->id(), channels, mReference->id(), mMetric) : tfm::format("%d-%s", mImage->id(), channels); - auto iter = mMeanValues.find(key); - if (iter != end(mMeanValues)) { + auto iter = mCanvasStatistics.find(key); + if (iter != end(mCanvasStatistics)) { return iter->second; } @@ -578,14 +578,33 @@ shared_ptr>> ImageCanvas::canvasStatistics() { auto metric = mMetric; promise> promise; - mMeanValues.insert(make_pair(key, make_shared>>(promise.get_future()))); + mCanvasStatistics.insert(make_pair(key, make_shared>>(promise.get_future()))); + + // Remember the keys associateed with the participating images. Such that their + // canvas statistics can be retrieved and deleted when either of the images + // is closed or mutated. + mImageIdToCanvasStatisticsKey[mImage->id()].emplace_back(key); + mImage->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); + + if (mReference) { + mImageIdToCanvasStatisticsKey[mReference->id()].emplace_back(key); + mReference->setStaleIdCallback([this](int id) { purgeCanvasStatistics(id); }); + } invokeTaskDetached([image, reference, requestedChannelGroup, metric, priority, p=std::move(promise)]() mutable -> Task { co_await gThreadPool->enqueueCoroutine(priority); p.set_value(co_await computeCanvasStatistics(image, reference, requestedChannelGroup, metric, priority)); }); - return mMeanValues.at(key); + return mCanvasStatistics.at(key); +} + +void ImageCanvas::purgeCanvasStatistics(int imageId) { + for (const auto& key : mImageIdToCanvasStatisticsKey[imageId]) { + mCanvasStatistics.erase(key); + } + + mImageIdToCanvasStatisticsKey.erase(imageId); } vector ImageCanvas::channelsFromImages( diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 70a5db728..79a769b22 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -907,7 +907,7 @@ void ImageViewer::draw_contents() { bool isShown = image == mCurrentImage || image == mCurrentReference; // If the image is no longer shown, bump ID immediately. Otherwise, wait until canvas statistics were ready for over 200 ms. - if (!isShown || (std::chrono::steady_clock::now() - mImageCanvas->canvasStatistics()->becameReadyAt()) > 200ms) { + if (!isShown || std::chrono::steady_clock::now() - mImageCanvas->canvasStatistics()->becameReadyAt() > 200ms) { image->bumpId(); auto localIt = it; ++it; From 832c7ee85c6812a786bb7eb9ad26b7c55ffc4ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Tue, 23 Nov 2021 23:36:21 +0100 Subject: [PATCH 70/83] Fix undefined behavior in IPC packet handling (memcpy on overlapping ranges) --- src/Ipc.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Ipc.cpp b/src/Ipc.cpp index e289e7488..4839523c8 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -591,14 +591,17 @@ void Ipc::SocketConnection::service(function callback) { } } - // TODO: we could save the memcpy by treating 'buffer' as a ring-buffer, + // TODO: we could save the copy by treating 'buffer' as a ring-buffer, // but it's probably not worth the trouble. Revisit when someone throws around // buffers with a size of gigabytes. if (processedOffset > 0) { // There's a partial message; copy it to the start of 'buffer' // and update the offsets accordingly. - memcpy(mBuffer.data(), mBuffer.data() + processedOffset, mRecvOffset - processedOffset); - mRecvOffset -= processedOffset; + size_t nBytes = mRecvOffset - processedOffset; + for (size_t i = 0; i < nBytes; ++i) { + mBuffer[i] = mBuffer[i + processedOffset]; + } + mRecvOffset = nBytes; } } } From aa8f012a233e98423b92bce912579c454c0a71a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Wed, 24 Nov 2021 11:23:04 +0100 Subject: [PATCH 71/83] Minor improvements --- src/ImageViewer.cpp | 3 ++- src/Ipc.cpp | 9 +++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 79a769b22..42d35b986 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -520,8 +520,9 @@ bool ImageViewer::mouse_motion_event(const nanogui::Vector2i& p, const nanogui:: } // Only need high refresh rate responsiveness if tev is actually in focus. - if (focused()) + if (focused()) { redraw(); + } if (mIsDraggingSidebar || canDragSidebarFrom(p)) { mSidebarLayout->set_cursor(Cursor::HResize); diff --git a/src/Ipc.cpp b/src/Ipc.cpp index 4839523c8..888f4425a 100644 --- a/src/Ipc.cpp +++ b/src/Ipc.cpp @@ -591,17 +591,14 @@ void Ipc::SocketConnection::service(function callback) { } } - // TODO: we could save the copy by treating 'buffer' as a ring-buffer, + // TODO: we could save the memcpy by treating 'buffer' as a ring-buffer, // but it's probably not worth the trouble. Revisit when someone throws around // buffers with a size of gigabytes. if (processedOffset > 0) { // There's a partial message; copy it to the start of 'buffer' // and update the offsets accordingly. - size_t nBytes = mRecvOffset - processedOffset; - for (size_t i = 0; i < nBytes; ++i) { - mBuffer[i] = mBuffer[i + processedOffset]; - } - mRecvOffset = nBytes; + memmove(mBuffer.data(), mBuffer.data() + processedOffset, mRecvOffset - processedOffset); + mRecvOffset -= processedOffset; } } } From a1f053eea26dfe8e9b9eca7919a56e67a1b606e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 25 Nov 2021 10:41:14 +0100 Subject: [PATCH 72/83] Bump dev version to 1.20 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8aaab08d5..cc5fe6523 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ project( DESCRIPTION "High dynamic range (HDR) image comparison tool for graphics people. With an emphasis on OpenEXR images." LANGUAGES C CXX ) -set(TEV_VERSION "1.19") +set(TEV_VERSION "1.20") if (NOT TEV_DEPLOY) set(TEV_VERSION "${TEV_VERSION}dev") From e34f57f942ecc5bfe061cd0b2d60001c1efcd74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 16 Dec 2021 09:13:38 +0100 Subject: [PATCH 73/83] Add missing #include --- include/tev/SharedQueue.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/tev/SharedQueue.h b/include/tev/SharedQueue.h index a9dc9339f..0e2fcdf68 100644 --- a/include/tev/SharedQueue.h +++ b/include/tev/SharedQueue.h @@ -5,9 +5,10 @@ #include +#include #include #include -#include +#include TEV_NAMESPACE_BEGIN From f2b1bf739246445f3ea0aa1ea50d89b8c10aefc1 Mon Sep 17 00:00:00 2001 From: Tiago Chaves Date: Sat, 27 Nov 2021 14:21:44 -0300 Subject: [PATCH 74/83] Add initial support for the QOI image format --- .gitmodules | 3 + CMakeLists.txt | 3 + README.md | 1 + dependencies/CMakeLists.txt | 2 + dependencies/qoi | 1 + include/tev/imageio/QoiImageLoader.h | 23 ++++++++ include/tev/imageio/QoiImageSaver.h | 28 +++++++++ src/HelpWindow.cpp | 1 + src/ImageViewer.cpp | 2 + src/imageio/ImageLoader.cpp | 2 + src/imageio/ImageSaver.cpp | 2 + src/imageio/QoiImageLoader.cpp | 88 ++++++++++++++++++++++++++++ src/imageio/QoiImageSaver.cpp | 35 +++++++++++ src/imageio/QoiImplementation.c | 11 ++++ src/imageio/StbiLdrImageSaver.cpp | 10 ++-- 15 files changed, 207 insertions(+), 5 deletions(-) create mode 160000 dependencies/qoi create mode 100644 include/tev/imageio/QoiImageLoader.h create mode 100644 include/tev/imageio/QoiImageSaver.h create mode 100644 src/imageio/QoiImageLoader.cpp create mode 100644 src/imageio/QoiImageSaver.cpp create mode 100644 src/imageio/QoiImplementation.c diff --git a/.gitmodules b/.gitmodules index 521bbb1e1..bf64e6c78 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "dependencies/nanogui"] path = dependencies/nanogui url = https://github.com/Tom94/nanogui-1 +[submodule "dependencies/qoi"] + path = dependencies/qoi + url = https://github.com/phoboslab/qoi.git diff --git a/CMakeLists.txt b/CMakeLists.txt index cc5fe6523..5e52e5bd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,8 @@ set(TEV_SOURCES include/tev/imageio/ImageLoader.h src/imageio/ImageLoader.cpp include/tev/imageio/ImageSaver.h src/imageio/ImageSaver.cpp include/tev/imageio/PfmImageLoader.h src/imageio/PfmImageLoader.cpp + include/tev/imageio/QoiImageLoader.h src/imageio/QoiImageLoader.cpp src/imageio/QoiImplementation.c + include/tev/imageio/QoiImageSaver.h src/imageio/QoiImageSaver.cpp include/tev/imageio/StbiHdrImageSaver.h src/imageio/StbiHdrImageSaver.cpp include/tev/imageio/StbiImageLoader.h src/imageio/StbiImageLoader.cpp include/tev/imageio/StbiLdrImageSaver.h src/imageio/StbiLdrImageSaver.cpp @@ -206,6 +208,7 @@ include_directories( ${NANOGUI_EXTRA_INCS} ${NANOGUI_INCLUDE} ${OPENEXR_INCLUDE_DIRS} + ${QOI_INCLUDE} ${STB_INCLUDE} ${TINYFORMAT_INCLUDE} ${TINYLOGGER_INCLUDE} diff --git a/README.md b/README.md index 4c42c8a08..45d7e9d9f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ While the predominantly supported file format is OpenEXR certain other types of - __HDR__, BMP, GIF, JPEG, PIC, PNG, PNM, PSD, TGA (via [stb_image](https://github.com/wjakob/nanovg/blob/master/src/stb_image.h)) - stb_image only supports [subsets](https://github.com/wjakob/nanovg/blob/master/src/stb_image.h#L23) of each of the aforementioned file formats. - Low-dynamic-range (LDR) images are "promoted" to HDR through the reverse sRGB transformation. +- __QOI__ (via [qoi](https://github.com/phoboslab/qoi)) ## Screenshot diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 66d2e9fdf..b640583cc 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -91,6 +91,8 @@ PARENT_SCOPE) set(FILESYSTEM_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/filesystem PARENT_SCOPE) +set(QOI_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/qoi PARENT_SCOPE) + set(STB_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/stb PARENT_SCOPE) set(TINYFORMAT_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/tinyformat PARENT_SCOPE) diff --git a/dependencies/qoi b/dependencies/qoi new file mode 160000 index 000000000..a902b57ed --- /dev/null +++ b/dependencies/qoi @@ -0,0 +1 @@ +Subproject commit a902b57ede3a54f6ea01a7161aa88e6ed3d98649 diff --git a/include/tev/imageio/QoiImageLoader.h b/include/tev/imageio/QoiImageLoader.h new file mode 100644 index 000000000..b34a30127 --- /dev/null +++ b/include/tev/imageio/QoiImageLoader.h @@ -0,0 +1,23 @@ +// This file was developed by Tiago Chaves & Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#pragma once + +#include +#include + +#include + +TEV_NAMESPACE_BEGIN + +class QoiImageLoader : public ImageLoader { +public: + bool canLoadFile(std::istream& iStream) const override; + Task> load(std::istream& iStream, const filesystem::path& path, const std::string& channelSelector, int priority) const override; + + std::string name() const override { + return "QOI"; + } +}; + +TEV_NAMESPACE_END diff --git a/include/tev/imageio/QoiImageSaver.h b/include/tev/imageio/QoiImageSaver.h new file mode 100644 index 000000000..72b9ae3f9 --- /dev/null +++ b/include/tev/imageio/QoiImageSaver.h @@ -0,0 +1,28 @@ +// This file was developed by Tiago Chaves & Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#pragma once + +#include + +#include + +TEV_NAMESPACE_BEGIN + +class QoiImageSaver : public TypedImageSaver { +public: + void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; + + bool hasPremultipliedAlpha() const override { + // TODO: Update this when the final QOI data format is decided. + // https://github.com/phoboslab/qoi/issues/37 + return false; + } + + virtual bool canSaveFile(const std::string& extension) const override { + std::string lowerExtension = toLower(extension); + return lowerExtension == "qoi"; + } +}; + +TEV_NAMESPACE_END diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index efb5409fc..8355e6c8b 100644 --- a/src/HelpWindow.cpp +++ b/src/HelpWindow.cpp @@ -170,6 +170,7 @@ HelpWindow::HelpWindow(Widget *parent, bool supportsHdr, function closeC addLibrary(about, "NanoGUI", "", "Small GUI Library"); addLibrary(about, "NanoVG", "", "Small Vector Graphics Library"); addLibrary(about, "OpenEXR", "", "High Dynamic-Range (HDR) Image File Format"); + addLibrary(about, "qoi", "", "File Format for Fast, Lossless Image Compression"); addLibrary(about, "stb_image(_write)", "", "Single-Header Library for Loading and Writing Images"); addLibrary(about, "tinyformat", "", "Minimal Type-Safe printf() Replacement"); addLibrary(about, "tinylogger", "", "Minimal Pretty-Logging Library"); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 42d35b986..a8ef59ae7 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -1561,6 +1561,7 @@ void ImageViewer::openImageDialog() { {"ppm", "Portable PixMap image"}, {"psd", "PSD image"}, {"tga", "Truevision TGA image"}, + {"qoi", "Quite OK Image format"}, }, false, true); for (size_t i = 0; i < paths.size(); ++i) { @@ -1587,6 +1588,7 @@ void ImageViewer::saveImageDialog() { {"jpeg", "JPEG image"}, {"png", "Portable Network Graphics image"}, {"tga", "Truevision TGA image"}, + {"qoi", "Quite OK Image format"}, }, true)); if (path.empty()) { diff --git a/src/imageio/ImageLoader.cpp b/src/imageio/ImageLoader.cpp index 9d9f395f2..d697cf2df 100644 --- a/src/imageio/ImageLoader.cpp +++ b/src/imageio/ImageLoader.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #ifdef _WIN32 # include @@ -26,6 +27,7 @@ const vector>& ImageLoader::getLoaders() { #ifdef _WIN32 imageLoaders.emplace_back(new DdsImageLoader()); #endif + imageLoaders.emplace_back(new QoiImageLoader()); imageLoaders.emplace_back(new StbiImageLoader()); return imageLoaders; }; diff --git a/src/imageio/ImageSaver.cpp b/src/imageio/ImageSaver.cpp index e765b5f9f..cb9d0db56 100644 --- a/src/imageio/ImageSaver.cpp +++ b/src/imageio/ImageSaver.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -17,6 +18,7 @@ const vector>& ImageSaver::getSavers() { auto makeSavers = [] { vector> imageSavers; imageSavers.emplace_back(new ExrImageSaver()); + imageSavers.emplace_back(new QoiImageSaver()); imageSavers.emplace_back(new StbiHdrImageSaver()); imageSavers.emplace_back(new StbiLdrImageSaver()); return imageSavers; diff --git a/src/imageio/QoiImageLoader.cpp b/src/imageio/QoiImageLoader.cpp new file mode 100644 index 000000000..5892afe50 --- /dev/null +++ b/src/imageio/QoiImageLoader.cpp @@ -0,0 +1,88 @@ +// This file was developed by Tiago Chaves & Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#include +#include + +#include + +using namespace filesystem; +using namespace nanogui; +using namespace std; + +TEV_NAMESPACE_BEGIN + +bool QoiImageLoader::canLoadFile(istream& iStream) const { + char b[4]; + iStream.read(b, sizeof(b)); + + bool result = !!iStream && iStream.gcount() == sizeof(b) && string(b, sizeof(b)) == "qoif"; + + iStream.clear(); + iStream.seekg(0); + return result; +} + +Task> QoiImageLoader::load(istream& iStream, const path&, const string& channelSelector, int priority) const { + vector result(1); + ImageData& resultData = result.front(); + + char magic[4]; + iStream.read(magic, 4); + string magicString(magic, 4); + + if (magicString != "qoif") { + throw invalid_argument{tfm::format("Invalid magic QOI string %s", magicString)}; + } + + iStream.clear(); + iStream.seekg(0, iStream.end); + size_t dataSize = iStream.tellg(); + iStream.seekg(0, iStream.beg); + vector data(dataSize); + iStream.read(data.data(), dataSize); + + // FIXME: we assume numChannels = 4 as this information is not stored in the format. + // TODO: Update this when the final QOI data format is decided. + // https://github.com/phoboslab/qoi/issues/37 + int numChannels = 4; + Vector2i size; + + void* decodedData = qoi_decode(data.data(), static_cast(dataSize), &size.x(), &size.y(), numChannels); + + ScopeGuard decodedDataGuard{[decodedData] { free(decodedData); }}; + + if (!decodedData) { + throw invalid_argument{"Failed to decode data from the QOI format."}; + } + + auto numPixels = (size_t)size.x() * size.y(); + if (numPixels == 0) { + throw invalid_argument{"Image has zero pixels."}; + } + + resultData.channels = makeNChannels(numChannels, size); + int alphaChannelIndex = 3; + + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { + auto typedData = reinterpret_cast(decodedData); + size_t baseIdx = i * numChannels; + for (int c = 0; c < numChannels; ++c) { + // TODO: Update this when the final QOI data format is decided. + // https://github.com/phoboslab/qoi/issues/37 + if (c == alphaChannelIndex) { + resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; + } else { + resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); + } + } + }, priority); + + // TODO: Update this when the final QOI data format is decided. + // https://github.com/phoboslab/qoi/issues/37 + resultData.hasPremultipliedAlpha = false; + + co_return result; +} + +TEV_NAMESPACE_END diff --git a/src/imageio/QoiImageSaver.cpp b/src/imageio/QoiImageSaver.cpp new file mode 100644 index 000000000..28f6cc3c0 --- /dev/null +++ b/src/imageio/QoiImageSaver.cpp @@ -0,0 +1,35 @@ +// This file was developed by Tiago Chaves & Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#include + +#include + +#include +#include + +using namespace filesystem; +using namespace nanogui; +using namespace std; + +TEV_NAMESPACE_BEGIN + +void QoiImageSaver::save(ostream& oStream, const path&, const vector& data, const Vector2i& imageSize, int nChannels) const { + // The QOI image format expects nChannels to be either 3 for RGB data or 4 for RGBA. + if (nChannels != 3 && nChannels != 4) { + throw invalid_argument{tfm::format("Invalid number of channels %d.", nChannels)}; + } + + int sizeInBytes = 0; + void *encodedData = qoi_encode(data.data(), imageSize.x(), imageSize.y(), nChannels, &sizeInBytes); + + ScopeGuard encodedDataGuard{[encodedData] { free(encodedData); }}; + + if (!encodedData) { + throw invalid_argument{"Failed to encode data into the QOI format."}; + } + + oStream.write(reinterpret_cast(encodedData), sizeInBytes); +} + +TEV_NAMESPACE_END diff --git a/src/imageio/QoiImplementation.c b/src/imageio/QoiImplementation.c new file mode 100644 index 000000000..89ed691d8 --- /dev/null +++ b/src/imageio/QoiImplementation.c @@ -0,0 +1,11 @@ +// This file was developed by Tiago Chaves & Thomas Müller . +// It is published under the BSD 3-Clause License within the LICENSE file. + +#define QOI_NO_STDIO +#define QOI_IMPLEMENTATION +#include + +// This is kept in a separate file to make sure that qoi.h is compiled as C99 +// code instead of C++ as converting 'void*' to 'char*' would require casting. +// We define QOI_NO_STDIO because we are using qoi_decode/qoi_encode directly, +// hence, we do not need qoi_read/qoi_write to be include. diff --git a/src/imageio/StbiLdrImageSaver.cpp b/src/imageio/StbiLdrImageSaver.cpp index bb5adb75b..6c7b2b096 100644 --- a/src/imageio/StbiLdrImageSaver.cpp +++ b/src/imageio/StbiLdrImageSaver.cpp @@ -15,7 +15,7 @@ using namespace std; TEV_NAMESPACE_BEGIN -void StbiLdrImageSaver::save(ostream& iStream, const path& path, const vector& data, const Vector2i& imageSize, int nChannels) const { +void StbiLdrImageSaver::save(ostream& oStream, const path& path, const vector& data, const Vector2i& imageSize, int nChannels) const { static const auto stbiOStreamWrite = [](void* context, void* data, int size) { reinterpret_cast(context)->write(reinterpret_cast(data), size); }; @@ -23,13 +23,13 @@ void StbiLdrImageSaver::save(ostream& iStream, const path& path, const vector Date: Mon, 29 Nov 2021 09:23:49 -0300 Subject: [PATCH 75/83] Update QOI data format --- dependencies/qoi | 2 +- include/tev/imageio/QoiImageSaver.h | 2 -- src/imageio/QoiImageLoader.cpp | 55 ++++++++++++++++++++--------- src/imageio/QoiImageSaver.cpp | 11 ++++-- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/dependencies/qoi b/dependencies/qoi index a902b57ed..fda5167d7 160000 --- a/dependencies/qoi +++ b/dependencies/qoi @@ -1 +1 @@ -Subproject commit a902b57ede3a54f6ea01a7161aa88e6ed3d98649 +Subproject commit fda5167d76d05de67b821c787824c8d177fd22d8 diff --git a/include/tev/imageio/QoiImageSaver.h b/include/tev/imageio/QoiImageSaver.h index 72b9ae3f9..fdad21bb4 100644 --- a/include/tev/imageio/QoiImageSaver.h +++ b/include/tev/imageio/QoiImageSaver.h @@ -14,8 +14,6 @@ class QoiImageSaver : public TypedImageSaver { void save(std::ostream& oStream, const filesystem::path& path, const std::vector& data, const nanogui::Vector2i& imageSize, int nChannels) const override; bool hasPremultipliedAlpha() const override { - // TODO: Update this when the final QOI data format is decided. - // https://github.com/phoboslab/qoi/issues/37 return false; } diff --git a/src/imageio/QoiImageLoader.cpp b/src/imageio/QoiImageLoader.cpp index 5892afe50..be0639b72 100644 --- a/src/imageio/QoiImageLoader.cpp +++ b/src/imageio/QoiImageLoader.cpp @@ -4,6 +4,7 @@ #include #include +#define QOI_NO_STDIO #include using namespace filesystem; @@ -42,13 +43,8 @@ Task> QoiImageLoader::load(istream& iStream, const path&, cons vector data(dataSize); iStream.read(data.data(), dataSize); - // FIXME: we assume numChannels = 4 as this information is not stored in the format. - // TODO: Update this when the final QOI data format is decided. - // https://github.com/phoboslab/qoi/issues/37 - int numChannels = 4; - Vector2i size; - - void* decodedData = qoi_decode(data.data(), static_cast(dataSize), &size.x(), &size.y(), numChannels); + qoi_desc desc; + void* decodedData = qoi_decode(data.data(), static_cast(dataSize), &desc, 0); ScopeGuard decodedDataGuard{[decodedData] { free(decodedData); }}; @@ -56,32 +52,57 @@ Task> QoiImageLoader::load(istream& iStream, const path&, cons throw invalid_argument{"Failed to decode data from the QOI format."}; } + Vector2i size{static_cast(desc.width), static_cast(desc.height)}; auto numPixels = (size_t)size.x() * size.y(); if (numPixels == 0) { throw invalid_argument{"Image has zero pixels."}; } + int numChannels = static_cast(desc.channels); + if (numChannels != 4 && numChannels != 3) { + throw invalid_argument{tfm::format("Invalid number of channels %d.", numChannels)}; + } + resultData.channels = makeNChannels(numChannels, size); - int alphaChannelIndex = 3; + resultData.hasPremultipliedAlpha = false; + + // QOI uses a bitmap 0000rgba for 'colorspace', where a bit 1 indicates linear, + // however, it is purely informative (meaning it has no effect in en/decoding). + // Thus, we interpret the default 0x0 value to mean: sRGB encoded RGB channels + // with linear encoded alpha channel: + bool isSRGBChannel[4] = {true, true, true, false}; + switch (desc.colorspace) { + case 0x0: // case QOI_SRGB: + case QOI_SRGB_LINEAR_ALPHA: + break; + case QOI_LINEAR: + isSRGBChannel[0] = false; + isSRGBChannel[1] = false; + isSRGBChannel[2] = false; + break; + default: + // FIXME: should we handle "per-channel" encoding information or just the two cases above? + // Another option is assuming all values except for QOI_LINEAR mean QOI_SRGB_LINEAR_ALPHA. + // throw invalid_argument{tfm::format("Unsupported QOI colorspace: %X", desc.colorspace)}; + isSRGBChannel[0] = (desc.colorspace & 0x8) == 0x0; // R channel => 0000rgba & 1000 = r000 + isSRGBChannel[1] = (desc.colorspace & 0x4) == 0x0; // G channel => 0000rgba & 0100 = 0g00 + isSRGBChannel[2] = (desc.colorspace & 0x2) == 0x0; // B channel => 0000rgba & 0010 = 00b0 + isSRGBChannel[3] = (desc.colorspace & 0x1) == 0x0; // A channel => 0000rgba & 0001 = 000a + break; + } co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { auto typedData = reinterpret_cast(decodedData); size_t baseIdx = i * numChannels; for (int c = 0; c < numChannels; ++c) { - // TODO: Update this when the final QOI data format is decided. - // https://github.com/phoboslab/qoi/issues/37 - if (c == alphaChannelIndex) { - resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; - } else { + if (isSRGBChannel[c]) { resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); + } else { + resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; } } }, priority); - // TODO: Update this when the final QOI data format is decided. - // https://github.com/phoboslab/qoi/issues/37 - resultData.hasPremultipliedAlpha = false; - co_return result; } diff --git a/src/imageio/QoiImageSaver.cpp b/src/imageio/QoiImageSaver.cpp index 28f6cc3c0..959d1b895 100644 --- a/src/imageio/QoiImageSaver.cpp +++ b/src/imageio/QoiImageSaver.cpp @@ -3,6 +3,7 @@ #include +#define QOI_NO_STDIO #include #include @@ -16,12 +17,18 @@ TEV_NAMESPACE_BEGIN void QoiImageSaver::save(ostream& oStream, const path&, const vector& data, const Vector2i& imageSize, int nChannels) const { // The QOI image format expects nChannels to be either 3 for RGB data or 4 for RGBA. - if (nChannels != 3 && nChannels != 4) { + if (nChannels != 4 && nChannels != 3) { throw invalid_argument{tfm::format("Invalid number of channels %d.", nChannels)}; } + const qoi_desc desc{ + .width = static_cast(imageSize.x()), + .height = static_cast(imageSize.y()), + .channels = static_cast(nChannels), + .colorspace = QOI_SRGB_LINEAR_ALPHA, + }; int sizeInBytes = 0; - void *encodedData = qoi_encode(data.data(), imageSize.x(), imageSize.y(), nChannels, &sizeInBytes); + void *encodedData = qoi_encode(data.data(), &desc, &sizeInBytes); ScopeGuard encodedDataGuard{[encodedData] { free(encodedData); }}; From f9f3829f2f39e7dc25b2c80643447c2bf2548110 Mon Sep 17 00:00:00 2001 From: Tiago Chaves Date: Sun, 19 Dec 2021 21:36:06 -0300 Subject: [PATCH 76/83] Update QOI to its final specification --- CMakeLists.txt | 2 +- dependencies/qoi | 2 +- src/imageio/QoiImageLoader.cpp | 54 ++++++++++++--------------------- src/imageio/QoiImageSaver.cpp | 2 +- src/imageio/QoiImplementation.c | 11 ------- 5 files changed, 23 insertions(+), 48 deletions(-) delete mode 100644 src/imageio/QoiImplementation.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e52e5bd6..5409ad578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,7 +149,7 @@ set(TEV_SOURCES include/tev/imageio/ImageLoader.h src/imageio/ImageLoader.cpp include/tev/imageio/ImageSaver.h src/imageio/ImageSaver.cpp include/tev/imageio/PfmImageLoader.h src/imageio/PfmImageLoader.cpp - include/tev/imageio/QoiImageLoader.h src/imageio/QoiImageLoader.cpp src/imageio/QoiImplementation.c + include/tev/imageio/QoiImageLoader.h src/imageio/QoiImageLoader.cpp include/tev/imageio/QoiImageSaver.h src/imageio/QoiImageSaver.cpp include/tev/imageio/StbiHdrImageSaver.h src/imageio/StbiHdrImageSaver.cpp include/tev/imageio/StbiImageLoader.h src/imageio/StbiImageLoader.cpp diff --git a/dependencies/qoi b/dependencies/qoi index fda5167d7..4bc071df7 160000 --- a/dependencies/qoi +++ b/dependencies/qoi @@ -1 +1 @@ -Subproject commit fda5167d76d05de67b821c787824c8d177fd22d8 +Subproject commit 4bc071df7811c9c42e4a98aca13ac3dd0c98a576 diff --git a/src/imageio/QoiImageLoader.cpp b/src/imageio/QoiImageLoader.cpp index be0639b72..fd295b938 100644 --- a/src/imageio/QoiImageLoader.cpp +++ b/src/imageio/QoiImageLoader.cpp @@ -5,6 +5,7 @@ #include #define QOI_NO_STDIO +#define QOI_IMPLEMENTATION #include using namespace filesystem; @@ -66,42 +67,27 @@ Task> QoiImageLoader::load(istream& iStream, const path&, cons resultData.channels = makeNChannels(numChannels, size); resultData.hasPremultipliedAlpha = false; - // QOI uses a bitmap 0000rgba for 'colorspace', where a bit 1 indicates linear, - // however, it is purely informative (meaning it has no effect in en/decoding). - // Thus, we interpret the default 0x0 value to mean: sRGB encoded RGB channels - // with linear encoded alpha channel: - bool isSRGBChannel[4] = {true, true, true, false}; - switch (desc.colorspace) { - case 0x0: // case QOI_SRGB: - case QOI_SRGB_LINEAR_ALPHA: - break; - case QOI_LINEAR: - isSRGBChannel[0] = false; - isSRGBChannel[1] = false; - isSRGBChannel[2] = false; - break; - default: - // FIXME: should we handle "per-channel" encoding information or just the two cases above? - // Another option is assuming all values except for QOI_LINEAR mean QOI_SRGB_LINEAR_ALPHA. - // throw invalid_argument{tfm::format("Unsupported QOI colorspace: %X", desc.colorspace)}; - isSRGBChannel[0] = (desc.colorspace & 0x8) == 0x0; // R channel => 0000rgba & 1000 = r000 - isSRGBChannel[1] = (desc.colorspace & 0x4) == 0x0; // G channel => 0000rgba & 0100 = 0g00 - isSRGBChannel[2] = (desc.colorspace & 0x2) == 0x0; // B channel => 0000rgba & 0010 = 00b0 - isSRGBChannel[3] = (desc.colorspace & 0x1) == 0x0; // A channel => 0000rgba & 0001 = 000a - break; - } - - co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { - auto typedData = reinterpret_cast(decodedData); - size_t baseIdx = i * numChannels; - for (int c = 0; c < numChannels; ++c) { - if (isSRGBChannel[c]) { - resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); - } else { + if (desc.colorspace == QOI_LINEAR) { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { + auto typedData = reinterpret_cast(decodedData); + size_t baseIdx = i * numChannels; + for (int c = 0; c < numChannels; ++c) { resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; } - } - }, priority); + }, priority); + } else { + co_await gThreadPool->parallelForAsync(0, numPixels, [&](size_t i) { + auto typedData = reinterpret_cast(decodedData); + size_t baseIdx = i * numChannels; + for (int c = 0; c < numChannels; ++c) { + if (c == 3) { + resultData.channels[c].at(i) = (typedData[baseIdx + c]) / 255.0f; + } else { + resultData.channels[c].at(i) = toLinear((typedData[baseIdx + c]) / 255.0f); + } + } + }, priority); + } co_return result; } diff --git a/src/imageio/QoiImageSaver.cpp b/src/imageio/QoiImageSaver.cpp index 959d1b895..bf1b66860 100644 --- a/src/imageio/QoiImageSaver.cpp +++ b/src/imageio/QoiImageSaver.cpp @@ -25,7 +25,7 @@ void QoiImageSaver::save(ostream& oStream, const path&, const vector& data .width = static_cast(imageSize.x()), .height = static_cast(imageSize.y()), .channels = static_cast(nChannels), - .colorspace = QOI_SRGB_LINEAR_ALPHA, + .colorspace = QOI_SRGB, }; int sizeInBytes = 0; void *encodedData = qoi_encode(data.data(), &desc, &sizeInBytes); diff --git a/src/imageio/QoiImplementation.c b/src/imageio/QoiImplementation.c deleted file mode 100644 index 89ed691d8..000000000 --- a/src/imageio/QoiImplementation.c +++ /dev/null @@ -1,11 +0,0 @@ -// This file was developed by Tiago Chaves & Thomas Müller . -// It is published under the BSD 3-Clause License within the LICENSE file. - -#define QOI_NO_STDIO -#define QOI_IMPLEMENTATION -#include - -// This is kept in a separate file to make sure that qoi.h is compiled as C99 -// code instead of C++ as converting 'void*' to 'char*' would require casting. -// We define QOI_NO_STDIO because we are using qoi_decode/qoi_encode directly, -// hence, we do not need qoi_read/qoi_write to be include. From beed43db37abb36a393e85d48578f4cf5541be50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Mon, 20 Dec 2021 07:30:43 +0000 Subject: [PATCH 77/83] Minor reformatting --- src/ImageCanvas.cpp | 6 ++---- src/ImageViewer.cpp | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 00b95cd4b..4f2f9e3f2 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -531,15 +531,13 @@ void ImageCanvas::saveImage(const path& path) const { if (hdrSaver) { hdrSaver->save( f, path, - getHdrImageData(!saver->hasPremultipliedAlpha(), - std::numeric_limits::max()), + getHdrImageData(!saver->hasPremultipliedAlpha(), std::numeric_limits::max()), imageSize, 4 ); } else if (ldrSaver) { ldrSaver->save( f, path, - getLdrImageData(!saver->hasPremultipliedAlpha(), - std::numeric_limits::max()), + getLdrImageData(!saver->hasPremultipliedAlpha(), std::numeric_limits::max()), imageSize, 4 ); } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index a8ef59ae7..d419984ca 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -1560,8 +1560,8 @@ void ImageViewer::openImageDialog() { {"pnm", "Portable AnyMap image"}, {"ppm", "Portable PixMap image"}, {"psd", "PSD image"}, - {"tga", "Truevision TGA image"}, {"qoi", "Quite OK Image format"}, + {"tga", "Truevision TGA image"}, }, false, true); for (size_t i = 0; i < paths.size(); ++i) { @@ -1587,8 +1587,8 @@ void ImageViewer::saveImageDialog() { {"jpg", "JPEG image"}, {"jpeg", "JPEG image"}, {"png", "Portable Network Graphics image"}, - {"tga", "Truevision TGA image"}, {"qoi", "Quite OK Image format"}, + {"tga", "Truevision TGA image"}, }, true)); if (path.empty()) { From f667125bc7afaf2b622836b4d82695f57a009439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Mon, 20 Dec 2021 07:30:49 +0000 Subject: [PATCH 78/83] Credits --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45d7e9d9f..221b7088c 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ __New__: __tev__ can display true HDR on Apple extended dynamic range (EDR) and While the predominantly supported file format is OpenEXR certain other types of images can also be loaded. The following file formats are currently supported: - __EXR__ (via [OpenEXR](https://github.com/wjakob/openexr)) - __PFM__ (compatible with [Netbpm](http://www.pauldebevec.com/Research/HDR/PFM/)) +- __QOI__ (via [qoi](https://github.com/phoboslab/qoi). Shoutout to [Tiago Chaves](https://github.com/laurelkeys) for adding support!) - __DDS__ (via [DirectXTex](https://github.com/microsoft/DirectXTex); Windows only. Shoutout to [Craig Kolb](https://github.com/cek) for adding support!) - Supports BC1-BC7 compressed formats. - Low-dynamic-range (LDR) images are "promoted" to HDR through the reverse sRGB transformation. - __HDR__, BMP, GIF, JPEG, PIC, PNG, PNM, PSD, TGA (via [stb_image](https://github.com/wjakob/nanovg/blob/master/src/stb_image.h)) - stb_image only supports [subsets](https://github.com/wjakob/nanovg/blob/master/src/stb_image.h#L23) of each of the aforementioned file formats. - Low-dynamic-range (LDR) images are "promoted" to HDR through the reverse sRGB transformation. -- __QOI__ (via [qoi](https://github.com/phoboslab/qoi)) ## Screenshot From f6eeed278a7a0bc478028f0a7b986f3d223c60f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Mon, 20 Dec 2021 18:17:37 +0000 Subject: [PATCH 79/83] Reduce startup verbosity --- src/main.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9aa2497db..e2fcad887 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -319,8 +319,6 @@ int mainFunc(const vector& arguments) { Imf::setGlobalThreadCount(thread::hardware_concurrency()); - tlog::info() << "Loading window..."; - shared_ptr imagesLoader = make_shared(); atomic shallShutdown{false}; From db1b5936f8baa624a9f7117048370f3a67a8ebc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Mon, 20 Dec 2021 18:18:05 +0000 Subject: [PATCH 80/83] Exception-safe shutdown of nanogui and background threads --- src/main.cpp | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index e2fcad887..7c9b7b4ae 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -384,6 +384,20 @@ int mainFunc(const vector& arguments) { } }}; + ScopeGuard backgroundThreadShutdownGuard{[&]() { + shallShutdown = true; + + if (ipcThread.joinable()) { + ipcThread.join(); + } + + // stdinThread should not be joinable, since it has been + // detached earlier. But better to be safe than sorry. + if (stdinThread.joinable()) { + stdinThread.join(); + } + }}; + // Load images passed via command line in the background prior to // creating our main application such that they are not stalled // by the potentially slow initialization of opengl / glfw. @@ -400,6 +414,15 @@ int mainFunc(const vector& arguments) { // Init nanogui application nanogui::init(); + ScopeGuard nanoguiShutdownGuard{[&]() { + // On some linux distributions glfwTerminate() (which is called by + // nanogui::shutdown()) causes segfaults. Since we are done with our + // program here anyways, let's let the OS clean up after us. +#if defined(__APPLE__) or defined(_WIN32) + nanogui::shutdown(); +#endif + }}; + #ifdef __APPLE__ if (!imageFiles) { // If we didn't get any command line arguments for files to open, @@ -446,23 +469,6 @@ int mainFunc(const vector& arguments) { // This makes an idling tev surprisingly energy-efficient. :) nanogui::mainloop(250); - shallShutdown = true; - - // On some linux distributions glfwTerminate() (which is called by - // nanogui::shutdown()) causes segfaults. Since we are done with our - // program here anyways, let's let the OS clean up after us. -#if defined(__APPLE__) or defined(_WIN32) - nanogui::shutdown(); -#endif - - if (ipcThread.joinable()) { - ipcThread.join(); - } - - if (stdinThread.joinable()) { - stdinThread.join(); - } - return 0; } From b2fa16b6cd3463f622da28c816e2111dcfb25350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller?= Date: Mon, 20 Dec 2021 18:39:56 +0000 Subject: [PATCH 81/83] Hold Alt instead of Shift to show reference (& hold Shift+Ctrl instead of Alt to show raw bytes) --- src/HelpWindow.cpp | 4 ++-- src/ImageCanvas.cpp | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/HelpWindow.cpp b/src/HelpWindow.cpp index 8355e6c8b..57f928cca 100644 --- a/src/HelpWindow.cpp +++ b/src/HelpWindow.cpp @@ -89,13 +89,13 @@ HelpWindow::HelpWindow(Widget *parent, bool supportsHdr, function closeC addRow(imageSelection, "E / Shift+E", "Increase / Decrease Exposure by 0.5"); addRow(imageSelection, "O / Shift+O", "Increase / Decrease Offset by 0.1"); - addRow(imageSelection, ALT + " (hold)", "Display raw bytes on pixels when zoomed-in"); + addRow(imageSelection, "Shift+Ctrl (hold)", "Display raw bytes on pixels when zoomed-in"); new Label{shortcuts, "Reference Options", "sans-bold", 18}; auto referenceSelection = new Widget{shortcuts}; referenceSelection->set_layout(new BoxLayout{Orientation::Vertical, Alignment::Fill, 0, 0}); - addRow(referenceSelection, "Shift (hold)", "View currently selected Reference"); + addRow(referenceSelection, ALT + " (hold)", "View currently selected Reference"); addRow(referenceSelection, "Shift+Left Click or Right Click", "Select Hovered Image as Reference"); addRow(referenceSelection, "Shift+1…9", "Select N-th Image as Reference"); addRow(referenceSelection, "Shift+Down or Shift+S / Shift+Up or Shift+W", "Select Next / Previous Image as Reference"); diff --git a/src/ImageCanvas.cpp b/src/ImageCanvas.cpp index 4f2f9e3f2..5b8df537b 100644 --- a/src/ImageCanvas.cpp +++ b/src/ImageCanvas.cpp @@ -49,7 +49,9 @@ bool ImageCanvas::scroll_event(const Vector2i& p, const Vector2f& rel) { void ImageCanvas::draw_contents() { auto* glfwWindow = screen()->glfw_window(); - Image* image = (mReference && glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT)) ? mReference.get() : mImage.get(); + bool altHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_ALT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_ALT); + bool ctrlHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_CONTROL); + Image* image = (mReference && altHeld) ? mReference.get() : mImage.get(); if (!image) { mShader->draw( @@ -59,7 +61,7 @@ void ImageCanvas::draw_contents() { return; } - if (!mReference || glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) || image == mReference.get()) { + if (!mReference || ctrlHeld || image == mReference.get()) { mShader->draw( 2.0f * inverse(Vector2f{m_size}) / mPixelRatio, Vector2f{20.0f}, @@ -136,7 +138,9 @@ void ImageCanvas::drawPixelValuesAsText(NVGcontext* ctx) { nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); auto* glfwWindow = screen()->glfw_window(); - bool altHeld = glfwGetKey(glfwWindow, GLFW_KEY_LEFT_ALT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_ALT); + bool shiftAndControlHeld = + (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_SHIFT)) && + (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) || glfwGetKey(glfwWindow, GLFW_KEY_RIGHT_CONTROL)); Vector2i cur; vector values; @@ -151,7 +155,7 @@ void ImageCanvas::drawPixelValuesAsText(NVGcontext* ctx) { string str; Vector2f pos; - if (altHeld) { + if (shiftAndControlHeld) { float tonemappedValue = Channel::tail(channels[i]) == "A" ? values[i] : toSRGB(values[i]); unsigned char discretizedValue = (char)(tonemappedValue * 255 + 0.5f); str = tfm::format("%02X", discretizedValue); From a2d699dda8e52d4f7d563311859a7aa73ce97628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Sat, 25 Dec 2021 18:29:33 +0100 Subject: [PATCH 82/83] Make M1 compilation seamless & build universal binaries for release --- dependencies/nanogui | 2 +- scripts/create-dmg.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dependencies/nanogui b/dependencies/nanogui index 5f369b89e..2dcb01f11 160000 --- a/dependencies/nanogui +++ b/dependencies/nanogui @@ -1 +1 @@ -Subproject commit 5f369b89e0572bfcb8e2cbb24d3a3786b7841daf +Subproject commit 2dcb01f113407271114d213fd1cd57b18f890d67 diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh index 5e1851791..1973da6fc 100755 --- a/scripts/create-dmg.sh +++ b/scripts/create-dmg.sh @@ -12,6 +12,7 @@ MACOSX_DEPLOYMENT_TARGET=10.15 cmake \ -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ -DTEV_DEPLOY=1 \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ ../.. make -j cd .. From a05cbb4903dac8bf3619af6e3387cbbab6264cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Sun, 26 Dec 2021 09:17:00 +0100 Subject: [PATCH 83/83] Ignore .DS_Store files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b4431c7d9..a50eaa1d6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # Python cache __pycache__ +# macOS +.DS_Store + # Development project files *.sublime-project