diff --git a/libs/global/KisRegion.cpp b/libs/global/KisRegion.cpp index 915dfa14c3..d346a0924f 100644 --- a/libs/global/KisRegion.cpp +++ b/libs/global/KisRegion.cpp @@ -1,209 +1,406 @@ /* * Copyright (c) 2020 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KisRegion.h" #include +#include "kis_debug.h" namespace detail { struct HorizontalMergePolicy { static int col(const QRect &rc) { return rc.x(); } static int nextCol(const QRect &rc) { return rc.x() + rc.width(); } static int rowHeight(const QRect &rc) { return rc.height(); } static bool rowIsLess(const QRect &lhs, const QRect &rhs) { return lhs.y() < rhs.y(); } static bool elementIsLess(const QRect &lhs, const QRect &rhs) { return lhs.y() < rhs.y() || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); } }; struct VerticalMergePolicy { static int col(const QRect &rc) { return rc.y(); } static int nextCol(const QRect &rc) { return rc.y() + rc.height(); } static int rowHeight(const QRect &rc) { return rc.width(); } static bool rowIsLess(const QRect &lhs, const QRect &rhs) { return lhs.x() < rhs.x(); } static bool elementIsLess(const QRect &lhs, const QRect &rhs) { return lhs.x() < rhs.x() || (lhs.x() == rhs.x() && lhs.y() < rhs.y()); } }; template QVector::iterator mergeRects(QVector::iterator beginIt, QVector::iterator endIt, MergePolicy policy) { if (beginIt == endIt) return endIt; std::sort(beginIt, endIt, MergePolicy::elementIsLess); auto resultIt = beginIt; auto it = std::next(beginIt); while (it != endIt) { auto rowEnd = std::upper_bound(it, endIt, *it, MergePolicy::rowIsLess); for (auto rowIt = it; rowIt != rowEnd; ++rowIt) { if (policy.rowHeight(*resultIt) == policy.rowHeight(*rowIt) && policy.nextCol(*resultIt) == policy.col(*rowIt)) { *resultIt |= *rowIt; } else { resultIt++; *resultIt = *rowIt; } } it = rowEnd; } return std::next(resultIt); } + +struct VerticalSplitPolicy +{ + static int rowStart(const QRect &rc) { + return rc.y(); + } + static int rowEnd(const QRect &rc) { + return rc.bottom(); + } + static int rowHeight(const QRect &rc) { + return rc.height(); + } + static void setRowEnd(QRect &rc, int rowEnd) { + return rc.setBottom(rowEnd); + } + static bool rowIsLess(const QRect &lhs, const QRect &rhs) { + return lhs.y() < rhs.y(); + } + static QRect splitRectHi(const QRect &rc, int rowEnd) { + return QRect(rc.x(), rc.y(), + rc.width(), rowEnd - rc.y() + 1); + } + static QRect splitRectLo(const QRect &rc, int rowEnd) { + return QRect(rc.x(), rowEnd + 1, + rc.width(), rc.height() - (rowEnd - rc.y() + 1)); + } +}; + +struct HorizontalSplitPolicy +{ + static int rowStart(const QRect &rc) { + return rc.x(); + } + static int rowEnd(const QRect &rc) { + return rc.right(); + } + static int rowHeight(const QRect &rc) { + return rc.width(); + } + static void setRowEnd(QRect &rc, int rowEnd) { + return rc.setRight(rowEnd); + } + static bool rowIsLess(const QRect &lhs, const QRect &rhs) { + return lhs.x() < rhs.x(); + } + static QRect splitRectHi(const QRect &rc, int rowEnd) { + return QRect(rc.x(), rc.y(), + rowEnd - rc.x() + 1, rc.height()); + } + static QRect splitRectLo(const QRect &rc, int rowEnd) { + return QRect(rowEnd + 1, rc.y(), + rc.width() - (rowEnd - rc.x() + 1), rc.height()); + } +}; + + +struct VoidNoOp { + void operator()() const { }; + template + void operator()(P1 p1, Params... parameters) { + Q_UNUSED(p1); + operator()(parameters...); + } +}; + +struct MergeRectsOp +{ + MergeRectsOp(QVector &source, QVector &destination) + : m_source(source), + m_destination(destination) + { + } + + void operator()() { + m_destination.append(std::accumulate(m_source.begin(), m_source.end(), + QRect(), std::bit_or())); + m_source.clear(); + } + +private: + QVector &m_source; + QVector &m_destination; +}; + +template +void splitRects(QVector::iterator beginIt, QVector::iterator endIt, + OutIt resultIt, + QVector tempBuf[2], + int gridSize, + RowMergeOp rowMergeOp) +{ + if (beginIt == endIt) return; + + QVector &nextRowExtra = tempBuf[0]; + QVector &nextRowExtraTmp = tempBuf[1]; + + std::sort(beginIt, endIt, Policy::rowIsLess); + int rowStart = Policy::rowStart(*beginIt); + int rowEnd = rowStart + gridSize - 1; + + auto it = beginIt; + while (1) { + bool switchToNextRow = false; + + if (it == endIt) { + if (nextRowExtra.isEmpty()) { + rowMergeOp(); + break; + } else { + switchToNextRow = true; + } + } else if (Policy::rowStart(*it) > rowEnd) { + switchToNextRow = true; + } + + if (switchToNextRow) { + rowMergeOp(); + + if (!nextRowExtra.isEmpty()) { + rowStart = Policy::rowStart(nextRowExtra.first()); + rowEnd = rowStart + gridSize - 1; + + for (auto nextIt = nextRowExtra.begin(); nextIt != nextRowExtra.end(); ++nextIt) { + if (Policy::rowEnd(*nextIt) > rowEnd) { + nextRowExtraTmp.append(Policy::splitRectLo(*nextIt, rowEnd)); + *resultIt++ = Policy::splitRectHi(*nextIt, rowEnd); + } else { + *resultIt++ = *nextIt; + } + } + nextRowExtra.clear(); + std::swap(nextRowExtra, nextRowExtraTmp); + + continue; + } else { + rowStart = Policy::rowStart(*it); + rowEnd = rowStart + gridSize - 1; + } + } + + if (Policy::rowEnd(*it) > rowEnd) { + nextRowExtra.append(Policy::splitRectLo(*it, rowEnd)); + *resultIt++ = Policy::splitRectHi(*it, rowEnd); + } else { + *resultIt++ = *it; + } + + ++it; + } +} + } QVector::iterator KisRegion::mergeSparseRects(QVector::iterator beginIt, QVector::iterator endIt) { endIt = detail::mergeRects(beginIt, endIt, detail::HorizontalMergePolicy()); endIt = detail::mergeRects(beginIt, endIt, detail::VerticalMergePolicy()); return endIt; } +void KisRegion::approximateOverlappingRects(QVector &rects, int gridSize) +{ + using namespace detail; + + if (rects.isEmpty()) return; + + QVector rowsBuf; + QVector intermediate; + QVector tempBuf[2]; + + splitRects(rects.begin(), rects.end(), + std::back_inserter(rowsBuf), + tempBuf, gridSize, VoidNoOp()); + + rects.clear(); + KIS_SAFE_ASSERT_RECOVER_NOOP(tempBuf[0].isEmpty()); + KIS_SAFE_ASSERT_RECOVER_NOOP(tempBuf[1].isEmpty()); + + auto rowBegin = rowsBuf.begin(); + while (rowBegin != rowsBuf.end()) { + auto rowEnd = std::upper_bound(rowBegin, rowsBuf.end(), + QRect(rowBegin->x(), + rowBegin->y() + gridSize - 1, + 1,1), + VerticalSplitPolicy::rowIsLess); + + splitRects(rowBegin, rowEnd, + std::back_inserter(intermediate), + tempBuf, gridSize, + MergeRectsOp(intermediate, rects)); + rowBegin = rowEnd; + + KIS_SAFE_ASSERT_RECOVER_NOOP(intermediate.isEmpty()); + KIS_SAFE_ASSERT_RECOVER_NOOP(tempBuf[0].isEmpty()); + KIS_SAFE_ASSERT_RECOVER_NOOP(tempBuf[1].isEmpty()); + } +} + KisRegion::KisRegion(const QRect &rect) { m_rects << rect; } KisRegion::KisRegion(std::initializer_list rects) : m_rects(rects) { } KisRegion::KisRegion(const QVector &rects) : m_rects(rects) { mergeAllRects(); } KisRegion::KisRegion(QVector &&rects) : m_rects(rects) { mergeAllRects(); } KisRegion &KisRegion::operator=(const KisRegion &rhs) { m_rects = rhs.m_rects; return *this; } KisRegion &KisRegion::operator&=(const QRect &rect) { for (auto it = m_rects.begin(); it != m_rects.end(); /* noop */) { *it &= rect; if (it->isEmpty()) { it = m_rects.erase(it); } else { ++it; } } mergeAllRects(); return *this; } QRect KisRegion::boundingRect() const { return std::accumulate(m_rects.constBegin(), m_rects.constEnd(), QRect(), std::bit_or()); } QVector KisRegion::rects() const { return m_rects; } int KisRegion::rectCount() const { return m_rects.size(); } bool KisRegion::isEmpty() const { return boundingRect().isEmpty(); } QRegion KisRegion::toQRegion() const { // TODO: ustilize QRegion::setRects to make creation of QRegion much // faster. The only reason why we cannot use it "as is", our m_rects // do not satisfy the second setRects()'s precondition: "All rectangles with // a given top coordinate must have the same height". We can implement an // simple algorithm for cropping m_rects, and it will be much faster than // constructing QRegion iterationally. return std::accumulate(m_rects.constBegin(), m_rects.constEnd(), QRegion(), std::bit_or()); } void KisRegion::translate(int dx, int dy) { std::transform(m_rects.begin(), m_rects.end(), m_rects.begin(), [dx, dy] (const QRect &rc) { return rc.translated(dx, dy); }); } KisRegion KisRegion::translated(int dx, int dy) const { KisRegion region(*this); region.translate(dx, dy); return region; } KisRegion KisRegion::fromQRegion(const QRegion ®ion) { KisRegion result; result.m_rects.clear(); QRegion::const_iterator begin = region.begin(); while (begin != region.end()) { result.m_rects << *begin; begin++; } return result; } +KisRegion KisRegion::fromOverlappingRects(const QVector &rects, int gridSize) +{ + QVector tmp = rects; + approximateOverlappingRects(tmp, gridSize); + return KisRegion(tmp); +} + void KisRegion::mergeAllRects() { auto endIt = mergeSparseRects(m_rects.begin(), m_rects.end()); m_rects.erase(endIt, m_rects.end()); } bool operator==(const KisRegion &lhs, const KisRegion &rhs) { return lhs.m_rects == rhs.m_rects; } diff --git a/libs/global/KisRegion.h b/libs/global/KisRegion.h index 6b21f8b563..b9fa01ce64 100644 --- a/libs/global/KisRegion.h +++ b/libs/global/KisRegion.h @@ -1,98 +1,114 @@ /* * Copyright (c) 2020 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KISREGION_H #define KISREGION_H #include "kritaglobal_export.h" #include #include #include class QRegion; /** * An more efficient (and more limited) replacement for QRegion. * * Its main purpose it to be able to merge a huge set of rectangles * into a smalles set of bigger rectangles, the same thing that QRegion * is supposed to do. The main difference (and limitation) is: all the * input rects must be non-intersecting. This requirement is perfectly * fine for Krita's tiles, which do never intersect. */ class KRITAGLOBAL_EXPORT KisRegion : public boost::equality_comparable, public boost::andable { public: /** * @brief merge a set of rectanges into a smaller set of bigger rectangles * * The algorithm does two passes over the rectanges. First it tries to * merge all the rectanges horizontally, then vertically. The merge happens * in-place, that is, all the merged elements will be moved to the front * of the original range. * * The final range is defined by [beginIt, retvalIt) * * @param beginIt iterator to the beginning of the source range * @param endIt iterator to the end of the source range * @return iteration pointing past the last element of the merged range */ static QVector::iterator mergeSparseRects(QVector::iterator beginIt, QVector::iterator endIt); + + /** + * Simplifies \p rects in a way that they don't overlap anymore. The actual + * resulting area may be larger than original \p rects, but not more than + * \p gridSize in any dimension. + */ + static void approximateOverlappingRects(QVector &rects, int gridSize); + + public: KisRegion() = default; KisRegion(const KisRegion &rhs) = default; KisRegion(const QRect &rect); KisRegion(std::initializer_list rects); /** * @brief creates a region from a set of non-intersecting rectanges * @param rects rectangles that should be merged. Rectangles must not intersect. */ KisRegion(const QVector &rects); KisRegion(QVector &&rects); KisRegion& operator=(const KisRegion &rhs); friend bool operator==(const KisRegion &lhs, const KisRegion &rhs); KisRegion& operator&=(const QRect &rect); QRect boundingRect() const; QVector rects() const; int rectCount() const; bool isEmpty() const; QRegion toQRegion() const; void translate(int dx, int dy); KisRegion translated(int x, int y) const; static KisRegion fromQRegion(const QRegion ®ion); + /** + * Approximates a KisRegion from \p rects, which may overlap. The resulting + * KisRegion may be larger than the original set of rects, but it is guaranteed + * to cover it completely. + */ + static KisRegion fromOverlappingRects(const QVector &rects, int gridSize); + private: void mergeAllRects(); private: QVector m_rects; }; KRITAGLOBAL_EXPORT bool operator==(const KisRegion &lhs, const KisRegion &rhs); #endif // KISREGION_H diff --git a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp index 0265600f0a..2af45b0576 100644 --- a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp +++ b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp @@ -1,618 +1,622 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_suspend_projection_updates_stroke_strategy.h" #include #include #include #include "kis_image_signal_router.h" #include "kundo2command.h" #include "KisRunnableStrokeJobDataBase.h" #include "KisRunnableStrokeJobsInterface.h" #include "kis_paintop_utils.h" inline uint qHash(const QRect &rc) { return rc.x() + (rc.y() << 16) + (rc.width() << 8) + (rc.height() << 24); } struct KisSuspendProjectionUpdatesStrokeStrategy::Private { KisImageWSP image; bool suspend; QVector accumulatedDirtyRects; bool sanityResumingFinished = false; int updatesEpoch = 0; bool haveDisabledGUILodSync = false; SharedDataSP sharedData; void tryFetchUsedUpdatesFilter(KisImageSP image); void tryIssueRecordedDirtyRequests(KisImageSP image); class SuspendLod0Updates : public KisProjectionUpdatesFilter { struct Request { Request() : resetAnimationCache(false) {} Request(const QRect &_rect, bool _resetAnimationCache) : rect(_rect), resetAnimationCache(_resetAnimationCache) { } QRect rect; bool resetAnimationCache; }; struct FullRefreshRequest { FullRefreshRequest() {} FullRefreshRequest(const QRect &_rect, const QRect &_cropRect) : rect(_rect), cropRect(_cropRect) { } QRect rect; QRect cropRect; }; typedef QHash > UpdatesHash; typedef QHash > RefreshesHash; public: SuspendLod0Updates() { } bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) override { if (image->currentLevelOfDetail() > 0) return false; QMutexLocker l(&m_mutex); Q_FOREACH(const QRect &rc, rects) { m_requestsHash[KisNodeSP(node)].append(Request(rc, resetAnimationCache)); } return true; } bool filterRefreshGraph(KisImage *image, KisNode *node, const QVector &rects, const QRect &cropRect) override { if (image->currentLevelOfDetail() > 0) return false; QMutexLocker l(&m_mutex); Q_FOREACH(const QRect &rc, rects) { m_refreshesHash[KisNodeSP(node)].append(FullRefreshRequest(rc, cropRect)); } return true; } static inline QRect alignRect(const QRect &rc, const int step) { static const int decstep = step - 1; static const int invstep = ~decstep; int x0, y0, x1, y1; rc.getCoords(&x0, &y0, &x1, &y1); x0 &= invstep; y0 &= invstep; x1 |= decstep; y1 |= decstep; QRect result; result.setCoords(x0, y0, x1, y1); return result; } void notifyUpdates(KisImageSP image) { const int step = 64; { RefreshesHash::const_iterator it = m_refreshesHash.constBegin(); RefreshesHash::const_iterator end = m_refreshesHash.constEnd(); for (; it != end; ++it) { KisNodeSP node = it.key(); QHash> fullRefreshRequests; Q_FOREACH (const FullRefreshRequest &req, it.value()) { - fullRefreshRequests[req.cropRect] += alignRect(req.rect, step); + fullRefreshRequests[req.cropRect] += req.rect; } auto reqIt = fullRefreshRequests.begin(); for (; reqIt != fullRefreshRequests.end(); ++reqIt) { + const QVector simplifiedRects = KisRegion::fromOverlappingRects(reqIt.value(), step).rects(); + // FIXME: constness: port rPU to SP - image->refreshGraphAsync(const_cast(node.data()), reqIt.value(), reqIt.key()); + image->refreshGraphAsync(const_cast(node.data()), simplifiedRects, reqIt.key()); } } } { UpdatesHash::const_iterator it = m_requestsHash.constBegin(); UpdatesHash::const_iterator end = m_requestsHash.constEnd(); for (; it != end; ++it) { KisNodeSP node = it.key(); QVector dirtyRects; bool resetAnimationCache = false; Q_FOREACH (const Request &req, it.value()) { - dirtyRects += alignRect(req.rect, step); + dirtyRects += req.rect; resetAnimationCache |= req.resetAnimationCache; } + const QVector simplifiedRects = KisRegion::fromOverlappingRects(dirtyRects, step).rects(); + // FIXME: constness: port rPU to SP - image->requestProjectionUpdate(const_cast(node.data()), dirtyRects, resetAnimationCache); + image->requestProjectionUpdate(const_cast(node.data()), simplifiedRects, resetAnimationCache); } } } private: UpdatesHash m_requestsHash; RefreshesHash m_refreshesHash; QMutex m_mutex; }; QVector> usedFilters; struct StrokeJobCommand : public KUndo2Command { StrokeJobCommand(KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL) : m_sequentiality(sequentiality), m_exclusivity(exclusivity) {} KisStrokeJobData::Sequentiality m_sequentiality; KisStrokeJobData::Exclusivity m_exclusivity; }; struct UndoableData : public KisRunnableStrokeJobDataBase { UndoableData(StrokeJobCommand *command) : KisRunnableStrokeJobDataBase(command->m_sequentiality, command->m_exclusivity), m_command(command) { } void run() override { KIS_SAFE_ASSERT_RECOVER_RETURN(m_command); m_command->redo(); } QScopedPointer m_command; }; // Suspend job should be a barrier to ensure all // previous lodN strokes reach the GUI. Otherwise, // they will be blocked in // KisImage::notifyProjectionUpdated() struct SuspendUpdatesCommand : public StrokeJobCommand { SuspendUpdatesCommand(Private *d) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_d(d) {} void redo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(!image->currentProjectionUpdatesFilter()); KIS_SAFE_ASSERT_RECOVER_RETURN(!m_d->sharedData->installedFilterCookie); m_d->sharedData->installedFilterCookie = image->addProjectionUpdatesFilter( toQShared(new Private::SuspendLod0Updates())); } void undo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(image->currentProjectionUpdatesFilter()); KIS_SAFE_ASSERT_RECOVER_RETURN(image->currentProjectionUpdatesFilter() == m_d->sharedData->installedFilterCookie); m_d->tryFetchUsedUpdatesFilter(image); } Private *m_d; }; struct ResumeAndIssueGraphUpdatesCommand : public StrokeJobCommand { ResumeAndIssueGraphUpdatesCommand(Private *d) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_d(d) {} void redo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(image->currentProjectionUpdatesFilter()); KIS_SAFE_ASSERT_RECOVER_RETURN(image->currentProjectionUpdatesFilter() == m_d->sharedData->installedFilterCookie); image->disableUIUpdates(); m_d->tryFetchUsedUpdatesFilter(image); m_d->tryIssueRecordedDirtyRequests(image); } void undo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(!image->currentProjectionUpdatesFilter()); KIS_SAFE_ASSERT_RECOVER_RETURN(!m_d->sharedData->installedFilterCookie); m_d->sharedData->installedFilterCookie = image->addProjectionUpdatesFilter( toQShared(new Private::SuspendLod0Updates())); image->enableUIUpdates(); } Private *m_d; }; struct UploadDataToUIData : public KisRunnableStrokeJobDataBase { UploadDataToUIData(const QRect &rc, int updateEpoch, KisSuspendProjectionUpdatesStrokeStrategy *strategy) : KisRunnableStrokeJobDataBase(KisStrokeJobData::CONCURRENT), m_strategy(strategy), m_rc(rc), m_updateEpoch(updateEpoch) { } void run() override { // check if we've already started stinking... if (m_strategy->m_d->updatesEpoch > m_updateEpoch) { return; } KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->notifyProjectionUpdated(m_rc); } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; QRect m_rc; int m_updateEpoch; }; struct BlockUILodSync : public KisRunnableStrokeJobDataBase { BlockUILodSync(bool block, KisSuspendProjectionUpdatesStrokeStrategy *strategy) : KisRunnableStrokeJobDataBase(KisStrokeJobData::BARRIER), m_strategy(strategy), m_block(block) {} void run() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitRequestLodPlanesSyncBlocked(m_block); m_strategy->m_d->haveDisabledGUILodSync = m_block; } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; bool m_block; }; struct StartBatchUIUpdatesCommand : public StrokeJobCommand { StartBatchUIUpdatesCommand(KisSuspendProjectionUpdatesStrokeStrategy *strategy) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_strategy(strategy) {} void redo() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); /** * We accumulate dirty rects from all(!) epochs, because some updates of the * previous epochs might have been cancelled without doing any real work. */ const QVector totalDirtyRects = image->enableUIUpdates() + m_strategy->m_d->accumulatedDirtyRects; const QRect totalRect = image->bounds() & std::accumulate(totalDirtyRects.begin(), totalDirtyRects.end(), QRect(), std::bit_or()); m_strategy->m_d->accumulatedDirtyRects = KisPaintOpUtils::splitAndFilterDabRect(totalRect, totalDirtyRects, KritaUtils::optimalPatchSize().width()); image->signalRouter()->emitNotifyBatchUpdateStarted(); QVector jobsData; Q_FOREACH (const QRect &rc, m_strategy->m_d->accumulatedDirtyRects) { jobsData << new Private::UploadDataToUIData(rc, m_strategy->m_d->updatesEpoch, m_strategy); } m_strategy->runnableJobsInterface()->addRunnableJobs(jobsData); } void undo() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitNotifyBatchUpdateEnded(); image->disableUIUpdates(); } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; }; struct EndBatchUIUpdatesCommand : public StrokeJobCommand { EndBatchUIUpdatesCommand(KisSuspendProjectionUpdatesStrokeStrategy *strategy) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_strategy(strategy) {} void redo() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitNotifyBatchUpdateEnded(); m_strategy->m_d->sanityResumingFinished = true; m_strategy->m_d->accumulatedDirtyRects.clear(); KIS_SAFE_ASSERT_RECOVER_NOOP(m_strategy->m_d->usedFilters.isEmpty()); } void undo() override { /** * Even though this comand is the last command of the stroke is can * still be undone by suspendStrokeCallback(). It happens when a LodN * stroke is started right after the last job of resume strategy was * being executed. In such a case new stroke is placed right in front * of our resume strategy and all the resuming work is undone (mimicing * a normal suspend strategy). * * The only thing we should control here is whether the state of the * stroke is reset to default. Otherwise we'll do all the updates twice. */ KIS_SAFE_ASSERT_RECOVER_NOOP(m_strategy->m_d->usedFilters.isEmpty()); KIS_SAFE_ASSERT_RECOVER_NOOP(m_strategy->m_d->accumulatedDirtyRects.isEmpty()); m_strategy->m_d->sanityResumingFinished = false; KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitNotifyBatchUpdateStarted(); } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; }; QVector executedCommands; }; KisSuspendProjectionUpdatesStrokeStrategy::KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP image, bool suspend, SharedDataSP sharedData) : KisRunnableBasedStrokeStrategy(suspend ? QLatin1String("suspend_stroke_strategy") : QLatin1String("resume_stroke_strategy")), m_d(new Private) { m_d->image = image; m_d->suspend = suspend; m_d->sharedData = sharedData; /** * Here we add a dumb INIT job so that KisStrokesQueue would know that the * stroke has already started or not. When the queue reaches the resume * stroke and starts its execution, no Lod0 can execute anymore. So all the * new Lod0 strokes should go to the end of the queue and wrapped into * their own Suspend/Resume pair. */ enableJob(JOB_INIT, true); enableJob(JOB_DOSTROKE, true); enableJob(JOB_CANCEL, true); enableJob(JOB_SUSPEND, true, KisStrokeJobData::BARRIER); enableJob(JOB_RESUME, true, KisStrokeJobData::BARRIER); setNeedsExplicitCancel(true); } KisSuspendProjectionUpdatesStrokeStrategy::~KisSuspendProjectionUpdatesStrokeStrategy() { qDeleteAll(m_d->executedCommands); } void KisSuspendProjectionUpdatesStrokeStrategy::initStrokeCallback() { QVector jobs; if (m_d->suspend) { jobs << new Private::UndoableData(new Private::SuspendUpdatesCommand(m_d.data())); } else { jobs << new Private::UndoableData(new Private::ResumeAndIssueGraphUpdatesCommand(m_d.data())); jobs << new Private::BlockUILodSync(true, this); jobs << new Private::UndoableData(new Private::StartBatchUIUpdatesCommand(this)); jobs << new Private::UndoableData(new Private::EndBatchUIUpdatesCommand(this)); jobs << new Private::BlockUILodSync(false, this); } runnableJobsInterface()->addRunnableJobs(jobs); } /** * When the Lod0 stroke is being recalculated in the background we * should block all the updates it issues to avoid user distraction. * The result of the final stroke should be shown to the user in the * very end when everything is fully ready. Ideally the use should not * notice that the image has changed :) * * (Don't mix this process with suspend/resume capabilities of a * single stroke. That is a different system!) * * The process of the Lod0 regeneration consists of the following: * * 1) Suspend stroke executes. It sets a special updates filter on the * image. The filter blocks all the updates and saves them in an * internal structure to be emitted in the future. * * 2) Lod0 strokes are being recalculated. All their updates are * blocked and saved in the filter. * * 3) Resume stroke starts: * * 3.1) First it disables emitting of sigImageUpdated() so the gui * will not get any update notifications. * * 3.2) Then it enables updates themselves. * * 3.3) Initiates all the updates that were requested by the Lod0 * stroke. The node graph is regenerated, but the GUI does * not get this change. * * 3.4) Special barrier job waits for all the updates to finish * and, when they are done, enables GUI notifications again. * * 3.5) In a multithreaded way emits the GUI notifications for the * entire image. Multithreaded way is used to conform the * double-stage update principle of KisCanvas2. */ void KisSuspendProjectionUpdatesStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { KisRunnableStrokeJobDataBase *runnable = dynamic_cast(data); if (runnable) { runnable->run(); if (Private::UndoableData *undoable = dynamic_cast(data)) { Private::StrokeJobCommand *command = undoable->m_command.take(); m_d->executedCommands.append(command); } } } QList KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP /*image*/) { return QList(); } QList KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP /*_image*/) { return QList(); } KisSuspendProjectionUpdatesStrokeStrategy::SharedDataSP KisSuspendProjectionUpdatesStrokeStrategy::createSharedData() { return toQShared(new SharedData()); } void KisSuspendProjectionUpdatesStrokeStrategy::Private::tryFetchUsedUpdatesFilter(KisImageSP image) { if (!this->sharedData->installedFilterCookie) return; KisProjectionUpdatesFilterSP filter = image->removeProjectionUpdatesFilter(image->currentProjectionUpdatesFilter()); this->sharedData->installedFilterCookie = KisProjectionUpdatesFilterCookie(); KIS_SAFE_ASSERT_RECOVER_RETURN(filter); QSharedPointer localFilter = filter.dynamicCast(); KIS_SAFE_ASSERT_RECOVER_RETURN(localFilter); this->usedFilters.append(localFilter); } void KisSuspendProjectionUpdatesStrokeStrategy::Private::tryIssueRecordedDirtyRequests(KisImageSP image) { Q_FOREACH (QSharedPointer filter, usedFilters) { filter->notifyUpdates(image.data()); } usedFilters.clear(); } void KisSuspendProjectionUpdatesStrokeStrategy::cancelStrokeCallback() { KisImageSP image = m_d->image.toStrongRef(); if (!image) { return; } for (auto it = m_d->executedCommands.rbegin(); it != m_d->executedCommands.rend(); ++it) { (*it)->undo(); } m_d->tryFetchUsedUpdatesFilter(image); if (m_d->haveDisabledGUILodSync) { image->signalRouter()->emitRequestLodPlanesSyncBlocked(false); } /** * We shouldn't emit any ad-hoc updates when cancelling the * stroke. It generates weird temporary holes on the canvas, * making the user feel awful, thinking his image got * corrupted. We will just emit a common refreshGraphAsync() that * will do all the work in a beautiful way */ if (!m_d->suspend) { // FIXME: optimize image->refreshGraphAsync(); } } void KisSuspendProjectionUpdatesStrokeStrategy::suspendStrokeCallback() { /** * The resume stroke can be suspended even when all its jobs are completed. * In such a case, we should just ensure that all the internal state is reset * to default. */ KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->suspend || !m_d->sanityResumingFinished || (m_d->sanityResumingFinished && m_d->usedFilters.isEmpty() && m_d->accumulatedDirtyRects.isEmpty())); for (auto it = m_d->executedCommands.rbegin(); it != m_d->executedCommands.rend(); ++it) { (*it)->undo(); } // reset all the issued updates m_d->updatesEpoch++; } void KisSuspendProjectionUpdatesStrokeStrategy::resumeStrokeCallback() { QVector jobs; Q_FOREACH (Private::StrokeJobCommand *command, m_d->executedCommands) { jobs << new Private::UndoableData(command); } m_d->executedCommands.clear(); runnableJobsInterface()->addRunnableJobs(jobs); } diff --git a/libs/image/tiles3/tests/kis_tiled_data_manager_test.cpp b/libs/image/tiles3/tests/kis_tiled_data_manager_test.cpp index fce68797ae..5578513554 100644 --- a/libs/image/tiles3/tests/kis_tiled_data_manager_test.cpp +++ b/libs/image/tiles3/tests/kis_tiled_data_manager_test.cpp @@ -1,1051 +1,1126 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tiled_data_manager_test.h" #include #include "tiles3/kis_tiled_data_manager.h" #include "tiles_test_utils.h" #include "config-limit-long-tests.h" bool KisTiledDataManagerTest::checkHole(quint8* buffer, quint8 holeColor, QRect holeRect, quint8 backgroundColor, QRect backgroundRect) { for(qint32 y = backgroundRect.y(); y <= backgroundRect.bottom(); y++) { for(qint32 x = backgroundRect.x(); x <= backgroundRect.right(); x++) { quint8 expectedColor = holeRect.contains(x,y) ? holeColor : backgroundColor; if(*buffer != expectedColor) { qDebug() << "Expected" << expectedColor << "but found" << *buffer; return false; } buffer++; } } return true; } bool KisTiledDataManagerTest::checkTilesShared(KisTiledDataManager *srcDM, KisTiledDataManager *dstDM, bool takeOldSrc, bool takeOldDst, QRect tilesRect) { for(qint32 row = tilesRect.y(); row <= tilesRect.bottom(); row++) { for(qint32 col = tilesRect.x(); col <= tilesRect.right(); col++) { KisTileSP srcTile = takeOldSrc ? srcDM->getOldTile(col, row) : srcDM->getTile(col, row, false); KisTileSP dstTile = takeOldDst ? dstDM->getOldTile(col, row) : dstDM->getTile(col, row, false); if(srcTile->tileData() != dstTile->tileData()) { qDebug() << "Expected tile data (" << col << row << ")" << srcTile->extent() << srcTile->tileData() << "but found" << dstTile->tileData(); qDebug() << "Expected" << srcTile->data()[0] << "but found" << dstTile->data()[0]; return false; } } } return true; } bool KisTiledDataManagerTest::checkTilesNotShared(KisTiledDataManager *srcDM, KisTiledDataManager *dstDM, bool takeOldSrc, bool takeOldDst, QRect tilesRect) { for(qint32 row = tilesRect.y(); row <= tilesRect.bottom(); row++) { for(qint32 col = tilesRect.x(); col <= tilesRect.right(); col++) { KisTileSP srcTile = takeOldSrc ? srcDM->getOldTile(col, row) : srcDM->getTile(col, row, false); KisTileSP dstTile = takeOldDst ? dstDM->getOldTile(col, row) : dstDM->getTile(col, row, false); if(srcTile->tileData() == dstTile->tileData()) { qDebug() << "Expected tiles not be shared:"<< srcTile->extent(); return false; } } } return true; } void KisTiledDataManagerTest::testUndoingNewTiles() { // "growing extent bug" const QRect nullRect; quint8 defaultPixel = 0; KisTiledDataManager srcDM(1, &defaultPixel); KisTileSP emptyTile = srcDM.getTile(0, 0, false); QCOMPARE(srcDM.extent(), nullRect); KisMementoSP memento0 = srcDM.getMemento(); KisTileSP createdTile = srcDM.getTile(0, 0, true); srcDM.commit(); QCOMPARE(srcDM.extent(), QRect(0,0,64,64)); srcDM.rollback(memento0); QCOMPARE(srcDM.extent(), nullRect); } void KisTiledDataManagerTest::testPurgedAndEmptyTransactions() { quint8 defaultPixel = 0; KisTiledDataManager srcDM(1, &defaultPixel); quint8 oddPixel1 = 128; QRect rect(0,0,512,512); QRect clearRect1(50,50,100,100); QRect clearRect2(150,50,100,100); quint8 *buffer = new quint8[rect.width()*rect.height()]; // purged transaction KisMementoSP memento0 = srcDM.getMemento(); srcDM.clear(clearRect1, &oddPixel1); srcDM.purgeHistory(memento0); memento0 = 0; srcDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, clearRect1, defaultPixel, rect)); // one more purged transaction KisMementoSP memento1 = srcDM.getMemento(); srcDM.clear(clearRect2, &oddPixel1); srcDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, clearRect1 | clearRect2, defaultPixel, rect)); srcDM.purgeHistory(memento1); memento1 = 0; srcDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, clearRect1 | clearRect2, defaultPixel, rect)); // empty one KisMementoSP memento2 = srcDM.getMemento(); srcDM.commit(); srcDM.rollback(memento2); srcDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, clearRect1 | clearRect2, defaultPixel, rect)); // now check that everything works still KisMementoSP memento3 = srcDM.getMemento(); srcDM.setExtent(clearRect2); srcDM.commit(); srcDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, clearRect2, defaultPixel, rect)); srcDM.rollback(memento3); srcDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, clearRect1 | clearRect2, defaultPixel, rect)); } void KisTiledDataManagerTest::testUnversionedBitBlt() { quint8 defaultPixel = 0; KisTiledDataManager srcDM(1, &defaultPixel); KisTiledDataManager dstDM(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; QRect rect(0,0,512,512); QRect cloneRect(81,80,250,250); QRect tilesRect(2,2,3,3); srcDM.clear(rect, &oddPixel1); dstDM.clear(rect, &oddPixel2); dstDM.bitBlt(&srcDM, cloneRect); quint8 *buffer = new quint8[rect.width()*rect.height()]; dstDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, cloneRect, oddPixel2, rect)); delete[] buffer; // Test whether tiles became shared QVERIFY(checkTilesShared(&srcDM, &dstDM, false, false, tilesRect)); } void KisTiledDataManagerTest::testVersionedBitBlt() { quint8 defaultPixel = 0; KisTiledDataManager srcDM1(1, &defaultPixel); KisTiledDataManager srcDM2(1, &defaultPixel); KisTiledDataManager dstDM(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; quint8 oddPixel3 = 130; quint8 oddPixel4 = 131; QRect rect(0,0,512,512); QRect cloneRect(81,80,250,250); QRect tilesRect(2,2,3,3); KisMementoSP memento1 = srcDM1.getMemento(); srcDM1.clear(rect, &oddPixel1); srcDM2.clear(rect, &oddPixel2); dstDM.clear(rect, &oddPixel3); KisMementoSP memento2 = dstDM.getMemento(); dstDM.bitBlt(&srcDM1, cloneRect); QVERIFY(checkTilesShared(&srcDM1, &dstDM, false, false, tilesRect)); QVERIFY(checkTilesNotShared(&srcDM1, &srcDM1, true, false, tilesRect)); QVERIFY(checkTilesNotShared(&dstDM, &dstDM, true, false, tilesRect)); dstDM.commit(); QVERIFY(checkTilesShared(&dstDM, &dstDM, true, false, tilesRect)); KisMementoSP memento3 = srcDM2.getMemento(); srcDM2.clear(rect, &oddPixel4); KisMementoSP memento4 = dstDM.getMemento(); dstDM.bitBlt(&srcDM2, cloneRect); QVERIFY(checkTilesShared(&srcDM2, &dstDM, false, false, tilesRect)); QVERIFY(checkTilesNotShared(&srcDM2, &srcDM2, true, false, tilesRect)); QVERIFY(checkTilesNotShared(&dstDM, &dstDM, true, false, tilesRect)); dstDM.commit(); QVERIFY(checkTilesShared(&dstDM, &dstDM, true, false, tilesRect)); dstDM.rollback(memento4); QVERIFY(checkTilesShared(&srcDM1, &dstDM, false, false, tilesRect)); QVERIFY(checkTilesShared(&dstDM, &dstDM, true, false, tilesRect)); QVERIFY(checkTilesNotShared(&srcDM1, &srcDM1, true, false, tilesRect)); dstDM.rollforward(memento4); QVERIFY(checkTilesShared(&srcDM2, &dstDM, false, false, tilesRect)); QVERIFY(checkTilesShared(&dstDM, &dstDM, true, false, tilesRect)); QVERIFY(checkTilesNotShared(&srcDM1, &srcDM1, true, false, tilesRect)); } void KisTiledDataManagerTest::testBitBltOldData() { quint8 defaultPixel = 0; KisTiledDataManager srcDM(1, &defaultPixel); KisTiledDataManager dstDM(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; QRect rect(0,0,512,512); QRect cloneRect(81,80,250,250); quint8 *buffer = new quint8[rect.width()*rect.height()]; KisMementoSP memento1 = srcDM.getMemento(); srcDM.clear(rect, &oddPixel1); srcDM.commit(); dstDM.bitBltOldData(&srcDM, cloneRect); dstDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, cloneRect, defaultPixel, rect)); KisMementoSP memento2 = srcDM.getMemento(); srcDM.clear(rect, &oddPixel2); dstDM.bitBltOldData(&srcDM, cloneRect); srcDM.commit(); dstDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, cloneRect, defaultPixel, rect)); delete[] buffer; } void KisTiledDataManagerTest::testBitBltRough() { quint8 defaultPixel = 0; KisTiledDataManager srcDM(1, &defaultPixel); KisTiledDataManager dstDM(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; quint8 oddPixel3 = 130; QRect rect(0,0,512,512); QRect cloneRect(81,80,250,250); QRect actualCloneRect(64,64,320,320); QRect tilesRect(1,1,4,4); srcDM.clear(rect, &oddPixel1); dstDM.clear(rect, &oddPixel2); dstDM.bitBltRough(&srcDM, cloneRect); quint8 *buffer = new quint8[rect.width()*rect.height()]; dstDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, actualCloneRect, oddPixel2, rect)); // Test whether tiles became shared QVERIFY(checkTilesShared(&srcDM, &dstDM, false, false, tilesRect)); // check bitBltRoughOldData KisMementoSP memento1 = srcDM.getMemento(); srcDM.clear(rect, &oddPixel3); dstDM.bitBltRoughOldData(&srcDM, cloneRect); srcDM.commit(); dstDM.readBytes(buffer, rect.x(), rect.y(), rect.width(), rect.height()); QVERIFY(checkHole(buffer, oddPixel1, actualCloneRect, oddPixel2, rect)); delete[] buffer; } void KisTiledDataManagerTest::testTransactions() { quint8 defaultPixel = 0; KisTiledDataManager dm(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; quint8 oddPixel3 = 130; KisTileSP tile00; KisTileSP oldTile00; // Create a named transaction: versioning is enabled KisMementoSP memento1 = dm.getMemento(); dm.clear(0, 0, 64, 64, &oddPixel1); tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(defaultPixel, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; // Create an anonymous transaction: versioning is disabled dm.commit(); tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel1, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; dm.clear(0, 0, 64, 64, &oddPixel2); // Versioning is disabled, i said! >:) tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel2, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel2, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; // And the last round: named transaction: KisMementoSP memento2 = dm.getMemento(); dm.clear(0, 0, 64, 64, &oddPixel3); tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel3, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel2, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; } void KisTiledDataManagerTest::testPurgeHistory() { quint8 defaultPixel = 0; KisTiledDataManager dm(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; quint8 oddPixel3 = 130; quint8 oddPixel4 = 131; KisMementoSP memento1 = dm.getMemento(); dm.clear(0, 0, 64, 64, &oddPixel1); dm.commit(); KisMementoSP memento2 = dm.getMemento(); dm.clear(0, 0, 64, 64, &oddPixel2); KisTileSP tile00; KisTileSP oldTile00; tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel2, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel1, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; dm.purgeHistory(memento1); /** * Nothing nas changed in the visible state of the data manager */ tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel2, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel1, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; dm.commit(); dm.purgeHistory(memento2); /** * We've removed all the history of the device, so it * became "unversioned". * NOTE: the return value for getOldTile() when there is no * history present is a subject for change */ tile00 = dm.getTile(0, 0, false); oldTile00 = dm.getOldTile(0, 0); QVERIFY(memoryIsFilled(oddPixel2, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel2, oldTile00->data(), TILESIZE)); tile00 = oldTile00 = 0; /** * Just test we won't crash when the memento is not * present in history anymore */ KisMementoSP memento3 = dm.getMemento(); dm.clear(0, 0, 64, 64, &oddPixel3); dm.commit(); KisMementoSP memento4 = dm.getMemento(); dm.clear(0, 0, 64, 64, &oddPixel4); dm.commit(); dm.rollback(memento4); dm.purgeHistory(memento3); dm.purgeHistory(memento4); } void KisTiledDataManagerTest::testUndoSetDefaultPixel() { quint8 defaultPixel = 0; KisTiledDataManager dm(1, &defaultPixel); quint8 oddPixel1 = 128; quint8 oddPixel2 = 129; QRect fillRect(0,0,64,64); KisTileSP tile00; KisTileSP tile10; tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(defaultPixel, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(defaultPixel, tile10->data(), TILESIZE)); KisMementoSP memento1 = dm.getMemento(); dm.clear(fillRect, &oddPixel1); dm.commit(); tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(defaultPixel, tile10->data(), TILESIZE)); KisMementoSP memento2 = dm.getMemento(); dm.setDefaultPixel(&oddPixel2); dm.commit(); tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel2, tile10->data(), TILESIZE)); dm.rollback(memento2); tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(defaultPixel, tile10->data(), TILESIZE)); dm.rollback(memento1); tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(defaultPixel, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(defaultPixel, tile10->data(), TILESIZE)); dm.rollforward(memento1); tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(defaultPixel, tile10->data(), TILESIZE)); dm.rollforward(memento2); tile00 = dm.getTile(0, 0, false); tile10 = dm.getTile(1, 0, false); QVERIFY(memoryIsFilled(oddPixel1, tile00->data(), TILESIZE)); QVERIFY(memoryIsFilled(oddPixel2, tile10->data(), TILESIZE)); } //#include void KisTiledDataManagerTest::benchmarkReadOnlyTileLazy() { quint8 defaultPixel = 0; KisTiledDataManager dm(1, &defaultPixel); /* * See KisTileHashTableTraits2 for more details */ const qint32 numTilesToTest = 0x7fff; //CALLGRIND_START_INSTRUMENTATION; QBENCHMARK_ONCE { for(qint32 i = 0; i < numTilesToTest; i++) { KisTileSP tile = dm.getTile(i, i, false); } } //CALLGRIND_STOP_INSTRUMENTATION; } class KisSimpleClass : public KisShared { qint64 m_int; public: KisSimpleClass() { Q_UNUSED(m_int); } }; typedef KisSharedPtr KisSimpleClassSP; void KisTiledDataManagerTest::benchmarkSharedPointers() { const qint32 numIterations = 2 * 1000000; //CALLGRIND_START_INSTRUMENTATION; QBENCHMARK_ONCE { for(qint32 i = 0; i < numIterations; i++) { KisSimpleClassSP pointer = new KisSimpleClass; pointer = 0; } } //CALLGRIND_STOP_INSTRUMENTATION; } void KisTiledDataManagerTest::benchmarkCOWImpl() { const int pixelSize = 8; quint8 defaultPixel[pixelSize]; memset(defaultPixel, 1, pixelSize); KisTiledDataManager dm(pixelSize, defaultPixel); KisMementoSP memento1 = dm.getMemento(); /** * Imagine a regular image of 4096x2048 pixels * (64x32 tiles) */ for (int i = 0; i < 32; i++) { for (int j = 0; j < 64; j++) { KisTileSP tile = dm.getTile(j, i, true); tile->lockForWrite(); tile->unlockForWrite(); } } dm.commit(); QTest::qSleep(200); KisMementoSP memento2 = dm.getMemento(); QTest::qSleep(200); QBENCHMARK_ONCE { for (int i = 0; i < 32; i++) { for (int j = 0; j < 64; j++) { KisTileSP tile = dm.getTile(j, i, true); tile->lockForWrite(); tile->unlockForWrite(); } } } dm.commit(); } void KisTiledDataManagerTest::benchmarkCOWNoPooler() { KisTileDataStore::instance()->testingSuspendPooler(); QTest::qSleep(200); benchmarkCOWImpl(); KisTileDataStore::instance()->testingResumePooler(); QTest::qSleep(200); } void KisTiledDataManagerTest::benchmarkCOWWithPooler() { benchmarkCOWImpl(); } /******************* Stress job ***********************/ #ifdef LIMIT_LONG_TESTS #define NUM_CYCLES 10000 #else #define NUM_CYCLES 100000 #endif #define NUM_TYPES 12 #define TILE_DIMENSION 64 /** * The data manager has partial guarantees of reentrancy. That is * you can call any arbitrary number of methods concurrently as long * as their access areas do not intersect. * * Though the rule can be quite tricky -- some of the methods always * use entire image as their access area, so they cannot be called * concurrently in any circumstances. * The examples are: clear(), commit(), rollback() and etc... */ #define run_exclusive(lock, _i) for(_i = 0, (lock).lockForWrite(); _i < 1; _i++, (lock).unlock()) #define run_concurrent(lock, _i) for(_i = 0, (lock).lockForRead(); _i < 1; _i++, (lock).unlock()) //#define run_exclusive(lock, _i) while(0) //#define run_concurrent(lock, _i) while(0) class KisStressJob : public QRunnable { public: KisStressJob(KisTiledDataManager &dataManager, QRect rect, QReadWriteLock &_lock) : m_accessRect(rect), dm(dataManager), lock(_lock) { } void run() override { qsrand(QTime::currentTime().msec()); for(qint32 i = 0; i < NUM_CYCLES; i++) { qint32 type = qrand() % NUM_TYPES; qint32 t; switch(type) { case 0: run_concurrent(lock,t) { quint8 *buf; buf = new quint8[dm.pixelSize()]; memcpy(buf, dm.defaultPixel(), dm.pixelSize()); dm.setDefaultPixel(buf); delete[] buf; } break; case 1: case 2: run_concurrent(lock,t) { KisTileSP tile; tile = dm.getTile(m_accessRect.x() / TILE_DIMENSION, m_accessRect.y() / TILE_DIMENSION, false); tile->lockForRead(); tile->unlockForRead(); tile = dm.getTile(m_accessRect.x() / TILE_DIMENSION, m_accessRect.y() / TILE_DIMENSION, true); tile->lockForWrite(); tile->unlockForWrite(); tile = dm.getOldTile(m_accessRect.x() / TILE_DIMENSION, m_accessRect.y() / TILE_DIMENSION); tile->lockForRead(); tile->unlockForRead(); } break; case 3: run_concurrent(lock,t) { QRect newRect = dm.extent(); Q_UNUSED(newRect); } break; case 4: run_concurrent(lock,t) { dm.clear(m_accessRect.x(), m_accessRect.y(), m_accessRect.width(), m_accessRect.height(), 4); } break; case 5: run_concurrent(lock,t) { quint8 *buf; buf = new quint8[m_accessRect.width() * m_accessRect.height() * dm.pixelSize()]; dm.readBytes(buf, m_accessRect.x(), m_accessRect.y(), m_accessRect.width(), m_accessRect.height()); dm.writeBytes(buf, m_accessRect.x(), m_accessRect.y(), m_accessRect.width(), m_accessRect.height()); delete[] buf; } break; case 6: run_concurrent(lock,t) { quint8 oddPixel = 13; KisTiledDataManager srcDM(1, &oddPixel); dm.bitBlt(&srcDM, m_accessRect); } break; case 7: case 8: run_exclusive(lock,t) { m_memento = dm.getMemento(); dm.clear(m_accessRect.x(), m_accessRect.y(), m_accessRect.width(), m_accessRect.height(), 2); dm.commit(); dm.rollback(m_memento); dm.rollforward(m_memento); dm.purgeHistory(m_memento); m_memento = 0; } break; case 9: run_exclusive(lock,t) { bool b = dm.hasCurrentMemento(); Q_UNUSED(b); } break; case 10: run_exclusive(lock,t) { dm.clear(); } break; case 11: run_exclusive(lock,t) { dm.setExtent(m_accessRect); } break; } } } private: KisMementoSP m_memento; QRect m_accessRect; KisTiledDataManager &dm; QReadWriteLock &lock; }; void KisTiledDataManagerTest::stressTest() { quint8 defaultPixel = 0; KisTiledDataManager dm(1, &defaultPixel); QReadWriteLock lock; #ifdef LIMIT_LONG_TESTS const int numThreads = 8; const int numWorkers = 8; #else const int numThreads = 16; const int numWorkers = 48; #endif QThreadPool pool; pool.setMaxThreadCount(numThreads); QRect accessRect(0,0,512,512); for(qint32 i = 0; i < numWorkers; i++) { KisStressJob *job = new KisStressJob(dm, accessRect, lock); pool.start(job); accessRect.translate(512, 0); } pool.waitForDone(); } template void applyToRect(const QRect &rc, Func func) { for (int y = rc.y(); y < rc.y() + rc.height(); y += KisTileData::HEIGHT) { for (int x = rc.x(); x < rc.x() + rc.width(); x += KisTileData::WIDTH) { const int col = x / KisTileData::WIDTH; const int row = y / KisTileData::HEIGHT; func(col, row); } } } class LazyCopyingStressJob : public QRunnable { public: LazyCopyingStressJob(KisTiledDataManager &dataManager, const QRect &rect, QReadWriteLock &dmExclusiveLock, QReadWriteLock &tileExclusiveLock, int numCycles, bool isWriter) : m_accessRect(rect), dm(dataManager), m_dmExclusiveLock(dmExclusiveLock), m_tileExclusiveLock(tileExclusiveLock), m_numCycles(numCycles), m_isWriter(isWriter) { } void run() override { for(qint32 i = 0; i < m_numCycles; i++) { //const int epoch = i % 100; int t; if (m_isWriter && 0) { } else { const bool shouldClear = i % 5 <= 1; // 40% of requests are clears const bool shouldWrite = i % 5 <= 3; // other 40% of requests are writes run_concurrent(m_dmExclusiveLock, t) { if (shouldClear) { QWriteLocker locker(&m_tileExclusiveLock); dm.clear(m_accessRect, 4); } else { auto readFunc = [this] (int col, int row) { KisTileSP tile = dm.getTile(col, row, false); tile->lockForRead(); tile->unlockForRead(); }; auto writeFunc = [this] (int col, int row) { KisTileSP tile = dm.getTile(col, row, true); tile->lockForWrite(); tile->unlockForWrite(); }; auto readOldFunc = [this] (int col, int row) { KisTileSP tile = dm.getOldTile(col, row); tile->lockForRead(); tile->unlockForRead(); }; applyToRect(m_accessRect, readFunc); if (shouldWrite) { QReadLocker locker(&m_tileExclusiveLock); applyToRect(m_accessRect, writeFunc); } applyToRect(m_accessRect, readOldFunc); } } } } } private: KisMementoSP m_memento; QRect m_accessRect; KisTiledDataManager &dm; QReadWriteLock &m_dmExclusiveLock; QReadWriteLock &m_tileExclusiveLock; const int m_numCycles; const bool m_isWriter; }; void KisTiledDataManagerTest::stressTestLazyCopying() { quint8 defaultPixel = 0; KisTiledDataManager dm(1, &defaultPixel); QReadWriteLock dmLock; QReadWriteLock tileLock; #ifdef LIMIT_LONG_TESTS const int numCycles = 10000; const int numThreads = 8; const int numWorkers = 8; #else const int numThreads = 16; const int numWorkers = 32; const int numCycles = 100000; #endif QThreadPool pool; pool.setMaxThreadCount(numThreads); const QRect accessRect(0,0,512,256); for(qint32 i = 0; i < numWorkers; i++) { const bool isWriter = i == 0; LazyCopyingStressJob *job = new LazyCopyingStressJob(dm, accessRect, dmLock, tileLock, numCycles, isWriter); pool.start(job); } pool.waitForDone(); } void KisTiledDataManagerTest::stressTestExtentsColumn() { KisTiledExtentManager::Data column; struct Job : public QRunnable { Job(KisTiledExtentManager::Data &column, int index, int numCycles) : m_column(column), m_index(index), m_numCycles(numCycles) {} void run() override { for(qint32 i = 0; i < m_numCycles; i++) { if (!m_isCreated) { m_column.add(m_index); KIS_SAFE_ASSERT_RECOVER_NOOP(m_column.max() >= m_index); KIS_SAFE_ASSERT_RECOVER_NOOP(m_column.min() <= m_index); } else { m_column.remove(m_index); } m_isCreated = !m_isCreated; } } KisTiledExtentManager::Data &m_column; const int m_index; const int m_numCycles; bool m_isCreated = false; }; #ifdef LIMIT_LONG_TESTS const int numThreads = 8; const int numWorkers = 32; const int numCycles = 10000; #else const int numThreads = 16; const int numWorkers = 32; const int numCycles = 100000; #endif QThreadPool pool; pool.setMaxThreadCount(numThreads); for(qint32 i = 0; i < numWorkers; i++) { const int index = 18 + i / 13; pool.start(new Job(column, index, numCycles)); } pool.waitForDone(); QVERIFY(column.isEmpty()); QVERIFY(column.max() < column.min()); // really empty :) } void KisTiledDataManagerTest::benchmaskQRegion() { QVector rects; int poison = 0; for (int y = 0; y < 8000; y += 64) { for (int x = 0; x < 8000; x += 64) { if (poison++ % 7 == 0) continue; rects << QRect(x, y, 64, 64); } } std::random_shuffle(rects.begin(), rects.end()); QElapsedTimer timer; timer.start(); QRegion region; Q_FOREACH (const QRect &rc, rects) { region += rc; } qDebug() << "compressed rects:" << ppVar(rects.size()) << "-->" << ppVar(region.rectCount()); qDebug() << "compression time:" << timer.elapsed() << "ms"; } #include "KisRegion.h" void KisTiledDataManagerTest::benchmaskKisRegion() { QVector rects; int poison = 0; for (int y = 0; y < 8000; y += 64) { for (int x = 0; x < 8000; x += 64) { if (poison++ % 7 == 0) continue; rects << QRect(x, y, 64, 64); } } std::random_shuffle(rects.begin(), rects.end()); QElapsedTimer timer; timer.start(); auto endIt = KisRegion::mergeSparseRects(rects.begin(), rects.end()); qDebug() << "compressed rects:" << ppVar(rects.size()) << "-->" << ppVar(std::distance(rects.begin(), endIt)); qDebug() << "compression time:" << timer.elapsed() << "ms"; } +inline bool findPoint (const QPoint &pt, const QVector &rects) +{ + for (auto it = rects.begin(); it != rects.end(); ++it) { + if (it->contains(pt)) return true; + } + + return false; +} + +void KisTiledDataManagerTest::benchmaskOverlappedKisRegion() +{ + QVector rects; + + int poison = 0; + for (int y = 0; y < 8000; y += 13) { + for (int x = 0; x < 8000; x += 17) { + if (poison++ % 7 == 0) continue; + rects << QRect(x, y, 13 + (poison % 17) * 7, 17 + (poison % 13) * 7); + } + } + + const int originalSize = rects.size(); + QVector originalRects = rects; + + std::random_shuffle(rects.begin(), rects.end()); + + QElapsedTimer timer; + timer.start(); + +#if 0 + // speed reference: executes for about 150 seconds! (150000ms) + QRegion region; + Q_FOREACH (const QRect &rc, rects) { + region += rc; + } +#endif + + KisRegion::approximateOverlappingRects(rects, 64); + + qDebug() << "deoverlapped rects:" << ppVar(originalSize) << "-->" << ppVar(rects.size()); + qDebug() << "deoverlaping time:" << timer.restart() << "ms"; + + KisRegion region(rects); + + qDebug() << "compressed rects:" << ppVar(region.rects().size()); + qDebug() << "compression time:" << timer.restart() << "ms"; + + for (auto it1 = rects.begin(); it1 != rects.end(); ++it1) { + for (auto it2 = std::next(it1); it2 != rects.end(); ++it2) { + QVERIFY(!it1->intersects(*it2)); + } + } + + +#if 0 + /// very slow sanity check for invariant: "all source rects are + /// represented in the deoverlapped set of rects" + + QVector comressedRects = region.rects(); + int i = 0; + Q_FOREACH(const QRect &rc, originalRects) { + if (i % 1000 == 0) { + qDebug() << ppVar(i); + } + + for (int y = rc.y(); y <= rc.bottom(); ++y) { + for (int x = rc.x(); x <= rc.right(); ++x) { + QVERIFY(findPoint(QPoint(x, y), comressedRects)); + } + } + i++; + } +#endif +} + QTEST_MAIN(KisTiledDataManagerTest) diff --git a/libs/image/tiles3/tests/kis_tiled_data_manager_test.h b/libs/image/tiles3/tests/kis_tiled_data_manager_test.h index 5f03b47cc5..289e330028 100644 --- a/libs/image/tiles3/tests/kis_tiled_data_manager_test.h +++ b/libs/image/tiles3/tests/kis_tiled_data_manager_test.h @@ -1,73 +1,74 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_TILED_DATA_MANAGER_TEST_H #define KIS_TILED_DATA_MANAGER_TEST_H #include class KisTiledDataManager; class KisTiledDataManagerTest : public QObject { Q_OBJECT private: bool checkHole(quint8* buffer, quint8 holeColor, QRect holeRect, quint8 backgroundColor, QRect backgroundRect); bool checkTilesShared(KisTiledDataManager *srcDM, KisTiledDataManager *dstDM, bool takeOldSrc, bool takeOldDst, QRect tilesRect); bool checkTilesNotShared(KisTiledDataManager *srcDM, KisTiledDataManager *dstDM, bool takeOldSrc, bool takeOldDst, QRect tilesRect); void benchmarkCOWImpl(); private Q_SLOTS: void testUndoingNewTiles(); void testPurgedAndEmptyTransactions(); void testUnversionedBitBlt(); void testVersionedBitBlt(); void testBitBltOldData(); void testBitBltRough(); void testTransactions(); void testPurgeHistory(); void testUndoSetDefaultPixel(); void benchmarkReadOnlyTileLazy(); void benchmarkSharedPointers(); void benchmarkCOWNoPooler(); void benchmarkCOWWithPooler(); void stressTest(); void stressTestLazyCopying(); void stressTestExtentsColumn(); void benchmaskQRegion(); void benchmaskKisRegion(); + void benchmaskOverlappedKisRegion(); }; #endif /* KIS_TILED_DATA_MANAGER_TEST_H */