diff --git a/libs/brush/kis_auto_brush.cpp b/libs/brush/kis_auto_brush.cpp --- a/libs/brush/kis_auto_brush.cpp +++ b/libs/brush/kis_auto_brush.cpp @@ -251,28 +251,13 @@ // if there's coloring information, we merely change the alpha: in that case, // the dab should be big enough! if (coloringInformation) { - - // old bounds - QRect oldBounds = dst->bounds(); - // new bounds. we don't care if there is some extra memory occcupied. dst->setRect(QRect(0, 0, dstWidth, dstHeight)); - - if (dstWidth * dstHeight <= oldBounds.width() * oldBounds.height()) { - // just clear the data in dst, - memset(dst->data(), OPACITY_TRANSPARENT_U8, dstWidth * dstHeight * dst->pixelSize()); - } - else { - // enlarge the data - dst->initialize(); - } + dst->lazyGrowBufferWithoutInitialization(); } else { - if (dst->data() == 0 || dst->bounds().isEmpty()) { - warnKrita << "Creating a default black dab: no coloring info and no initialized paint device to mask"; - dst->clear(QRect(0, 0, dstWidth, dstHeight)); - } - Q_ASSERT(dst->bounds().width() >= dstWidth && dst->bounds().height() >= dstHeight); + KIS_SAFE_ASSERT_RECOVER_RETURN(dst->bounds().width() >= dstWidth && + dst->bounds().height() >= dstHeight); } quint8* dabPointer = dst->data(); @@ -317,7 +302,7 @@ applicator->initializeData(&data); int jobs = d->idealThreadCountCached; - if (dstHeight > 100 && jobs >= 4) { + if (threadingAllowed() && dstHeight > 100 && jobs >= 4) { int splitter = dstHeight / jobs; QVector rects; for (int i = 0; i < jobs - 1; i++) { diff --git a/libs/brush/kis_brush.h b/libs/brush/kis_brush.h --- a/libs/brush/kis_brush.h +++ b/libs/brush/kis_brush.h @@ -234,28 +234,45 @@ * Having got this notification the brush can update the counters * of dabs, generate some new random values if needed. * + * * NOTE: one should use **either** notifyCachedDabPainted() or prepareForSeqNo() + * * Currently, this is used by pipe'd brushes to implement * incremental and random parasites */ virtual void notifyCachedDabPainted(const KisPaintInformation& info); /** + * Is called by the multithreaded queue to prepare a specific brush + * tip for the particular seqNo. + * + * NOTE: one should use **either** notifyCachedDabPainted() or prepareForSeqNo() + * + * Currently, this is used by pipe'd brushes to implement + * incremental and random parasites + */ + virtual void prepareForSeqNo(const KisPaintInformation& info, int seqNo); + + /** + * Notify the brush if it can use QtConcurrent's threading capabilities in its + * internal routines. By default it is allowed, but some paintops (who do their + * own multithreading) may ask the brush to avoid internal threading. + */ + void setThreadingAllowed(bool value); + + /** + * \see setThreadingAllowed() for details + */ + bool threadingAllowed() const; + + /** * Return a fixed paint device that contains a correctly scaled image dab. */ virtual KisFixedPaintDeviceSP paintDevice(const KoColorSpace * colorSpace, KisDabShape const&, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0) const; /** - * Apply the brush mask to the pixels in dst. Dst should be big enough! - */ - void mask(KisFixedPaintDeviceSP dst, - KisDabShape const& shape, - const KisPaintInformation& info, - double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const; - - /** * clear dst fill it with a mask colored with KoColor */ void mask(KisFixedPaintDeviceSP dst, diff --git a/libs/brush/kis_brush.cpp b/libs/brush/kis_brush.cpp --- a/libs/brush/kis_brush.cpp +++ b/libs/brush/kis_brush.cpp @@ -108,6 +108,7 @@ , brushType(INVALID) , autoSpacingActive(false) , autoSpacingCoeff(1.0) + , threadingAllowed(true) {} ~Private() { @@ -131,6 +132,8 @@ bool autoSpacingActive; qreal autoSpacingCoeff; + + bool threadingAllowed; }; KisBrush::KisBrush() @@ -161,6 +164,7 @@ d->scale = rhs.d->scale; d->autoSpacingActive = rhs.d->autoSpacingActive; d->autoSpacingCoeff = rhs.d->autoSpacingCoeff; + d->threadingAllowed = rhs.d->threadingAllowed; setFilename(rhs.filename()); /** @@ -435,6 +439,22 @@ Q_UNUSED(info); } +void KisBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) +{ + Q_UNUSED(info); + Q_UNUSED(seqNo); +} + +void KisBrush::setThreadingAllowed(bool value) +{ + d->threadingAllowed = value; +} + +bool KisBrush::threadingAllowed() const +{ + return d->threadingAllowed; +} + void KisBrush::prepareBrushPyramid() const { if (!d->brushPyramid) { @@ -447,11 +467,6 @@ d->brushPyramid.clear(); } -void KisBrush::mask(KisFixedPaintDeviceSP dst, KisDabShape const& shape, const KisPaintInformation& info , double subPixelX, double subPixelY, qreal softnessFactor) const -{ - generateMaskAndApplyMaskOrCreateDab(dst, 0, shape, info, subPixelX, subPixelY, softnessFactor); -} - void KisBrush::mask(KisFixedPaintDeviceSP dst, const KoColor& color, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor) const { PlainColoringInformation pci(color.data()); @@ -485,7 +500,7 @@ qint32 maskHeight = outputImage.height(); dst->setRect(QRect(0, 0, maskWidth, maskHeight)); - dst->initialize(); + dst->lazyGrowBufferWithoutInitialization(); quint8* color = 0; diff --git a/libs/brush/kis_brushes_pipe.h b/libs/brush/kis_brushes_pipe.h --- a/libs/brush/kis_brushes_pipe.h +++ b/libs/brush/kis_brushes_pipe.h @@ -100,7 +100,11 @@ } void notifyCachedDabPainted(const KisPaintInformation& info) { - updateBrushIndexes(info); + updateBrushIndexes(info, -1); + } + + void prepareForSeqNo(const KisPaintInformation& info, int seqNo) { + updateBrushIndexes(info, seqNo); } void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, @@ -114,7 +118,7 @@ brush->generateMaskAndApplyMaskOrCreateDab(dst, coloringInformation, shape, info, subPixelX, subPixelY, softnessFactor); - updateBrushIndexes(info); + notifyCachedDabPainted(info); } KisFixedPaintDeviceSP paintDevice(const KoColorSpace * colorSpace, @@ -126,7 +130,7 @@ if (!brush) return 0; KisFixedPaintDeviceSP device = brush->paintDevice(colorSpace, shape, info, subPixelX, subPixelY); - updateBrushIndexes(info); + notifyCachedDabPainted(info); return device; } @@ -136,7 +140,7 @@ void testingSelectNextBrush(const KisPaintInformation& info) { (void) chooseNextBrush(info); - updateBrushIndexes(info); + notifyCachedDabPainted(info); } /** @@ -165,8 +169,11 @@ * Updates internal counters of the brush *after* a dab has been * painted on the canvas. Some incremental switching of the brushes * may me implemented in this method. + * + * If \p seqNo is equal or greater than zero, then incremental iteration is + * overriden by this seqNo value */ - virtual void updateBrushIndexes(const KisPaintInformation& info) = 0; + virtual void updateBrushIndexes(const KisPaintInformation& info, int seqNo) = 0; protected: QVector m_brushes; diff --git a/libs/brush/kis_dab_shape.h b/libs/brush/kis_dab_shape.h --- a/libs/brush/kis_dab_shape.h +++ b/libs/brush/kis_dab_shape.h @@ -36,6 +36,14 @@ , m_ratio(ratio) , m_rotation(rotation) {} + + bool operator==(const KisDabShape &rhs) const { + return + qFuzzyCompare(m_scale, rhs.m_scale) && + qFuzzyCompare(m_ratio, rhs.m_ratio) && + qFuzzyCompare(m_rotation, rhs.m_rotation); + } + qreal scale() const { return m_scale; } qreal scaleX() const { return scale(); } qreal scaleY() const { return m_scale * m_ratio; } diff --git a/libs/brush/kis_gbr_brush.h b/libs/brush/kis_gbr_brush.h --- a/libs/brush/kis_gbr_brush.h +++ b/libs/brush/kis_gbr_brush.h @@ -57,6 +57,8 @@ /// Load brush as a copy from the specified QImage (handy when you need to copy a brush!) KisGbrBrush(const QImage& image, const QString& name = QString()); + KisGbrBrush(const KisGbrBrush& rhs); + ~KisGbrBrush() override; bool load() override; @@ -101,9 +103,6 @@ */ friend class KisImageBrushesPipe; - - KisGbrBrush(const KisGbrBrush& rhs); - void setBrushType(enumBrushType type) override; void setBrushTipImage(const QImage& image) override; diff --git a/libs/brush/kis_gbr_brush.cpp b/libs/brush/kis_gbr_brush.cpp --- a/libs/brush/kis_gbr_brush.cpp +++ b/libs/brush/kis_gbr_brush.cpp @@ -139,6 +139,7 @@ , d(new Private(*rhs.d)) { setName(rhs.name()); + setBrushTipImage(rhs.brushTipImage()); d->data = QByteArray(); setValid(rhs.valid()); } diff --git a/libs/brush/kis_imagepipe_brush.h b/libs/brush/kis_imagepipe_brush.h --- a/libs/brush/kis_imagepipe_brush.h +++ b/libs/brush/kis_imagepipe_brush.h @@ -99,6 +99,7 @@ void notifyStrokeStarted() override; void notifyCachedDabPainted(const KisPaintInformation& info) override; + void prepareForSeqNo(const KisPaintInformation& info, int seqNo) override; void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const&, diff --git a/libs/brush/kis_imagepipe_brush.cpp b/libs/brush/kis_imagepipe_brush.cpp --- a/libs/brush/kis_imagepipe_brush.cpp +++ b/libs/brush/kis_imagepipe_brush.cpp @@ -78,12 +78,13 @@ static int selectPost(KisParasite::SelectionMode mode, int index, int rank, - const KisPaintInformation& info) { + const KisPaintInformation& info, + int seqNo) { switch (mode) { case KisParasite::Constant: break; case KisParasite::Incremental: - index = (index + 1) % rank; + index = (seqNo >= 0 ? seqNo : (index + 1)) % rank; break; case KisParasite::Random: index = info.randomSource()->generate(0, rank); @@ -113,7 +114,7 @@ for (int i = 0; i < m_parasite.dim; i++) { m_parasite.index[i] = 0; } - updateBrushIndexes(info); + updateBrushIndexes(info, 0); m_isInitialized = true; } @@ -128,12 +129,13 @@ return brushIndex; } - void updateBrushIndexes(const KisPaintInformation& info) override { + void updateBrushIndexes(const KisPaintInformation& info, int seqNo) override { for (int i = 0; i < m_parasite.dim; i++) { m_parasite.index[i] = selectPost(m_parasite.selection[i], m_parasite.index[i], m_parasite.rank[i], - info); + info, + seqNo); } } @@ -359,6 +361,11 @@ m_d->brushesPipe.notifyCachedDabPainted(info); } +void KisImagePipeBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) +{ + m_d->brushesPipe.prepareForSeqNo(info, seqNo); +} + void KisImagePipeBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, diff --git a/libs/brush/kis_text_brush.h b/libs/brush/kis_text_brush.h --- a/libs/brush/kis_text_brush.h +++ b/libs/brush/kis_text_brush.h @@ -38,6 +38,7 @@ void notifyStrokeStarted() override; void notifyCachedDabPainted(const KisPaintInformation& info) override; + void prepareForSeqNo(const KisPaintInformation& info, int seqNo) override; void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const&, diff --git a/libs/brush/kis_text_brush.cpp b/libs/brush/kis_text_brush.cpp --- a/libs/brush/kis_text_brush.cpp +++ b/libs/brush/kis_text_brush.cpp @@ -45,24 +45,36 @@ } KisTextBrushesPipe(const KisTextBrushesPipe &rhs) - : KisBrushesPipe(rhs) { + : KisBrushesPipe(), // no copy here! + m_text(rhs.m_text), + m_charIndex(rhs.m_charIndex), + m_currentBrushIndex(rhs.m_currentBrushIndex) + { m_brushesMap.clear(); QMapIterator iter(rhs.m_brushesMap); while (iter.hasNext()) { iter.next(); - m_brushesMap.insert(iter.key(), iter.value()); + KisGbrBrush *brush = new KisGbrBrush(*iter.value()); + m_brushesMap.insert(iter.key(), brush); + KisBrushesPipe::addBrush(brush); } } void setText(const QString &text, const QFont &font) { m_text = text; + m_charIndex = 0; clear(); for (int i = 0; i < m_text.length(); i++) { - QChar letter = m_text.at(i); + + const QChar letter = m_text.at(i); + + // skip letters that are already present in the brushes pipe + if (m_brushesMap.contains(letter)) continue; + QImage image = renderChar(letter, font); KisGbrBrush *brush = new KisGbrBrush(image, letter); brush->setSpacing(0.1); // support for letter spacing? @@ -128,10 +140,15 @@ Q_UNUSED(info); return m_currentBrushIndex; } - void updateBrushIndexes(const KisPaintInformation& info) override { + void updateBrushIndexes(const KisPaintInformation& info, int seqNo) override { Q_UNUSED(info); - m_charIndex++; + if (m_text.size()) { + m_charIndex = (seqNo >= 0 ? seqNo : (m_charIndex + 1)) % m_text.size(); + } else { + m_charIndex = 0; + } + updateBrushIndexesImpl(); } @@ -165,6 +182,8 @@ KisTextBrush::KisTextBrush(const KisTextBrush &rhs) : KisScalingSizeBrush(rhs), + m_font(rhs.m_font), + m_text(rhs.m_text), m_brushesPipe(new KisTextBrushesPipe(*rhs.m_brushesPipe)) { } @@ -214,6 +233,11 @@ m_brushesPipe->notifyCachedDabPainted(info); } +void KisTextBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) +{ + m_brushesPipe->prepareForSeqNo(info, seqNo); +} + void KisTextBrush::generateMaskAndApplyMaskOrCreateDab( KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, diff --git a/libs/brush/tests/kis_gbr_brush_test.h b/libs/brush/tests/kis_gbr_brush_test.h --- a/libs/brush/tests/kis_gbr_brush_test.h +++ b/libs/brush/tests/kis_gbr_brush_test.h @@ -26,10 +26,8 @@ Q_OBJECT // XXX disabled until I figure out why they don't work from here, while the brushes do work from Krita - void testMaskGenerationNoColor(); void testMaskGenerationSingleColor(); void testMaskGenerationDevColor(); - void testMaskGenerationDefaultColor(); private Q_SLOTS: diff --git a/libs/brush/tests/kis_gbr_brush_test.cpp b/libs/brush/tests/kis_gbr_brush_test.cpp --- a/libs/brush/tests/kis_gbr_brush_test.cpp +++ b/libs/brush/tests/kis_gbr_brush_test.cpp @@ -32,39 +32,6 @@ #include #include "kis_qimage_pyramid.h" -void KisGbrBrushTest::testMaskGenerationNoColor() -{ - KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "brush.gbr"); - brush->load(); - Q_ASSERT(brush->valid()); - const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); - - KisPaintInformation info(QPointF(100.0, 100.0), 0.5); - - // check masking an existing paint device - KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(cs); - fdev->setRect(QRect(0, 0, 100, 100)); - fdev->initialize(); - cs->setOpacity(fdev->data(), OPACITY_OPAQUE_U8, 100 * 100); - - QPoint errpoint; - QImage result(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_1.png"); - QImage image = fdev->convertToQImage(0); - - if (!TestUtil::compareQImages(errpoint, image, result)) { - image.save("kis_gbr_brush_test_1.png"); - QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); - } - - brush->mask(fdev, KisDabShape(), info); - - result = QImage(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_2.png"); - image = fdev->convertToQImage(0); - if (!TestUtil::compareQImages(errpoint, image, result)) { - image.save("kis_gbr_brush_test_2.png"); - QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); - } -} void KisGbrBrushTest::testMaskGenerationSingleColor() { @@ -127,37 +94,6 @@ } } -void KisGbrBrushTest::testMaskGenerationDefaultColor() -{ - KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "brush.gbr"); - brush->load(); - Q_ASSERT(brush->valid()); - const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); - - KisPaintInformation info(QPointF(100.0, 100.0), 0.5); - - // check masking an existing paint device - KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(cs); - fdev->setRect(QRect(0, 0, 100, 100)); - fdev->initialize(); - cs->setOpacity(fdev->data(), OPACITY_OPAQUE_U8, 100 * 100); - - // check creating a mask dab with a default color - fdev = new KisFixedPaintDevice(cs); - brush->mask(fdev, KisDabShape(), info); - - QPoint errpoint; - QImage result = QImage(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_3.png"); - QImage image = fdev->convertToQImage(0); - if (!TestUtil::compareQImages(errpoint, image, result)) { - image.save("kis_gbr_brush_test_5.png"); - QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); - } - - delete brush; -} - - void KisGbrBrushTest::testImageGeneration() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "testing_brush_512_bars.gbr"); diff --git a/libs/brush/tests/kis_imagepipe_brush_test.cpp b/libs/brush/tests/kis_imagepipe_brush_test.cpp --- a/libs/brush/tests/kis_imagepipe_brush_test.cpp +++ b/libs/brush/tests/kis_imagepipe_brush_test.cpp @@ -142,10 +142,9 @@ QRect fillRect(0, 0, maskWidth, maskHeight); fixedDab->setRect(fillRect); - fixedDab->initialize(); - fixedDab->fill(fillRect.x(), fillRect.y(), fillRect.width(), fillRect.height(), fillColor.data()); + fixedDab->lazyGrowBufferWithoutInitialization(); - brush->mask(fixedDab, KisDabShape(realScale, 1.0, realAngle), info); + brush->mask(fixedDab, fillColor, KisDabShape(realScale, 1.0, realAngle), info); QCOMPARE(fixedDab->bounds(), fillRect); QImage result = fixedDab->convertToQImage(0); diff --git a/libs/global/CMakeLists.txt b/libs/global/CMakeLists.txt --- a/libs/global/CMakeLists.txt +++ b/libs/global/CMakeLists.txt @@ -1,3 +1,5 @@ +add_subdirectory( tests ) + include(CheckFunctionExists) check_function_exists(backtrace HAVE_BACKTRACE) configure_file(config-debug.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-debug.h) @@ -21,6 +23,9 @@ kis_acyclic_signal_connector.cpp kis_latency_tracker.cpp KisQPainterStateSaver.cpp + KisSharedThreadPoolAdapter.cpp + KisSharedRunnable.cpp + KisRollingMeanAccumulatorWrapper.cpp KisLoggingManager.cpp ) diff --git a/libs/global/KisRollingMeanAccumulatorWrapper.h b/libs/global/KisRollingMeanAccumulatorWrapper.h new file mode 100644 --- /dev/null +++ b/libs/global/KisRollingMeanAccumulatorWrapper.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017 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 KISROLLINGMEANACCUMULATORWRAPPER_H +#define KISROLLINGMEANACCUMULATORWRAPPER_H + +#include +#include +#include "kritaglobal_export.h" + +/** + * @brief A simple wrapper class that hides boost includes from QtCreator preventing it + * from crashing when one adds boost's accumulator into a file + */ + +class KRITAGLOBAL_EXPORT KisRollingMeanAccumulatorWrapper +{ +public: + /** + * Create a rolling mean accumulator with window \p windowSize + */ + KisRollingMeanAccumulatorWrapper(int windowSize); + ~KisRollingMeanAccumulatorWrapper(); + + /** + * Add \p value to a set of numbers + */ + void operator()(qreal value); + + /** + * Get rolling mean of the numbers passed to the operator + */ + qreal rollingMean() const; + + /** + * Reset accumulator and any stored value + */ + void reset(int windowSize); + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISROLLINGMEANACCUMULATORWRAPPER_H diff --git a/libs/global/KisRollingMeanAccumulatorWrapper.cpp b/libs/global/KisRollingMeanAccumulatorWrapper.cpp new file mode 100644 --- /dev/null +++ b/libs/global/KisRollingMeanAccumulatorWrapper.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017 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 "KisRollingMeanAccumulatorWrapper.h" + +#include +#include +#include + +using namespace boost::accumulators; + +struct KisRollingMeanAccumulatorWrapper::Private { + Private(int windowSize) + : accumulator(tag::rolling_window::window_size = windowSize) + { + } + + accumulator_set > accumulator; +}; + + +KisRollingMeanAccumulatorWrapper::KisRollingMeanAccumulatorWrapper(int windowSize) + : m_d(new Private(windowSize)) +{ +} + +KisRollingMeanAccumulatorWrapper::~KisRollingMeanAccumulatorWrapper() +{ +} + +void KisRollingMeanAccumulatorWrapper::operator()(qreal value) +{ + m_d->accumulator(value); +} + +qreal KisRollingMeanAccumulatorWrapper::rollingMean() const +{ + return boost::accumulators::rolling_mean(m_d->accumulator); +} + +void KisRollingMeanAccumulatorWrapper::reset(int windowSize) +{ + m_d->accumulator = + accumulator_set>( + tag::rolling_window::window_size = windowSize); +} diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/global/KisSharedRunnable.h copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/global/KisSharedRunnable.h --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/global/KisSharedRunnable.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,30 +16,27 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISSHAREDRUNNABLE_H +#define KISSHAREDRUNNABLE_H -#include +#include +#include +class KisSharedThreadPoolAdapter; -class KisOpenglCanvasDebugger +class KRITAGLOBAL_EXPORT KisSharedRunnable : public QRunnable { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); + virtual void runShared() = 0; + void run() final; - static KisOpenglCanvasDebugger* instance(); - - bool showFpsOnCanvas() const; - - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); +private: + friend class KisSharedThreadPoolAdapter; + void setSharedThreadPoolAdapter(KisSharedThreadPoolAdapter *adapter); private: - struct Private; - const QScopedPointer m_d; + KisSharedThreadPoolAdapter *m_adapter = 0; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISSHAREDRUNNABLE_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/global/KisSharedRunnable.cpp copy from libs/image/kis_projection_updates_filter.cpp copy to libs/global/KisSharedRunnable.cpp --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/global/KisSharedRunnable.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,22 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisSharedRunnable.h" +#include "KisSharedThreadPoolAdapter.h" +#include "kis_assert.h" -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +void KisSharedRunnable::run() { + runShared(); + + if (m_adapter) { + m_adapter->notifyJobCompleted(); + } } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +void KisSharedRunnable::setSharedThreadPoolAdapter(KisSharedThreadPoolAdapter *adapter) { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; + m_adapter = adapter; } + diff --git a/libs/global/KisSharedThreadPoolAdapter.h b/libs/global/KisSharedThreadPoolAdapter.h new file mode 100644 --- /dev/null +++ b/libs/global/KisSharedThreadPoolAdapter.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 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 KISSHAREDTHREADPOOLADAPTER_H +#define KISSHAREDTHREADPOOLADAPTER_H + +#include +#include + +#include + +class QThreadPool; + +class KRITAGLOBAL_EXPORT KisSharedThreadPoolAdapter +{ +public: + KisSharedThreadPoolAdapter(QThreadPool *parentPool); + ~KisSharedThreadPoolAdapter(); + + void start(KisSharedRunnable *runnable, int priority = 0); + bool tryStart(KisSharedRunnable *runnable); + + bool waitForDone(int msecs = -1); + +private: + friend class KisSharedRunnable; + void notifyJobCompleted(); + + KisSharedThreadPoolAdapter(KisSharedThreadPoolAdapter &rhs) = delete; + +private: + QThreadPool *m_parentPool; + QMutex m_mutex; + QWaitCondition m_waitCondition; + int m_numRunningJobs; +}; + +#endif // KISSHAREDTHREADPOOLADAPTER_H diff --git a/libs/global/KisSharedThreadPoolAdapter.cpp b/libs/global/KisSharedThreadPoolAdapter.cpp new file mode 100644 --- /dev/null +++ b/libs/global/KisSharedThreadPoolAdapter.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017 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 "KisSharedThreadPoolAdapter.h" + +#include "kis_assert.h" +#include +#include + + +KisSharedThreadPoolAdapter::KisSharedThreadPoolAdapter(QThreadPool *parentPool) + : m_parentPool(parentPool), + m_numRunningJobs(0) +{ +} + +KisSharedThreadPoolAdapter::~KisSharedThreadPoolAdapter() +{ + waitForDone(); + KIS_SAFE_ASSERT_RECOVER_NOOP(!m_numRunningJobs); +} + +void KisSharedThreadPoolAdapter::start(KisSharedRunnable *runnable, int priority) +{ + QMutexLocker l(&m_mutex); + + runnable->setSharedThreadPoolAdapter(this); + m_parentPool->start(runnable, priority); + + m_numRunningJobs++; +} + +bool KisSharedThreadPoolAdapter::tryStart(KisSharedRunnable *runnable) +{ + QMutexLocker l(&m_mutex); + + runnable->setSharedThreadPoolAdapter(this); + const bool result = m_parentPool->tryStart(runnable); + + if (result) { + m_numRunningJobs++; + } + + return result; +} + +bool KisSharedThreadPoolAdapter::waitForDone(int msecs) +{ + QElapsedTimer t; + t.start(); + + while (1) { + QMutexLocker l(&m_mutex); + + if (!m_numRunningJobs) break; + + const qint64 elapsed = t.elapsed(); + if (msecs >= 0 && msecs < elapsed) return false; + + const unsigned long timeout = msecs < 0 ? ULONG_MAX : msecs - elapsed; + + m_waitCondition.wait(&m_mutex, timeout); + } + + return true; +} + +void KisSharedThreadPoolAdapter::notifyJobCompleted() +{ + QMutexLocker l(&m_mutex); + + KIS_SAFE_ASSERT_RECOVER (m_numRunningJobs > 0) { + m_waitCondition.wakeAll(); + return; + } + + m_numRunningJobs--; + if (!m_numRunningJobs) { + m_waitCondition.wakeAll(); + } +} + + diff --git a/libs/global/tests/CMakeLists.txt b/libs/global/tests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libs/global/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +include(ECMAddTests) +include(KritaAddBrokenUnitTest) + +macro_add_unittest_definitions() + +ecm_add_test(KisSharedThreadPoolAdapterTest.cpp + TEST_NAME KisSharedThreadPoolAdapter + LINK_LIBRARIES kritaglobal Qt5::Test) diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/global/tests/KisSharedThreadPoolAdapterTest.h copy from libs/image/kis_projection_updates_filter.cpp copy to libs/global/tests/KisSharedThreadPoolAdapterTest.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/global/tests/KisSharedThreadPoolAdapterTest.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,19 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISSHAREDTHREADPOOLADAPTERTEST_H +#define KISSHAREDTHREADPOOLADAPTERTEST_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisSharedThreadPoolAdapterTest : public QObject { -} + Q_OBJECT -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +private Q_SLOTS: + + void test(); + +}; + +#endif // KISSHAREDTHREADPOOLADAPTERTEST_H diff --git a/libs/global/tests/KisSharedThreadPoolAdapterTest.cpp b/libs/global/tests/KisSharedThreadPoolAdapterTest.cpp new file mode 100644 --- /dev/null +++ b/libs/global/tests/KisSharedThreadPoolAdapterTest.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017 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 "KisSharedThreadPoolAdapterTest.h" + +#include + +#include +#include +#include + +#include "kis_debug.h" + +struct NastyCounter : public KisSharedRunnable +{ + NastyCounter(QAtomicInt *value) + : m_value(value) + { + } + + void runShared() override { + for (int i = 0; i < numCycles(); i++) { + QThread::msleep(qrand() % 10); + m_value->ref(); + } + } + + static int numCycles() { + return 100; + } + +private: + QAtomicInt *m_value; +}; + + +void KisSharedThreadPoolAdapterTest::test() +{ + QAtomicInt value; + + KisSharedThreadPoolAdapter adapter(QThreadPool::globalInstance()); + + const int numThreads = 30; + + for (int i = 0; i < numThreads; i++) { + adapter.start(new NastyCounter(&value)); + } + + adapter.waitForDone(); + + QCOMPARE(int(value), numThreads * NastyCounter::numCycles()); +} + +// TODO: test waitForDone on empty queue!!!! + + +QTEST_MAIN(KisSharedThreadPoolAdapterTest) diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt --- a/libs/image/CMakeLists.txt +++ b/libs/image/CMakeLists.txt @@ -56,6 +56,7 @@ tiles3/swap/kis_tile_data_swapper.cpp kis_distance_information.cpp kis_painter.cc + kis_painter_blt_multi_fixed.cpp kis_marker_painter.cpp kis_progress_updater.cpp brushengine/kis_paint_information.cc @@ -67,6 +68,7 @@ brushengine/kis_paintop_registry.cc brushengine/kis_paintop_settings.cpp brushengine/kis_paintop_settings_update_proxy.cpp + brushengine/kis_paintop_utils.cpp brushengine/kis_no_size_paintop_settings.cpp brushengine/kis_locked_properties.cc brushengine/kis_locked_properties_proxy.cpp @@ -76,6 +78,7 @@ brushengine/kis_combo_based_paintop_property.cpp brushengine/kis_slider_based_paintop_property.cpp brushengine/kis_standard_uniform_properties_factory.cpp + brushengine/KisStrokeSpeedMeasurer.cpp commands/kis_deselect_global_selection_command.cpp commands/kis_image_change_layers_command.cpp commands/kis_image_change_visibility_command.cpp @@ -166,10 +169,15 @@ kis_update_job_item.cpp kis_stroke_strategy_undo_command_based.cpp kis_simple_stroke_strategy.cpp + KisRunnableBasedStrokeStrategy.cpp + KisRunnableStrokeJobData.cpp + KisRunnableStrokeJobsInterface.cpp + KisFakeRunnableStrokeJobsExecutor.cpp kis_stroke_job_strategy.cpp kis_stroke_strategy.cpp kis_stroke.cpp kis_strokes_queue.cpp + KisStrokesQueueMutatedJobInterface.cpp kis_simple_update_queue.cpp kis_update_scheduler.cpp kis_queues_progress_updater.cpp diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisFakeRunnableStrokeJobsExecutor.h copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisFakeRunnableStrokeJobsExecutor.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisFakeRunnableStrokeJobsExecutor.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,16 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISFAKERUNNABLESTROKEJOBSEXECUTOR_H +#define KISFAKERUNNABLESTROKEJOBSEXECUTOR_H +#include "KisRunnableStrokeJobsInterface.h" -#include -#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KRITAIMAGE_EXPORT KisFakeRunnableStrokeJobsExecutor : public KisRunnableStrokeJobsInterface { -} +public: + void addRunnableJobs(const QVector &list); +}; -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISFAKERUNNABLESTROKEJOBSEXECUTOR_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisFakeRunnableStrokeJobsExecutor.cpp --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,21 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisFakeRunnableStrokeJobsExecutor.h" +#include +#include -#include -#include +#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +void KisFakeRunnableStrokeJobsExecutor::addRunnableJobs(const QVector &list) { -} + Q_FOREACH (KisRunnableStrokeJobData *data, list) { + KIS_SAFE_ASSERT_RECOVER_NOOP(data->sequentiality() != KisStrokeJobData::BARRIER && "barrier jobs are not supported on the fake executor"); + KIS_SAFE_ASSERT_RECOVER_NOOP(data->exclusivity() != KisStrokeJobData::EXCLUSIVE && "exclusive jobs are not supported on the fake executor"); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; + data->run(); + } + + qDeleteAll(list); } diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/image/KisRenderedDab.h copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/image/KisRenderedDab.h --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/image/KisRenderedDab.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,30 +16,31 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISRENDEREDDAB_H +#define KISRENDEREDDAB_H -#include +#include "kis_types.h" +#include "kis_fixed_paint_device.h" - - -class KisOpenglCanvasDebugger +struct KisRenderedDab { -public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); - - static KisOpenglCanvasDebugger* instance(); - - bool showFpsOnCanvas() const; - - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); - -private: - struct Private; - const QScopedPointer m_d; + KisRenderedDab() {} + KisRenderedDab(KisFixedPaintDeviceSP _device) + : device(_device), + offset(_device->bounds().topLeft()) + { + } + + KisFixedPaintDeviceSP device; + QPoint offset; + + qreal opacity = OPACITY_OPAQUE_F; + qreal flow = OPACITY_OPAQUE_F; + qreal averageOpacity = OPACITY_TRANSPARENT_F; + + inline QRect realBounds() const { + return QRect(offset, device->bounds().size()); + } }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISRENDEREDDAB_H diff --git a/libs/image/KisRunnableBasedStrokeStrategy.h b/libs/image/KisRunnableBasedStrokeStrategy.h new file mode 100644 --- /dev/null +++ b/libs/image/KisRunnableBasedStrokeStrategy.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2017 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 KISRUNNABLEBASEDSTROKESTRATEGY_H +#define KISRUNNABLEBASEDSTROKESTRATEGY_H + +#include "kis_simple_stroke_strategy.h" + +class KisRunnableStrokeJobsInterface; + +class KRITAIMAGE_EXPORT KisRunnableBasedStrokeStrategy : public KisSimpleStrokeStrategy +{ +private: + struct JobsInterface; + +public: + KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name); + KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs); + ~KisRunnableBasedStrokeStrategy(); + + void doStrokeCallback(KisStrokeJobData *data) override; + + KisRunnableStrokeJobsInterface *runnableJobsInterface() const; + +private: + const QScopedPointer m_jobsInterface; +}; + +#endif // KISRUNNABLEBASEDSTROKESTRATEGY_H diff --git a/libs/image/KisRunnableBasedStrokeStrategy.cpp b/libs/image/KisRunnableBasedStrokeStrategy.cpp new file mode 100644 --- /dev/null +++ b/libs/image/KisRunnableBasedStrokeStrategy.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 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 "KisRunnableBasedStrokeStrategy.h" + +#include +#include + +#include "KisRunnableStrokeJobData.h" +#include "KisRunnableStrokeJobsInterface.h" + +struct KisRunnableBasedStrokeStrategy::JobsInterface : public KisRunnableStrokeJobsInterface +{ + JobsInterface(KisRunnableBasedStrokeStrategy *q) + : m_q(q) + { + } + + + void addRunnableJobs(const QVector &list) { + QVector newList; + + Q_FOREACH (KisRunnableStrokeJobData *item, list) { + newList.append(item); + } + + m_q->addMutatedJobs(newList); + } + +private: + KisRunnableBasedStrokeStrategy *m_q; +}; + + +KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name) + : KisSimpleStrokeStrategy(id, name), + m_jobsInterface(new JobsInterface(this)) +{ +} + +KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs) + : KisSimpleStrokeStrategy(rhs), + m_jobsInterface(new JobsInterface(this)) +{ +} + +KisRunnableBasedStrokeStrategy::~KisRunnableBasedStrokeStrategy() +{ +} + +void KisRunnableBasedStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) +{ + if (!data) return; + + KisRunnableStrokeJobData *runnable = dynamic_cast(data); + if (!runnable) return; + + runnable->run(); +} + +KisRunnableStrokeJobsInterface *KisRunnableBasedStrokeStrategy::runnableJobsInterface() const +{ + return m_jobsInterface.data(); +} diff --git a/libs/image/KisRunnableStrokeJobData.h b/libs/image/KisRunnableStrokeJobData.h new file mode 100644 --- /dev/null +++ b/libs/image/KisRunnableStrokeJobData.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017 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 KISRUNNABLESTROKEJOBDATA_H +#define KISRUNNABLESTROKEJOBDATA_H + +#include "kritaimage_export.h" +#include "kis_stroke_job_strategy.h" +#include + +class QRunnable; + +class KRITAIMAGE_EXPORT KisRunnableStrokeJobData : public KisStrokeJobData { +public: + KisRunnableStrokeJobData(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); + + KisRunnableStrokeJobData(std::function func, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); + + ~KisRunnableStrokeJobData(); + + void run(); + +private: + QRunnable *m_runnable = 0; + std::function m_func; +}; + +#endif // KISRUNNABLESTROKEJOBDATA_H diff --git a/libs/image/KisRunnableStrokeJobData.cpp b/libs/image/KisRunnableStrokeJobData.cpp new file mode 100644 --- /dev/null +++ b/libs/image/KisRunnableStrokeJobData.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 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 "KisRunnableStrokeJobData.h" + +#include +#include + +KisRunnableStrokeJobData::KisRunnableStrokeJobData(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) + : KisStrokeJobData(sequentiality, exclusivity), + m_runnable(runnable) +{ +} + +KisRunnableStrokeJobData::KisRunnableStrokeJobData(std::function func, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) + : KisStrokeJobData(sequentiality, exclusivity), + m_func(func) +{ +} + +KisRunnableStrokeJobData::~KisRunnableStrokeJobData() { + if (m_runnable && m_runnable->autoDelete()) { + delete m_runnable; + } +} + +void KisRunnableStrokeJobData::run() { + if (m_runnable) { + m_runnable->run(); + } else if (m_func) { + m_func(); + } +} diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisRunnableStrokeJobsInterface.h copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisRunnableStrokeJobsInterface.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisRunnableStrokeJobsInterface.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,22 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" - +#ifndef KISRUNNABLESTROKEJOBSINTERFACE_H +#define KISRUNNABLESTROKEJOBSINTERFACE_H +#include "kritaimage_export.h" #include -#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() -{ -} +class KisRunnableStrokeJobData; + -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +class KRITAIMAGE_EXPORT KisRunnableStrokeJobsInterface { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +public: + virtual ~KisRunnableStrokeJobsInterface(); + + void addRunnableJob(KisRunnableStrokeJobData *data); + virtual void addRunnableJobs(const QVector &list) = 0; +}; + +#endif // KISRUNNABLESTROKEJOBSINTERFACE_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisRunnableStrokeJobsInterface.cpp copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisRunnableStrokeJobsInterface.cpp --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisRunnableStrokeJobsInterface.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,16 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisRunnableStrokeJobsInterface.h" +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +KisRunnableStrokeJobsInterface::~KisRunnableStrokeJobsInterface() { + } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +void KisRunnableStrokeJobsInterface::addRunnableJob(KisRunnableStrokeJobData *data) { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; + addRunnableJobs({data}); } diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisStrokesQueueMutatedJobInterface.h copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisStrokesQueueMutatedJobInterface.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisStrokesQueueMutatedJobInterface.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,19 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISSTROKESQUEUEMUTATEDJOBINTERFACE_H +#define KISSTROKESQUEUEMUTATEDJOBINTERFACE_H +#include "kis_types.h" -#include -#include +class KisStrokeJobData; -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisStrokesQueueMutatedJobInterface { -} +public: + virtual ~KisStrokesQueueMutatedJobInterface(); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} + virtual void addMutatedJobs(KisStrokeId strokeId, const QVector list) = 0; +}; + +#endif // KISSTROKESQUEUEMUTATEDJOBINTERFACE_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisStrokesQueueMutatedJobInterface.cpp copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisStrokesQueueMutatedJobInterface.cpp --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisStrokesQueueMutatedJobInterface.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,9 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisStrokesQueueMutatedJobInterface.h" - -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +KisStrokesQueueMutatedJobInterface::~KisStrokesQueueMutatedJobInterface() { } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisUpdaterContextSnapshotEx.h copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisUpdaterContextSnapshotEx.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisUpdaterContextSnapshotEx.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,19 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISUPDATERCONTEXTSNAPSHOTEX_H +#define KISUPDATERCONTEXTSNAPSHOTEX_H +enum KisUpdaterContextSnapshotExTag { + ContextEmpty = 0x00, + HasSequentialJob = 0x01, + HasUniquelyConcurrentJob = 0x02, + HasConcurrentJob = 0x04, + HasBarrierJob = 0x08, + HasMergeJob = 0x10 +}; -#include -#include +Q_DECLARE_FLAGS(KisUpdaterContextSnapshotEx, KisUpdaterContextSnapshotExTag); +Q_DECLARE_OPERATORS_FOR_FLAGS(KisUpdaterContextSnapshotEx); -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() -{ -} - -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISUPDATERCONTEXTSNAPSHOTEX_H diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/image/brushengine/KisStrokeSpeedMeasurer.h copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/image/brushengine/KisStrokeSpeedMeasurer.h --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/image/brushengine/KisStrokeSpeedMeasurer.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,30 +16,38 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISSTROKESPEEDMEASURER_H +#define KISSTROKESPEEDMEASURER_H +#include "kritaimage_export.h" #include +#include +class QPointF; -class KisOpenglCanvasDebugger + +class KRITAIMAGE_EXPORT KisStrokeSpeedMeasurer { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); + KisStrokeSpeedMeasurer(int timeSmoothWindow); + ~KisStrokeSpeedMeasurer(); + + void addSample(const QPointF &pt, int time); + void addSamples(const QVector &points, int time); - static KisOpenglCanvasDebugger* instance(); + qreal averageSpeed() const; + qreal currentSpeed() const; + qreal maxSpeed() const; - bool showFpsOnCanvas() const; + void reset(); - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); +private: + void sampleMaxSpeed(); private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISSTROKESPEEDMEASURER_H diff --git a/libs/image/brushengine/KisStrokeSpeedMeasurer.cpp b/libs/image/brushengine/KisStrokeSpeedMeasurer.cpp new file mode 100644 --- /dev/null +++ b/libs/image/brushengine/KisStrokeSpeedMeasurer.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2017 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 "KisStrokeSpeedMeasurer.h" + +#include +#include + +#include "kis_global.h" + +struct KisStrokeSpeedMeasurer::Private +{ + struct StrokeSample { + StrokeSample() {} + StrokeSample(int _time, qreal _distance) : time(_time), distance(_distance) {} + + int time = 0; /* ms */ + qreal distance = 0; + }; + + int timeSmoothWindow = 0; + + QList samples; + QPointF lastSamplePos; + int startTime = 0; + + qreal maxSpeed = 0; + + void purgeOldSamples(); + void addSampleImpl(const QPointF &pt, int time); +}; + +KisStrokeSpeedMeasurer::KisStrokeSpeedMeasurer(int timeSmoothWindow) + : m_d(new Private()) +{ + m_d->timeSmoothWindow = timeSmoothWindow; +} + +KisStrokeSpeedMeasurer::~KisStrokeSpeedMeasurer() +{ +} + +void KisStrokeSpeedMeasurer::Private::addSampleImpl(const QPointF &pt, int time) +{ + if (samples.isEmpty()) { + lastSamplePos = pt; + startTime = time; + samples.append(Private::StrokeSample(time, 0)); + } else { + Private::StrokeSample &lastSample = samples.last(); + + const qreal newStrokeDistance = lastSample.distance + kisDistance(lastSamplePos, pt); + lastSamplePos = pt; + + if (lastSample.time >= time) { + lastSample.distance = newStrokeDistance; + } else { + samples.append(Private::StrokeSample(time, newStrokeDistance)); + } + } +} + +void KisStrokeSpeedMeasurer::addSample(const QPointF &pt, int time) +{ + m_d->addSampleImpl(pt, time); + m_d->purgeOldSamples(); + sampleMaxSpeed(); +} + +void KisStrokeSpeedMeasurer::addSamples(const QVector &points, int time) +{ + const int lastSampleTime = !m_d->samples.isEmpty() ? m_d->samples.last().time : 0; + + const int timeSmoothBase = qMin(lastSampleTime, time); + const qreal timeSmoothStep = qreal(time - timeSmoothBase) / points.size(); + + for (int i = 0; i < points.size(); i++) { + const int sampleTime = timeSmoothBase + timeSmoothStep * (i + 1); + m_d->addSampleImpl(points[i], sampleTime); + } + + m_d->purgeOldSamples(); + sampleMaxSpeed(); +} + +qreal KisStrokeSpeedMeasurer::averageSpeed() const +{ + if (m_d->samples.isEmpty()) return 0; + + const Private::StrokeSample &lastSample = m_d->samples.last(); + + const int timeDiff = lastSample.time - m_d->startTime; + if (!timeDiff) return 0; + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(timeDiff > 0, 0); + + return lastSample.distance / timeDiff; +} + +void KisStrokeSpeedMeasurer::Private::purgeOldSamples() +{ + if (samples.size() <= 1) return; + + const Private::StrokeSample lastSample = samples.last(); + + auto lastValueToKeep = samples.end(); + + for (auto it = samples.begin(); it != samples.end(); ++it) { + KIS_SAFE_ASSERT_RECOVER_RETURN(lastSample.time - it->time >= 0); + + if (lastSample.time - it->time < timeSmoothWindow) break; + lastValueToKeep = it; + } + + if (lastValueToKeep != samples.begin() && + lastValueToKeep != samples.end()) { + + samples.erase(samples.begin(), lastValueToKeep - 1); + } +} + +qreal KisStrokeSpeedMeasurer::currentSpeed() const +{ + if (m_d->samples.size() <= 1) return 0; + + const Private::StrokeSample firstSample = m_d->samples.first(); + const Private::StrokeSample lastSample = m_d->samples.last(); + + const int timeDiff = lastSample.time - firstSample.time; + if (!timeDiff) return 0; + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(timeDiff > 0, 0); + + return (lastSample.distance - firstSample.distance) / timeDiff; +} + +qreal KisStrokeSpeedMeasurer::maxSpeed() const +{ + return m_d->maxSpeed; +} + +void KisStrokeSpeedMeasurer::reset() +{ + m_d->samples.clear(); + m_d->lastSamplePos = QPointF(); + m_d->startTime = 0; + m_d->maxSpeed = 0; +} + +void KisStrokeSpeedMeasurer::sampleMaxSpeed() +{ + if (m_d->samples.size() <= 1) return; + + const Private::StrokeSample firstSample = m_d->samples.first(); + const Private::StrokeSample lastSample = m_d->samples.last(); + + const int timeDiff = lastSample.time - firstSample.time; + if (timeDiff < m_d->timeSmoothWindow) return; + + const qreal speed = currentSpeed(); + if (speed > m_d->maxSpeed) { + m_d->maxSpeed = speed; + } +} diff --git a/libs/image/brushengine/kis_paint_information.h b/libs/image/brushengine/kis_paint_information.h --- a/libs/image/brushengine/kis_paint_information.h +++ b/libs/image/brushengine/kis_paint_information.h @@ -113,6 +113,13 @@ DistanceInformationRegistrar r = registerDistanceInformation(distanceInfo); spacingInfo = op.paintAt(*this); timingInfo = op.updateTimingImpl(*this); + + // Initiate the process of locking the drawing angle. The locked value will + // always be present in the internals, but it will be requested but the users + // with a special parameter of drawingAngle() only. + if (!this->isHoveringMode()) { + distanceInfo->lockCurrentDrawingAngle(*this); + } } distanceInfo->registerPaintedDab(*this, spacingInfo, timingInfo); @@ -154,14 +161,7 @@ * WARNING: this method is available *only* inside paintAt() call, * that is when the distance information is registered. */ - qreal drawingAngle() const; - - /** - * Lock current drawing angle for the rest of the stroke. If some - * value has already been locked, \p alpha shown the coefficient - * with which the new velue should be blended in. - */ - void lockCurrentDrawingAngle(qreal alpha) const; + qreal drawingAngle(bool considerLockedAngle = false) const; /** * Current brush direction vector computed from the cursor movement diff --git a/libs/image/brushengine/kis_paint_information.cc b/libs/image/brushengine/kis_paint_information.cc --- a/libs/image/brushengine/kis_paint_information.cc +++ b/libs/image/brushengine/kis_paint_information.cc @@ -50,15 +50,14 @@ speed(speed_), isHoveringMode(isHoveringMode_), randomSource(0), - currentDistanceInfo(0), levelOfDetail(0) { } ~Private() { - KIS_ASSERT_RECOVER_NOOP(!currentDistanceInfo); + KIS_ASSERT_RECOVER_NOOP(!sanityIsRegistered); } Private(const Private &rhs) { copy(rhs); @@ -80,7 +79,8 @@ speed = rhs.speed; isHoveringMode = rhs.isHoveringMode; randomSource = rhs.randomSource; - currentDistanceInfo = rhs.currentDistanceInfo; + sanityIsRegistered = false; // HINT: we do not copy registration mark! + directionHistoryInfo = rhs.directionHistoryInfo; canvasRotation = rhs.canvasRotation; canvasMirroredH = rhs.canvasMirroredH; if (rhs.drawingAngleOverride) { @@ -106,16 +106,38 @@ bool canvasMirroredH; boost::optional drawingAngleOverride; - KisDistanceInformation *currentDistanceInfo; + bool sanityIsRegistered = false; + + struct DirectionHistoryInfo { + DirectionHistoryInfo() {} + DirectionHistoryInfo(qreal _lastAngle, + QPointF _lastPosition, + boost::optional _lockedDrawingAngle) + : lastAngle(_lastAngle), + lastPosition(_lastPosition), + lockedDrawingAngle(_lockedDrawingAngle) + { + } + + qreal lastAngle = 0.0; + QPointF lastPosition; + boost::optional lockedDrawingAngle; + }; + boost::optional directionHistoryInfo; int levelOfDetail; void registerDistanceInfo(KisDistanceInformation *di) { - currentDistanceInfo = di; + directionHistoryInfo = DirectionHistoryInfo(di->lastDrawingAngle(), + di->lastPosition(), + di->lockedDrawingAngleOptional()); + + KIS_SAFE_ASSERT_RECOVER_NOOP(!sanityIsRegistered); + sanityIsRegistered = true; } void unregisterDistanceInfo() { - currentDistanceInfo = 0; + sanityIsRegistered = false; } }; @@ -326,87 +348,58 @@ qreal KisPaintInformation::drawingAngleSafe(const KisDistanceInformation &distance) const { - if (d->drawingAngleOverride) return *d->drawingAngleOverride; + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!d->directionHistoryInfo, 0.0); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(distance.hasLastDabInformation(), 0.0); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!d->drawingAngleOverride, 0.0); - if (!distance.hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingAngleSafe()" << "Cannot access Distance Info last dab data"; - return 0.0; - } + return KisAlgebra2D::directionBetweenPoints(distance.lastPosition(), + pos(), + distance.lastDrawingAngle()); - return distance.nextDrawingAngle(pos()); } KisPaintInformation::DistanceInformationRegistrar KisPaintInformation::registerDistanceInformation(KisDistanceInformation *distance) { return DistanceInformationRegistrar(this, distance); } -qreal KisPaintInformation::drawingAngle() const +qreal KisPaintInformation::drawingAngle(bool considerLockedAngle) const { if (d->drawingAngleOverride) return *d->drawingAngleOverride; - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingAngle()" << "Cannot access Distance Info last dab data"; + if (!d->directionHistoryInfo) { + warnKrita << "KisPaintInformation::drawingAngleSafe()" << "DirectionHistoryInfo object is not available"; return 0.0; } - return d->currentDistanceInfo->nextDrawingAngle(pos()); -} - -void KisPaintInformation::lockCurrentDrawingAngle(qreal alpha_unused) const -{ - Q_UNUSED(alpha_unused); + if (considerLockedAngle && + d->directionHistoryInfo->lockedDrawingAngle) { - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::lockCurrentDrawingAngle()" << "Cannot access Distance Info last dab data"; - return; + return *d->directionHistoryInfo->lockedDrawingAngle; } - qreal angle = d->currentDistanceInfo->nextDrawingAngle(pos(), false); - - qreal newAngle = angle; - - if (d->currentDistanceInfo->hasLockedDrawingAngle()) { - const qreal stabilizingCoeff = 20.0; - const qreal dist = stabilizingCoeff * d->currentDistanceInfo->currentSpacing().scalarApprox(); - const qreal alpha = qMax(0.0, dist - d->currentDistanceInfo->scalarDistanceApprox()) / dist; - - const qreal oldAngle = d->currentDistanceInfo->lockedDrawingAngle(); - - if (shortestAngularDistance(oldAngle, newAngle) < M_PI / 6) { - newAngle = (1.0 - alpha) * oldAngle + alpha * newAngle; - } else { - newAngle = oldAngle; - } - } - - d->currentDistanceInfo->setLockedDrawingAngle(newAngle); + // If the start and end positions are the same, we can't compute an angle. In that case, use the + // provided default. + return KisAlgebra2D::directionBetweenPoints(d->directionHistoryInfo->lastPosition, + pos(), + d->directionHistoryInfo->lastAngle); } QPointF KisPaintInformation::drawingDirectionVector() const { - if (d->drawingAngleOverride) { - qreal angle = *d->drawingAngleOverride; - return QPointF(cos(angle), sin(angle)); - } - - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingDirectionVector()" << "Cannot access Distance Info last dab data"; - return QPointF(1.0, 0.0); - } - - return d->currentDistanceInfo->nextDrawingDirectionVector(pos()); + const qreal angle = drawingAngle(false); + return QPointF(cos(angle), sin(angle)); } qreal KisPaintInformation::drawingDistance() const { - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingDistance()" << "Cannot access Distance Info last dab data"; + if (!d->directionHistoryInfo) { + warnKrita << "KisPaintInformation::drawingDistance()" << "DirectionHistoryInfo object is not available"; return 1.0; } - QVector2D diff(pos() - d->currentDistanceInfo->lastPosition()); + QVector2D diff(pos() - d->directionHistoryInfo->lastPosition); qreal length = diff.length(); if (d->levelOfDetail) { @@ -532,7 +525,6 @@ if (posOnly) { this->d->pos = p; this->d->isHoveringMode = false; - this->d->currentDistanceInfo = 0; this->d->levelOfDetail = 0; return; } @@ -560,7 +552,6 @@ *(this->d) = Private(p, pressure, xTilt, yTilt, rotation, tangentialPressure, perspective, time, speed, other.isHoveringMode()); this->d->randomSource = other.d->randomSource; // this->d->isHoveringMode = other.isHoveringMode(); - this->d->currentDistanceInfo = 0; this->d->levelOfDetail = other.d->levelOfDetail; } } diff --git a/libs/image/brushengine/kis_paintop.h b/libs/image/brushengine/kis_paintop.h --- a/libs/image/brushengine/kis_paintop.h +++ b/libs/image/brushengine/kis_paintop.h @@ -34,6 +34,7 @@ class KisPainter; class KisPaintInformation; +class KisRunnableStrokeJobData; /** * KisPaintOp are use by tools to draw on a paint device. A paintop takes settings @@ -111,6 +112,15 @@ */ static void splitCoordinate(qreal coordinate, qint32 *whole, qreal *fraction); + /** + * If the preset supports asynchronous updates, then the stroke execution core will + * call this method with a desured frame rate. The jobs that should be run to prepare the update + * are returned via \p jobs + * + * @return the desired FPS rate (period of updates) + */ + virtual int doAsyncronousUpdate(QVector &jobs); + protected: friend class KisPaintInformation; /** diff --git a/libs/image/brushengine/kis_paintop.cc b/libs/image/brushengine/kis_paintop.cc --- a/libs/image/brushengine/kis_paintop.cc +++ b/libs/image/brushengine/kis_paintop.cc @@ -103,6 +103,12 @@ *fraction = f; } +int KisPaintOp::doAsyncronousUpdate(QVector &jobs) +{ + Q_UNUSED(jobs); + return 40; +} + static void paintBezierCurve(KisPaintOp *paintOp, const KisPaintInformation &pi1, const KisVector2D &control1, diff --git a/libs/image/brushengine/kis_paintop_settings.h b/libs/image/brushengine/kis_paintop_settings.h --- a/libs/image/brushengine/kis_paintop_settings.h +++ b/libs/image/brushengine/kis_paintop_settings.h @@ -152,6 +152,12 @@ virtual bool useSpacingUpdates() const; /** + * Indicates if the tool should call paintOp->doAsynchronousUpdate() inbetween + * paintAt() calls to do the asynchronous rendering + */ + virtual bool needsAsynchronousUpdates() const; + + /** * This enum defines the current mode for painting an outline. */ enum OutlineMode { diff --git a/libs/image/brushengine/kis_paintop_settings.cpp b/libs/image/brushengine/kis_paintop_settings.cpp --- a/libs/image/brushengine/kis_paintop_settings.cpp +++ b/libs/image/brushengine/kis_paintop_settings.cpp @@ -332,6 +332,11 @@ return getBool(SPACING_USE_UPDATES, false); } +bool KisPaintOpSettings::needsAsynchronousUpdates() const +{ + return false; +} + QPainterPath KisPaintOpSettings::brushOutline(const KisPaintInformation &info, OutlineMode mode) { QPainterPath path; diff --git a/libs/image/brushengine/kis_paintop_utils.h b/libs/image/brushengine/kis_paintop_utils.h --- a/libs/image/brushengine/kis_paintop_utils.h +++ b/libs/image/brushengine/kis_paintop_utils.h @@ -25,6 +25,10 @@ #include "kis_spacing_information.h" #include "kis_timing_information.h" +#include "kritaimage_export.h" + +class KisRenderedDab; + namespace KisPaintOpUtils { template @@ -118,7 +122,7 @@ * this the class stores two previosly requested points instead of the * last one. */ -class PositionHistory +class KRITAIMAGE_EXPORT PositionHistory { public: /** @@ -152,7 +156,7 @@ QPointF m_second; }; -bool checkSizeTooSmall(qreal scale, qreal width, qreal height) +inline bool checkSizeTooSmall(qreal scale, qreal width, qreal height) { return scale * width < 0.01 || scale * height < 0.01; } @@ -162,14 +166,15 @@ return coeff * (value < 1.0 ? value : sqrt(value)); } -QPointF calcAutoSpacing(const QPointF &pt, qreal coeff, qreal lodScale) +inline QPointF calcAutoSpacing(const QPointF &pt, qreal coeff, qreal lodScale) { const qreal invLodScale = 1.0 / lodScale; const QPointF lod0Point = invLodScale * pt; return lodScale * QPointF(calcAutoSpacing(lod0Point.x(), coeff), calcAutoSpacing(lod0Point.y(), coeff)); } +KRITAIMAGE_EXPORT KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, @@ -180,48 +185,18 @@ qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, - qreal lodScale) -{ - QPointF spacing; - - if (!isotropicSpacing) { - if (autoSpacingActive) { - spacing = calcAutoSpacing(QPointF(dabWidth, dabHeight), autoSpacingCoeff, lodScale); - } else { - spacing = QPointF(dabWidth, dabHeight); - spacing *= spacingVal; - } - } - else { - qreal significantDimension = qMax(dabWidth, dabHeight); - if (autoSpacingActive) { - significantDimension = calcAutoSpacing(significantDimension, autoSpacingCoeff); - } else { - significantDimension *= spacingVal; - } - spacing = QPointF(significantDimension, significantDimension); - rotation = 0.0; - axesFlipped = false; - } - - spacing *= extraScale; - - return KisSpacingInformation(distanceSpacingEnabled, spacing, rotation, axesFlipped); -} + qreal lodScale); +KRITAIMAGE_EXPORT KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, - qreal rateExtraScale) -{ + qreal rateExtraScale); - if (!timingEnabled) { - return KisTimingInformation(); - } - else { - qreal scaledInterval = rateExtraScale <= 0.0 ? LONG_TIME : timingInterval / rateExtraScale; - return KisTimingInformation(scaledInterval); - } -} +KRITAIMAGE_EXPORT +QVector splitAndFilterDabRect(const QRect &totalRect, const QList &dabs, int idealPatchSize); + +KRITAIMAGE_EXPORT +QVector splitDabsIntoRects(const QList &dabs, int idealNumRects, int diameter, qreal spacing); } diff --git a/libs/image/brushengine/kis_paintop_utils.cpp b/libs/image/brushengine/kis_paintop_utils.cpp new file mode 100644 --- /dev/null +++ b/libs/image/brushengine/kis_paintop_utils.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2017 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_paintop_utils.h" + +#include "krita_utils.h" +#include "krita_container_utils.h" +#include + +namespace KisPaintOpUtils { + + +KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, bool distanceSpacingEnabled, bool isotropicSpacing, qreal rotation, bool axesFlipped, qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, qreal lodScale) +{ + QPointF spacing; + + if (!isotropicSpacing) { + if (autoSpacingActive) { + spacing = calcAutoSpacing(QPointF(dabWidth, dabHeight), autoSpacingCoeff, lodScale); + } else { + spacing = QPointF(dabWidth, dabHeight); + spacing *= spacingVal; + } + } + else { + qreal significantDimension = qMax(dabWidth, dabHeight); + if (autoSpacingActive) { + significantDimension = calcAutoSpacing(significantDimension, autoSpacingCoeff); + } else { + significantDimension *= spacingVal; + } + spacing = QPointF(significantDimension, significantDimension); + rotation = 0.0; + axesFlipped = false; + } + + spacing *= extraScale; + + return KisSpacingInformation(distanceSpacingEnabled, spacing, rotation, axesFlipped); +} + +KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, qreal rateExtraScale) +{ + + if (!timingEnabled) { + return KisTimingInformation(); + } + else { + qreal scaledInterval = rateExtraScale <= 0.0 ? LONG_TIME : timingInterval / rateExtraScale; + return KisTimingInformation(scaledInterval); + } +} + +QVector splitAndFilterDabRect(const QRect &totalRect, const QList &dabs, int idealPatchSize) +{ + QVector rects = KritaUtils::splitRectIntoPatches(totalRect, QSize(idealPatchSize,idealPatchSize)); + + KritaUtils::filterContainer(rects, + [dabs] (const QRect &rc) { + Q_FOREACH (const KisRenderedDab &dab, dabs) { + if (dab.realBounds().intersects(rc)) { + return true; + } + } + return false; + }); + return rects; +} + +QVector splitDabsIntoRects(const QList &dabs, int idealNumRects, int diameter, qreal spacing) +{ + QRect totalRect; + + Q_FOREACH (const KisRenderedDab &dab, dabs) { + const QRect rc = dab.realBounds(); + totalRect |= rc; + } + + constexpr int minPatchSize = 128; + constexpr int maxPatchSize = 512; + constexpr int patchStep = 64; + constexpr int halfPatchStep= patchStep >> 1; + + + int idealPatchSize = qBound(minPatchSize, + (int(diameter * (2.0 - spacing)) + halfPatchStep) & ~(patchStep - 1), + maxPatchSize); + + + QVector rects = splitAndFilterDabRect(totalRect, dabs, idealPatchSize); + + while (rects.size() < idealNumRects && idealPatchSize >minPatchSize) { + idealPatchSize = qMax(minPatchSize, idealPatchSize - patchStep); + rects = splitAndFilterDabRect(totalRect, dabs, idealPatchSize); + } + + return rects; +} + + + +} diff --git a/libs/image/generator/kis_generator_layer.h b/libs/image/generator/kis_generator_layer.h --- a/libs/image/generator/kis_generator_layer.h +++ b/libs/image/generator/kis_generator_layer.h @@ -68,7 +68,7 @@ void update(); using KisSelectionBasedLayer::setDirty; - void setDirty(const QRect & rect) override; + void setDirty(const QVector &rects) override; void setX(qint32 x) override; void setY(qint32 y) override; diff --git a/libs/image/generator/kis_generator_layer.cpp b/libs/image/generator/kis_generator_layer.cpp --- a/libs/image/generator/kis_generator_layer.cpp +++ b/libs/image/generator/kis_generator_layer.cpp @@ -159,9 +159,9 @@ m_d->updateSignalCompressor.start(); } -void KisGeneratorLayer::setDirty(const QRect & rect) +void KisGeneratorLayer::setDirty(const QVector &rects) { - KisSelectionBasedLayer::setDirty(rect); + KisSelectionBasedLayer::setDirty(rects); m_d->updateSignalCompressor.start(); } diff --git a/libs/image/kis_brush_mask_applicators.h b/libs/image/kis_brush_mask_applicators.h --- a/libs/image/kis_brush_mask_applicators.h +++ b/libs/image/kis_brush_mask_applicators.h @@ -22,20 +22,11 @@ #include "kis_brush_mask_applicator_base.h" #include "kis_global.h" +#include "kis_random_source.h" // 3x3 supersampling #define SUPERSAMPLING 3 -#if defined(_WIN32) || defined(_WIN64) -#include -#define srand48 srand -inline double drand48() { - return double(rand()) / RAND_MAX; -} -#endif - -#include - template struct KisBrushMaskScalarApplicator : public KisBrushMaskApplicatorBase @@ -54,7 +45,7 @@ protected: MaskGenerator *m_maskGenerator; - std::random_device m_rand_dev; + KisRandomSource m_randomSource; // TODO: make it more deterministic for LoD }; #if defined HAVE_VC @@ -128,16 +119,16 @@ for (int x = 0; x < width; x++) { if (m_d->randomness!= 0.0){ - random = (1.0 - m_d->randomness) + m_d->randomness * float(rand()) / RAND_MAX; + random = (1.0 - m_d->randomness) + m_d->randomness * KisBrushMaskScalarApplicator::m_randomSource.generateNormalized(); } alphaValue = quint8( (OPACITY_OPAQUE_U8 - buffer[x]*255) * random); // avoid computation of random numbers if density is full if (m_d->density != 1.0){ // compute density only for visible pixels of the mask if (alphaValue != OPACITY_TRANSPARENT_U8){ - if ( !(m_d->density >= drand48()) ){ + if ( !(m_d->density >= KisBrushMaskScalarApplicator::m_randomSource.generateNormalized()) ){ alphaValue = OPACITY_TRANSPARENT_U8; } } @@ -163,9 +154,6 @@ const MaskProcessingData *m_d = KisBrushMaskApplicatorBase::m_d; MaskGenerator *m_maskGenerator = KisBrushMaskScalarApplicator::m_maskGenerator; - std::default_random_engine rand_engine{m_rand_dev()}; - std::uniform_real_distribution<> rand_distr(0.0f, 1.0f); - qreal random = 1.0; quint8* dabPointer = m_d->device->data() + rect.y() * rect.width() * m_d->pixelSize; quint8 alphaValue = OPACITY_TRANSPARENT_U8; @@ -189,16 +177,16 @@ if (supersample != 1) value /= samplearea; if (m_d->randomness!= 0.0){ - random = (1.0 - m_d->randomness) + m_d->randomness * rand_distr(rand_engine); + random = (1.0 - m_d->randomness) + m_d->randomness * m_randomSource.generateNormalized(); } alphaValue = quint8( (OPACITY_OPAQUE_U8 - value) * random); // avoid computation of random numbers if density is full if (m_d->density != 1.0){ // compute density only for visible pixels of the mask if (alphaValue != OPACITY_TRANSPARENT_U8){ - if ( !(m_d->density >= rand_distr(rand_engine)) ){ + if ( !(m_d->density >= m_randomSource.generateNormalized()) ){ alphaValue = OPACITY_TRANSPARENT_U8; } } diff --git a/libs/image/kis_distance_information.h b/libs/image/kis_distance_information.h --- a/libs/image/kis_distance_information.h +++ b/libs/image/kis_distance_information.h @@ -25,6 +25,7 @@ #include #include #include "kritaimage_export.h" +#include class KisPaintInformation; class KisSpacingInformation; @@ -56,13 +57,13 @@ * Creates a KisDistanceInitInfo with the specified last dab information, and spacing and timing * update intervals set to LONG_TIME. */ - explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, qreal lastAngle); + explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle); /** * Creates a KisDistanceInitInfo with the specified last dab information and spacing and timing * update intervals. */ - explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, qreal lastAngle, + explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle, qreal spacingUpdateInterval, qreal timingUpdateInterval); KisDistanceInitInfo(const KisDistanceInitInfo &rhs); @@ -97,16 +98,16 @@ public: KisDistanceInformation(); KisDistanceInformation(qreal spacingUpdateInterval, qreal timingUpdateInterval); - KisDistanceInformation(const QPointF &lastPosition, qreal lastTime, qreal lastAngle); + KisDistanceInformation(const QPointF &lastPosition, qreal lastAngle); /** * @param spacingUpdateInterval The amount of time allowed between spacing updates, in * milliseconds. Use LONG_TIME to only allow spacing updates when a * dab is painted. * @param timingUpdateInterval The amount of time allowed between time-based spacing updates, in * milliseconds. Use LONG_TIME to only allow timing updates when a * dab is painted. */ - KisDistanceInformation(const QPointF &lastPosition, qreal lastTime, qreal lastAngle, + KisDistanceInformation(const QPointF &lastPosition, qreal lastAngle, qreal spacingUpdateInterval, qreal timingUpdateInterval); KisDistanceInformation(const KisDistanceInformation &rhs); KisDistanceInformation(const KisDistanceInformation &rhs, int levelOfDetail); @@ -132,7 +133,6 @@ bool hasLastDabInformation() const; QPointF lastPosition() const; - qreal lastTime() const; qreal lastDrawingAngle() const; bool hasLastPaintInformation() const; @@ -159,26 +159,17 @@ */ bool isStarted() const; - bool hasLockedDrawingAngle() const; - qreal lockedDrawingAngle() const; - void setLockedDrawingAngle(qreal angle); + boost::optional lockedDrawingAngleOptional() const; /** - * Computes the next drawing angle assuming that the next painting position will be nextPos. - * This method should not be called when hasLastDabInformation() is false. + * Lock current drawing angle for the rest of the stroke. The new value is blended + * into the result proportional to the length of the stroke. */ - qreal nextDrawingAngle(const QPointF &nextPos, bool considerLockedAngle = true) const; - - /** - * Returns a unit vector pointing in the direction that would have been indicated by a call to - * nextDrawingAngle. This method should not be called when hasLastDabInformation() is false. - */ - QPointF nextDrawingDirectionVector(const QPointF &nextPos, - bool considerLockedAngle = true) const; + void lockCurrentDrawingAngle(const KisPaintInformation &info) const; qreal scalarDistanceApprox() const; - void overrideLastValues(const QPointF &lastPosition, qreal lastTime, qreal lastAngle); + void overrideLastValues(const QPointF &lastPosition, qreal lastAngle); private: qreal getNextPointPositionIsotropic(const QPointF &start, @@ -189,12 +180,6 @@ qreal endTime); void resetAccumulators(); - qreal drawingAngleImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle = true, qreal defaultAngle = 0.0) const; - QPointF drawingDirectionVectorImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle = true, - qreal defaultAngle = 0.0) const; - private: struct Private; Private * const m_d; diff --git a/libs/image/kis_distance_information.cpp b/libs/image/kis_distance_information.cpp --- a/libs/image/kis_distance_information.cpp +++ b/libs/image/kis_distance_information.cpp @@ -48,8 +48,6 @@ timeSinceTimingUpdate(0.0), lastDabInfoValid(false), lastPaintInfoValid(false), - lockedDrawingAngle(0.0), - hasLockedDrawingAngle(false), totalDistance(0.0) {} // Accumulators of time/distance passed since the last painted dab @@ -68,34 +66,30 @@ // Information about the last position considered (not necessarily a painted dab) QPointF lastPosition; - qreal lastTime; qreal lastAngle; bool lastDabInfoValid; // Information about the last painted dab KisPaintInformation lastPaintInformation; bool lastPaintInfoValid; - qreal lockedDrawingAngle; - bool hasLockedDrawingAngle; qreal totalDistance; + boost::optional lockedDrawingAngleOptional; }; struct Q_DECL_HIDDEN KisDistanceInitInfo::Private { Private() : hasLastInfo(false), lastPosition(), - lastTime(0.0), lastAngle(0.0), spacingUpdateInterval(LONG_TIME), timingUpdateInterval(LONG_TIME) {} - // Indicates whether lastPosition, lastTime, and lastAngle are valid or not. + // Indicates whether lastPosition, and lastAngle are valid or not. bool hasLastInfo; QPointF lastPosition; - qreal lastTime; qreal lastAngle; qreal spacingUpdateInterval; @@ -114,24 +108,22 @@ m_d->timingUpdateInterval = timingUpdateInterval; } -KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, +KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle) : m_d(new Private) { m_d->hasLastInfo = true; m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; } -KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, +KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle, qreal spacingUpdateInterval, qreal timingUpdateInterval) : m_d(new Private) { m_d->hasLastInfo = true; m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; @@ -156,7 +148,7 @@ return false; } if (m_d->hasLastInfo) { - if (m_d->lastPosition != other.m_d->lastPosition || m_d->lastTime != other.m_d->lastTime + if (m_d->lastPosition != other.m_d->lastPosition || m_d->lastAngle != other.m_d->lastAngle) { return false; @@ -180,7 +172,7 @@ KisDistanceInformation KisDistanceInitInfo::makeDistInfo() { if (m_d->hasLastInfo) { - return KisDistanceInformation(m_d->lastPosition, m_d->lastTime, m_d->lastAngle, + return KisDistanceInformation(m_d->lastPosition, m_d->lastAngle, m_d->spacingUpdateInterval, m_d->timingUpdateInterval); } else { @@ -196,7 +188,6 @@ QDomElement lastInfoElt = doc.createElement("LastInfo"); lastInfoElt.setAttribute("lastPosX", QString::number(m_d->lastPosition.x(), 'g', 15)); lastInfoElt.setAttribute("lastPosY", QString::number(m_d->lastPosition.y(), 'g', 15)); - lastInfoElt.setAttribute("lastTime", QString::number(m_d->lastTime, 'g', 15)); lastInfoElt.setAttribute("lastAngle", QString::number(m_d->lastAngle, 'g', 15)); elt.appendChild(lastInfoElt); } @@ -216,12 +207,10 @@ "0.0"))); const qreal lastPosY = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosY", "0.0"))); - const qreal lastTime = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastTime", - "0.0"))); const qreal lastAngle = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastAngle", "0.0"))); - return KisDistanceInitInfo(QPointF(lastPosX, lastPosY), lastTime, lastAngle, + return KisDistanceInitInfo(QPointF(lastPosX, lastPosY), lastAngle, spacingUpdateInterval, timingUpdateInterval); } else { @@ -243,23 +232,20 @@ } KisDistanceInformation::KisDistanceInformation(const QPointF &lastPosition, - qreal lastTime, qreal lastAngle) : m_d(new Private) { m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->lastDabInfoValid = true; } KisDistanceInformation::KisDistanceInformation(const QPointF &lastPosition, - qreal lastTime, qreal lastAngle, qreal spacingUpdateInterval, qreal timingUpdateInterval) - : KisDistanceInformation(lastPosition, lastTime, lastAngle) + : KisDistanceInformation(lastPosition, lastAngle) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; @@ -289,11 +275,10 @@ return *this; } -void KisDistanceInformation::overrideLastValues(const QPointF &lastPosition, qreal lastTime, +void KisDistanceInformation::overrideLastValues(const QPointF &lastPosition, qreal lastAngle) { m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->lastDabInfoValid = true; @@ -346,11 +331,6 @@ return m_d->lastPosition; } -qreal KisDistanceInformation::lastTime() const -{ - return m_d->lastTime; -} - qreal KisDistanceInformation::lastDrawingAngle() const { return m_d->lastAngle; @@ -380,9 +360,8 @@ m_d->lastPaintInformation = info; m_d->lastPaintInfoValid = true; - m_d->lastAngle = nextDrawingAngle(info.pos()); + m_d->lastAngle = info.drawingAngle(false); m_d->lastPosition = info.pos(); - m_d->lastTime = info.currentTime(); m_d->lastDabInfoValid = true; m_d->spacing = spacing; @@ -569,81 +548,37 @@ m_d->accumTime = 0.0; } -bool KisDistanceInformation::hasLockedDrawingAngle() const +boost::optional KisDistanceInformation::lockedDrawingAngleOptional() const { - return m_d->hasLockedDrawingAngle; + return m_d->lockedDrawingAngleOptional; } -qreal KisDistanceInformation::lockedDrawingAngle() const +void KisDistanceInformation::lockCurrentDrawingAngle(const KisPaintInformation &info) const { - return m_d->lockedDrawingAngle; -} + const qreal angle = info.drawingAngle(false); -void KisDistanceInformation::setLockedDrawingAngle(qreal angle) -{ - m_d->hasLockedDrawingAngle = true; - m_d->lockedDrawingAngle = angle; -} + qreal newAngle = angle; -qreal KisDistanceInformation::nextDrawingAngle(const QPointF &nextPos, - bool considerLockedAngle) const -{ - if (!m_d->lastDabInfoValid) { - warnKrita << "KisDistanceInformation::nextDrawingAngle()" << "No last dab data"; - return 0.0; - } + if (m_d->lockedDrawingAngleOptional) { + const qreal stabilizingCoeff = 20.0; + const qreal dist = stabilizingCoeff * m_d->spacing.scalarApprox(); + const qreal alpha = qMax(0.0, dist - scalarDistanceApprox()) / dist; - // Compute the drawing angle. If the new position is the same as the previous position, an angle - // can't be computed. In that case, act as if the angle is the same as in the previous dab. - return drawingAngleImpl(m_d->lastPosition, nextPos, considerLockedAngle, m_d->lastAngle); -} + const qreal oldAngle = *m_d->lockedDrawingAngleOptional; -QPointF KisDistanceInformation::nextDrawingDirectionVector(const QPointF &nextPos, - bool considerLockedAngle) const -{ - if (!m_d->lastDabInfoValid) { - warnKrita << "KisDistanceInformation::nextDrawingDirectionVector()" << "No last dab data"; - return QPointF(1.0, 0.0); + if (shortestAngularDistance(oldAngle, newAngle) < M_PI / 6) { + newAngle = (1.0 - alpha) * oldAngle + alpha * newAngle; + } else { + newAngle = oldAngle; + } } - // Compute the direction vector. If the new position is the same as the previous position, a - // direction can't be computed. In that case, act as if the direction is the same as in the - // previous dab. - return drawingDirectionVectorImpl(m_d->lastPosition, nextPos, considerLockedAngle, - m_d->lastAngle); + m_d->lockedDrawingAngleOptional = newAngle; } + qreal KisDistanceInformation::scalarDistanceApprox() const { return m_d->totalDistance; } -qreal KisDistanceInformation::drawingAngleImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle, qreal defaultAngle) const -{ - if (m_d->hasLockedDrawingAngle && considerLockedAngle) { - return m_d->lockedDrawingAngle; - } - - // If the start and end positions are the same, we can't compute an angle. In that case, use the - // provided default. - return KisAlgebra2D::directionBetweenPoints(start, end, defaultAngle); -} - -QPointF KisDistanceInformation::drawingDirectionVectorImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle, - qreal defaultAngle) const -{ - if (m_d->hasLockedDrawingAngle && considerLockedAngle) { - return QPointF(cos(m_d->lockedDrawingAngle), sin(m_d->lockedDrawingAngle)); - } - - // If the start and end positions are the same, we can't compute a drawing direction. In that - // case, use the provided default. - if (KisAlgebra2D::fuzzyPointCompare(start, end)) { - return QPointF(cos(defaultAngle), sin(defaultAngle)); - } - - const QPointF diff(end - start); - return KisAlgebra2D::normalize(diff); -} diff --git a/libs/image/kis_fixed_paint_device.h b/libs/image/kis_fixed_paint_device.h --- a/libs/image/kis_fixed_paint_device.h +++ b/libs/image/kis_fixed_paint_device.h @@ -93,10 +93,25 @@ bool initialize(quint8 defaultValue = 0); /** + * Changed the size of the internal buffer to accomodate the exact number of bytes + * needed to store area bounds(). The allocated data is *not* initialized! + */ + void reallocateBufferWithoutInitialization(); + + /** + * If the size of the internal buffer is smller than the one needed to accomodate + * bounds(), resize the buffer. Otherwise, do nothing. The allocated data is neither + * copying or initialized! + */ + void lazyGrowBufferWithoutInitialization(); + + /** * @return a pointer to the beginning of the data associated with this fixed paint device. */ quint8* data(); + const quint8* constData() const; + quint8* data() const; /** @@ -180,7 +195,7 @@ const KoColorSpace* m_colorSpace; QRect m_bounds; - QVector m_data; + QByteArray m_data; }; diff --git a/libs/image/kis_fixed_paint_device.cpp b/libs/image/kis_fixed_paint_device.cpp --- a/libs/image/kis_fixed_paint_device.cpp +++ b/libs/image/kis_fixed_paint_device.cpp @@ -45,7 +45,16 @@ { m_bounds = rhs.m_bounds; m_colorSpace = rhs.m_colorSpace; - m_data = rhs.m_data; + + + const int referenceSize = m_bounds.height() * m_bounds.width() * pixelSize(); + + if (m_data.size() >= referenceSize) { + memcpy(m_data.data(), rhs.m_data.data(), referenceSize); + } else { + m_data = rhs.m_data; + } + return *this; } @@ -79,14 +88,37 @@ return true; } +void KisFixedPaintDevice::reallocateBufferWithoutInitialization() +{ + const int referenceSize = m_bounds.height() * m_bounds.width() * pixelSize(); + + if (referenceSize != m_data.size()) { + m_data.resize(m_bounds.height() * m_bounds.width() * pixelSize()); + } +} + +void KisFixedPaintDevice::lazyGrowBufferWithoutInitialization() +{ + const int referenceSize = m_bounds.height() * m_bounds.width() * pixelSize(); + + if (m_data.size() < referenceSize) { + m_data.resize(referenceSize); + } +} + quint8* KisFixedPaintDevice::data() { - return m_data.data(); + return (quint8*) m_data.data(); +} + +const quint8 *KisFixedPaintDevice::constData() const +{ + return (const quint8*) m_data.constData(); } quint8* KisFixedPaintDevice::data() const { - return const_cast(m_data.data()); + return const_cast((quint8*)m_data.data()); } void KisFixedPaintDevice::convertTo(const KoColorSpace* dstColorSpace, @@ -97,17 +129,19 @@ return; } quint32 size = m_bounds.width() * m_bounds.height(); - QVector dstData(size * dstColorSpace->pixelSize()); + QByteArray dstData; + + // make sure that we are not initializing the destination pixels! + dstData.resize(size * dstColorSpace->pixelSize()); - m_colorSpace->convertPixelsTo(data(), dstData.data(), + m_colorSpace->convertPixelsTo(constData(), (quint8*)dstData.data(), dstColorSpace, size, renderingIntent, conversionFlags); m_colorSpace = dstColorSpace; m_data = dstData; - } void KisFixedPaintDevice::convertFromQImage(const QImage& _image, const QString &srcProfileName) @@ -118,7 +152,7 @@ image = image.convertToFormat(QImage::Format_ARGB32); } setRect(image.rect()); - initialize(); + lazyGrowBufferWithoutInitialization(); // Don't convert if not no profile is given and both paint dev and qimage are rgba. if (srcProfileName.isEmpty() && colorSpace()->id() == "RGBA") { @@ -158,15 +192,15 @@ return QImage(); if (QRect(x1, y1, w, h) == m_bounds) { - return colorSpace()->convertToQImage(data(), w, h, dstProfile, + return colorSpace()->convertToQImage(constData(), w, h, dstProfile, intent, conversionFlags); } else { try { // XXX: fill the image row by row! - int pSize = pixelSize(); - int deviceWidth = m_bounds.width(); + const int pSize = pixelSize(); + const int deviceWidth = m_bounds.width(); quint8* newData = new quint8[w * h * pSize]; - quint8* srcPtr = data() + x1 * pSize + y1 * deviceWidth * pSize; + const quint8* srcPtr = constData() + x1 * pSize + y1 * deviceWidth * pSize; quint8* dstPtr = newData; // copy the right area out of the paint device into data for (int row = 0; row < h; row++) { @@ -204,7 +238,7 @@ { if (m_data.isEmpty() || m_bounds.isEmpty()) { setRect(QRect(x, y, w, h)); - initialize(); + reallocateBufferWithoutInitialization(); } QRect rc(x, y, w, h); @@ -245,18 +279,16 @@ return; } - quint8 pixelSize = m_colorSpace->pixelSize(); - quint8* dabPointer = data(); + const int pixelSize = m_colorSpace->pixelSize(); + const quint8* dabPointer = constData(); if (rc == m_bounds) { - memcpy(dstData, dabPointer, pixelSize * w * h); - } - else - { - int deviceWidth = bounds().width(); - quint8* rowPointer = dabPointer + ((y - bounds().y()) * deviceWidth + (x - bounds().x())) * pixelSize; + memcpy(dstData, dabPointer, pixelSize * w * h); + } else { + int deviceWidth = m_bounds.width(); + const quint8* rowPointer = dabPointer + ((y - bounds().y()) * deviceWidth + (x - bounds().x())) * pixelSize; for (int row = 0; row < h; row++) { - memcpy(dstData,rowPointer, w * pixelSize); + memcpy(dstData, rowPointer, w * pixelSize); rowPointer += deviceWidth * pixelSize; dstData += w * pixelSize; } @@ -281,6 +313,8 @@ quint8 * mirror = 0; for (int y = 0; y < h ; y++){ + // TODO: implement better flipping of the data + memcpy(row, dabPointer, rowSize); mirror = row; mirror += (w-1) * pixelSize; diff --git a/libs/image/kis_image.h b/libs/image/kis_image.h --- a/libs/image/kis_image.h +++ b/libs/image/kis_image.h @@ -92,7 +92,7 @@ void nodeChanged(KisNode * node) override; void invalidateAllFrames() override; void notifySelectionChanged() override; - void requestProjectionUpdate(KisNode *node, const QRect& rect, bool resetAnimationCache) override; + void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override; void invalidateFrames(const KisTimeRange &range, const QRect &rect) override; void requestTimeSwitch(int time) override; @@ -951,7 +951,7 @@ void refreshHiddenArea(KisNodeSP rootNode, const QRect &preparedArea); void requestProjectionUpdateImpl(KisNode *node, - const QRect& rect, + const QVector &rects, const QRect &cropRect); friend class KisImageResizeCommand; diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc --- a/libs/image/kis_image.cc +++ b/libs/image/kis_image.cc @@ -1524,24 +1524,24 @@ } void KisImage::requestProjectionUpdateImpl(KisNode *node, - const QRect &rect, + const QVector &rects, const QRect &cropRect) { - if (rect.isEmpty()) return; + if (rects.isEmpty()) return; - m_d->scheduler.updateProjection(node, rect, cropRect); + m_d->scheduler.updateProjection(node, rects, cropRect); } -void KisImage::requestProjectionUpdate(KisNode *node, const QRect& rect, bool resetAnimationCache) +void KisImage::requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) { if (m_d->projectionUpdatesFilter - && m_d->projectionUpdatesFilter->filter(this, node, rect, resetAnimationCache)) { + && m_d->projectionUpdatesFilter->filter(this, node, rects, resetAnimationCache)) { return; } if (resetAnimationCache) { - m_d->animationInterface->notifyNodeChanged(node, rect, false); + m_d->animationInterface->notifyNodeChanged(node, rects, false); } /** @@ -1551,17 +1551,21 @@ * supporting the wrap-around mode will not make much harm. */ if (m_d->wrapAroundModePermitted) { - const QRect boundRect = effectiveLodBounds(); - KisWrappedRect splitRect(rect, boundRect); + QVector allSplitRects; - Q_FOREACH (const QRect &rc, splitRect) { - requestProjectionUpdateImpl(node, rc, boundRect); + const QRect boundRect = effectiveLodBounds(); + Q_FOREACH (const QRect &rc, rects) { + KisWrappedRect splitRect(rc, boundRect); + allSplitRects.append(splitRect); } + + requestProjectionUpdateImpl(node, allSplitRects, boundRect); + } else { - requestProjectionUpdateImpl(node, rect, bounds()); + requestProjectionUpdateImpl(node, rects, bounds()); } - KisNodeGraphListener::requestProjectionUpdate(node, rect, resetAnimationCache); + KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); } void KisImage::invalidateFrames(const KisTimeRange &range, const QRect &rect) diff --git a/libs/image/kis_image_animation_interface.h b/libs/image/kis_image_animation_interface.h --- a/libs/image/kis_image_animation_interface.h +++ b/libs/image/kis_image_animation_interface.h @@ -100,7 +100,9 @@ */ void requestFrameRegeneration(int frameId, const QRegion &dirtyRegion); + void notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive); + void notifyNodeChanged(const KisNode *node, const QVector &rects, bool recursive); void invalidateFrames(const KisTimeRange &range, const QRect &rect); /** diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp --- a/libs/image/kis_image_animation_interface.cpp +++ b/libs/image/kis_image_animation_interface.cpp @@ -335,24 +335,38 @@ const QRect &rect, bool recursive) { + notifyNodeChanged(node, QVector({rect}), recursive); +} + +void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, + const QVector &rects, + bool recursive) +{ if (externalFrameActive() || m_d->frameInvalidationBlocked) return; if (node->inherits("KisSelectionMask")) return; KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); - if (recursive) { - KisTimeRange affectedRange; - KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), affectedRange, false); + KisTimeRange invalidateRange; - invalidateFrames(affectedRange, rect); + if (recursive) { + KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), invalidateRange, false); } else if (channel) { const int currentTime = m_d->currentTime(); - - invalidateFrames(channel->affectedFrames(currentTime), rect); + invalidateRange = channel->affectedFrames(currentTime); } else { - invalidateFrames(KisTimeRange::infinite(0), rect); + invalidateRange = KisTimeRange::infinite(0); + } + + + // we compress the updated rect (atm, noone uses it anyway) + QRect unitedRect; + Q_FOREACH (const QRect &rc, rects) { + unitedRect |= rc; } + + invalidateFrames(invalidateRange, unitedRect); } void KisImageAnimationInterface::invalidateFrames(const KisTimeRange &range, const QRect &rect) diff --git a/libs/image/kis_node.h b/libs/image/kis_node.h --- a/libs/image/kis_node.h +++ b/libs/image/kis_node.h @@ -114,7 +114,7 @@ * this percolates up to parent nodes all the way to the root * node. */ - virtual void setDirty(const QRect & rect); + void setDirty(const QRect & rect); /** * Add the given rects to the set of dirty rects for this node; @@ -128,7 +128,7 @@ * this percolates up to parent nodes all the way to the root * node, if propagate is true; */ - virtual void setDirty(const QRegion ®ion); + void setDirty(const QRegion ®ion); /** * @brief setDirtyDontResetAnimationCache does almost the same thing as usual diff --git a/libs/image/kis_node.cpp b/libs/image/kis_node.cpp --- a/libs/image/kis_node.cpp +++ b/libs/image/kis_node.cpp @@ -587,8 +587,8 @@ void KisNode::setDirty(const QVector &rects) { - Q_FOREACH (const QRect &rc, rects) { - setDirty(rc); + if(m_d->graphListener) { + m_d->graphListener->requestProjectionUpdate(this, rects, true); } } @@ -600,15 +600,13 @@ void KisNode::setDirtyDontResetAnimationCache() { if(m_d->graphListener) { - m_d->graphListener->requestProjectionUpdate(this, extent(), false); + m_d->graphListener->requestProjectionUpdate(this, {extent()}, false); } } void KisNode::setDirty(const QRect & rect) { - if(m_d->graphListener) { - m_d->graphListener->requestProjectionUpdate(this, rect, true); - } + setDirty(QVector({rect})); } void KisNode::invalidateFrames(const KisTimeRange &range, const QRect &rect) diff --git a/libs/image/kis_node_graph_listener.h b/libs/image/kis_node_graph_listener.h --- a/libs/image/kis_node_graph_listener.h +++ b/libs/image/kis_node_graph_listener.h @@ -95,7 +95,7 @@ /** * Inform the model that a node has been changed (setDirty) */ - virtual void requestProjectionUpdate(KisNode * node, const QRect& rect, bool resetAnimationCache); + virtual void requestProjectionUpdate(KisNode * node, const QVector &rects, bool resetAnimationCache); virtual void invalidateFrames(const KisTimeRange &range, const QRect &rect); diff --git a/libs/image/kis_node_graph_listener.cpp b/libs/image/kis_node_graph_listener.cpp --- a/libs/image/kis_node_graph_listener.cpp +++ b/libs/image/kis_node_graph_listener.cpp @@ -86,7 +86,7 @@ { } -void KisNodeGraphListener::requestProjectionUpdate(KisNode * /*node*/, const QRect& /*rect*/, bool /*resetAnimationCache*/) +void KisNodeGraphListener::requestProjectionUpdate(KisNode * /*node*/, const QVector &/*rects*/, bool /*resetAnimationCache*/) { } diff --git a/libs/image/kis_painter.h b/libs/image/kis_painter.h --- a/libs/image/kis_painter.h +++ b/libs/image/kis_painter.h @@ -53,6 +53,8 @@ class KisPaintInformation; class KisPaintOp; class KisDistanceInformation; +class KisRenderedDab; +class KisRunnableStrokeJobsInterface; /** * KisPainter contains the graphics primitives necessary to draw on a @@ -295,6 +297,14 @@ qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight); + + /** + * Render the area \p rc from \p srcDevices on the destination device. + * If \p rc doesn't cross the device's rect, then the device is not + * rendered at all. + */ + void bltFixed(const QRect &rc, const QList allSrcDevices); + /** * Convenience method that uses QPoint and QRect. * @@ -620,16 +630,33 @@ void setMirrorInformation(const QPointF &axesCenter, bool mirrorHorizontally, bool mirrorVertically); /** - * copy the mirror information to other painter - */ - void copyMirrorInformation(KisPainter * painter); - - /** * Returns whether the mirroring methods will do any * work when called */ bool hasMirroring() const; + /** + * Indicates if horizontal mirroring mode is activated + */ + bool hasHorizontalMirroring() const; + + /** + * Indicates if vertical mirroring mode is activated + */ + bool hasVerticalMirroring() const; + + /** + * Mirror \p rc in the requested \p direction around the center point defined + * in the painter. + */ + void mirrorRect(Qt::Orientation direction, QRect *rc) const; + + /** + * Mirror \p dab in the requested direction around the center point defined + * in the painter. The dab's offset is adjusted automatically. + */ + void mirrorDab(Qt::Orientation direction, KisRenderedDab *dab) const; + /// Set the current pattern void setPattern(const KoPattern * pattern); @@ -706,6 +733,16 @@ */ void setOpacityUpdateAverage(quint8 opacity); + /** + * Sets average opacity, that is used to make ALPHA_DARKEN painting look correct + */ + void setAverageOpacity(qreal averageOpacity); + + /** + * Calculate average opacity value after painting a single dab with \p opacity + */ + static qreal blendAverageOpacity(qreal opacity, qreal averageOpacity); + /// Set the opacity which is used in painting (like filling polygons) void setOpacity(quint8 opacity); @@ -764,6 +801,20 @@ */ void setColorConversionFlags(KoColorConversionTransformation::ConversionFlags conversionFlags); + /** + * Set interface for running asynchronous jobs by paintops. + * + * NOTE: the painter does *not* own the interface device. It is the responsibility + * of the caller to ensure that the interface object is alive during the lifetime + * of the painter. + */ + void setRunnableStrokeJobsInterface(KisRunnableStrokeJobsInterface *interface); + + /** + * Get the interface for running asynchronous jobs. It is used by paintops mostly. + */ + KisRunnableStrokeJobsInterface* runnableStrokeJobsInterface() const; + protected: /// Initialize, set everything to '0' or defaults void init(); diff --git a/libs/image/kis_painter.cc b/libs/image/kis_painter.cc --- a/libs/image/kis_painter.cc +++ b/libs/image/kis_painter.cc @@ -42,10 +42,6 @@ #include #include -#include -#include -#include - #include "kis_image.h" #include "filter/kis_filter.h" #include "kis_layer.h" @@ -55,9 +51,7 @@ #include "kis_vec.h" #include "kis_iterator_ng.h" #include "kis_random_accessor_ng.h" -#include "kis_paintop.h" -#include "kis_selection.h" -#include "kis_fill_painter.h" + #include "filter/kis_filter_configuration.h" #include "kis_pixel_selection.h" #include @@ -68,7 +62,7 @@ #include #include "kis_lod_transform.h" #include "kis_algebra_2d.h" - +#include "krita_utils.h" // Maximum distance from a Bezier control point to the line through the start @@ -79,61 +73,7 @@ #endif -struct Q_DECL_HIDDEN KisPainter::Private { - Private(KisPainter *_q) : q(_q) {} - Private(KisPainter *_q, const KoColorSpace *cs) - : q(_q), paintColor(cs), backgroundColor(cs) {} - - KisPainter *q; - - KisPaintDeviceSP device; - KisSelectionSP selection; - KisTransaction* transaction; - KoUpdater* progressUpdater; - - QVector dirtyRects; - KisPaintOp* paintOp; - KoColor paintColor; - KoColor backgroundColor; - KoColor customColor; - KisFilterConfigurationSP generator; - KisPaintLayer* sourceLayer; - FillStyle fillStyle; - StrokeStyle strokeStyle; - bool antiAliasPolygonFill; - const KoPattern* pattern; - QPointF duplicateOffset; - quint32 pixelSize; - const KoColorSpace* colorSpace; - KoColorProfile* profile; - const KoCompositeOp* compositeOp; - const KoAbstractGradient* gradient; - KisPaintOpPresetSP paintOpPreset; - QImage polygonMaskImage; - QPainter* maskPainter; - KisFillPainter* fillPainter; - KisPaintDeviceSP polygon; - qint32 maskImageWidth; - qint32 maskImageHeight; - QPointF axesCenter; - bool mirrorHorizontally; - bool mirrorVertically; - bool isOpacityUnit; // TODO: move into ParameterInfo - KoCompositeOp::ParameterInfo paramInfo; - KoColorConversionTransformation::Intent renderingIntent; - KoColorConversionTransformation::ConversionFlags conversionFlags; - - bool tryReduceSourceRect(const KisPaintDevice *srcDev, - QRect *srcRect, - qint32 *srcX, - qint32 *srcY, - qint32 *srcWidth, - qint32 *srcHeight, - qint32 *dstX, - qint32 *dstY); - - void fillPainterPathImpl(const QPainterPath& path, const QRect &requestedRect); -}; +#include "kis_painter_p.h" KisPainter::KisPainter() : d(new Private(this)) @@ -1129,7 +1069,7 @@ numPoints = points.count() - index; if (numPoints > 1) { - KisDistanceInformation saveDist(points[0], 0.0, + KisDistanceInformation saveDist(points[0], KisAlgebra2D::directionBetweenPoints(points[0], points[1], 0.0)); for (int i = index; i < index + numPoints - 1; i++) { paintLine(points [i], points [i + 1], &saveDist); @@ -1296,7 +1236,7 @@ if (d->strokeStyle != StrokeStyleNone) { if (points.count() > 1) { - KisDistanceInformation distance(points[0], 0.0, + KisDistanceInformation distance(points[0], KisAlgebra2D::directionBetweenPoints(points[0], points[1], 0.0)); for (int i = 0; i < points.count() - 1; i++) { @@ -2602,6 +2542,20 @@ d->paramInfo.updateOpacityAndAverage(float(opacity) / 255.0f); } +void KisPainter::setAverageOpacity(qreal averageOpacity) +{ + d->paramInfo.setOpacityAndAverage(d->paramInfo.opacity, averageOpacity); +} + +qreal KisPainter::blendAverageOpacity(qreal opacity, qreal averageOpacity) +{ + const float exponent = 0.1; + + return averageOpacity < opacity ? + opacity : + exponent * opacity + (1.0 - exponent) * (averageOpacity); +} + void KisPainter::setOpacity(quint8 opacity) { d->isOpacityUnit = opacity == OPACITY_OPAQUE_U8; @@ -2687,14 +2641,19 @@ d->mirrorVertically = mirrorVertically; } -void KisPainter::copyMirrorInformation(KisPainter* painter) +bool KisPainter::hasMirroring() const { - painter->setMirrorInformation(d->axesCenter, d->mirrorHorizontally, d->mirrorVertically); + return d->mirrorHorizontally || d->mirrorVertically; } -bool KisPainter::hasMirroring() const +bool KisPainter::hasHorizontalMirroring() const { - return d->mirrorHorizontally || d->mirrorVertically; + return d->mirrorHorizontally; +} + +bool KisPainter::hasVerticalMirroring() const +{ + return d->mirrorVertically; } void KisPainter::setMaskImageSize(qint32 width, qint32 height) @@ -2741,6 +2700,23 @@ d->conversionFlags = conversionFlags; } +void KisPainter::setRunnableStrokeJobsInterface(KisRunnableStrokeJobsInterface *interface) +{ + d->runnableStrokeJobsInterface = interface; +} + +KisRunnableStrokeJobsInterface *KisPainter::runnableStrokeJobsInterface() const +{ + if (!d->runnableStrokeJobsInterface) { + if (!d->fakeRunnableStrokeJobsInterface) { + d->fakeRunnableStrokeJobsInterface.reset(new KisFakeRunnableStrokeJobsExecutor()); + } + return d->fakeRunnableStrokeJobsInterface.data(); + } + + return d->runnableStrokeJobsInterface; +} + void KisPainter::renderMirrorMaskSafe(QRect rc, KisFixedPaintDeviceSP dab, bool preserveDab) { if (!d->mirrorHorizontally && !d->mirrorVertically) return; @@ -2769,7 +2745,7 @@ int y = rc.topLeft().y(); KisLodTransform t(d->device); - QPointF effectiveAxesCenter = t.map(d->axesCenter); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); int mirrorX = -((x+rc.width()) - effectiveAxesCenter.x()) + effectiveAxesCenter.x(); int mirrorY = -((y+rc.height()) - effectiveAxesCenter.y()) + effectiveAxesCenter.y(); @@ -2800,7 +2776,7 @@ int y = rc.topLeft().y(); KisLodTransform t(d->device); - QPointF effectiveAxesCenter = t.map(d->axesCenter); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); int mirrorX = -((x+rc.width()) - effectiveAxesCenter.x()) + effectiveAxesCenter.x(); int mirrorY = -((y+rc.height()) - effectiveAxesCenter.y()) + effectiveAxesCenter.y(); @@ -2837,7 +2813,7 @@ KisFixedPaintDeviceSP mirrorDab(new KisFixedPaintDevice(dab->colorSpace())); QRect dabRc( QPoint(0,0), QSize(rc.width(),rc.height()) ); mirrorDab->setRect(dabRc); - mirrorDab->initialize(); + mirrorDab->lazyGrowBufferWithoutInitialization(); dab->readBytes(mirrorDab->data(),rc); @@ -2851,7 +2827,7 @@ KisFixedPaintDeviceSP mirrorDab(new KisFixedPaintDevice(dab->colorSpace())); QRect dabRc( QPoint(0,0), QSize(rc.width(),rc.height()) ); mirrorDab->setRect(dabRc); - mirrorDab->initialize(); + mirrorDab->lazyGrowBufferWithoutInitialization(); dab->readBytes(mirrorDab->data(),QRect(QPoint(sx,sy),rc.size())); renderMirrorMask(rc, mirrorDab, mask); } @@ -2865,7 +2841,7 @@ int y = rc.topLeft().y(); KisLodTransform t(d->device); - QPointF effectiveAxesCenter = t.map(d->axesCenter); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); int mirrorX = -((x+rc.width()) - effectiveAxesCenter.x()) + effectiveAxesCenter.x(); int mirrorY = -((y+rc.height()) - effectiveAxesCenter.y()) + effectiveAxesCenter.y(); @@ -2913,3 +2889,18 @@ } } +void KisPainter::mirrorRect(Qt::Orientation direction, QRect *rc) const +{ + KisLodTransform t(d->device); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); + + KritaUtils::mirrorRect(direction, effectiveAxesCenter, rc); +} + +void KisPainter::mirrorDab(Qt::Orientation direction, KisRenderedDab *dab) const +{ + KisLodTransform t(d->device); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); + + KritaUtils::mirrorDab(direction, effectiveAxesCenter, dab); +} diff --git a/libs/image/kis_painter_blt_multi_fixed.cpp b/libs/image/kis_painter_blt_multi_fixed.cpp new file mode 100644 --- /dev/null +++ b/libs/image/kis_painter_blt_multi_fixed.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2017 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_painter.h" +#include "kis_painter_p.h" + +#include "kis_paint_device.h" +#include "kis_fixed_paint_device.h" +#include "kis_random_accessor_ng.h" +#include "KisRenderedDab.h" + +void KisPainter::Private::applyDevice(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo) +{ + const QRect dabRect = dab.realBounds(); + const QRect rc = applyRect & dabRect; + + const int srcPixelSize = srcColorSpace->pixelSize(); + const int dabRowStride = srcPixelSize * dabRect.width(); + + + qint32 dstY = rc.y(); + qint32 rowsRemaining = rc.height(); + + while (rowsRemaining > 0) { + qint32 dstX = rc.x(); + + qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY); + qint32 rows = qMin(rowsRemaining, numContiguousDstRows); + + qint32 columnsRemaining = rc.width(); + + while (columnsRemaining > 0) { + + qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX); + qint32 columns = qMin(numContiguousDstColumns, columnsRemaining); + + qint32 dstRowStride = dstIt->rowStride(dstX, dstY); + dstIt->moveTo(dstX, dstY); + + localParamInfo.dstRowStart = dstIt->rawData(); + localParamInfo.dstRowStride = dstRowStride; + localParamInfo.maskRowStart = 0; + localParamInfo.maskRowStride = 0; + localParamInfo.rows = rows; + localParamInfo.cols = columns; + + + const int dabX = dstX - dabRect.x(); + const int dabY = dstY - dabRect.y(); + + localParamInfo.srcRowStart = dab.device->constData() + dabX * pixelSize + dabY * dabRowStride; + localParamInfo.srcRowStride = dabRowStride; + localParamInfo.setOpacityAndAverage(dab.opacity, dab.averageOpacity); + localParamInfo.flow = dab.flow; + colorSpace->bitBlt(srcColorSpace, localParamInfo, compositeOp, renderingIntent, conversionFlags); + + dstX += columns; + columnsRemaining -= columns; + } + + dstY += rows; + rowsRemaining -= rows; + } + +} + +void KisPainter::Private::applyDeviceWithSelection(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + KisRandomConstAccessorSP maskIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo) +{ + const QRect dabRect = dab.realBounds(); + const QRect rc = applyRect & dabRect; + + const int srcPixelSize = srcColorSpace->pixelSize(); + const int dabRowStride = srcPixelSize * dabRect.width(); + + + qint32 dstY = rc.y(); + qint32 rowsRemaining = rc.height(); + + while (rowsRemaining > 0) { + qint32 dstX = rc.x(); + + qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY); + qint32 numContiguousMaskRows = maskIt->numContiguousRows(dstY); + qint32 rows = qMin(rowsRemaining, qMin(numContiguousDstRows, numContiguousMaskRows)); + + qint32 columnsRemaining = rc.width(); + + while (columnsRemaining > 0) { + + qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX); + qint32 numContiguousMaskColumns = maskIt->numContiguousColumns(dstX); + qint32 columns = qMin(columnsRemaining, qMin(numContiguousDstColumns, numContiguousMaskColumns)); + + qint32 dstRowStride = dstIt->rowStride(dstX, dstY); + qint32 maskRowStride = maskIt->rowStride(dstX, dstY); + dstIt->moveTo(dstX, dstY); + maskIt->moveTo(dstX, dstY); + + localParamInfo.dstRowStart = dstIt->rawData(); + localParamInfo.dstRowStride = dstRowStride; + localParamInfo.maskRowStart = maskIt->rawDataConst(); + localParamInfo.maskRowStride = maskRowStride; + localParamInfo.rows = rows; + localParamInfo.cols = columns; + + + const int dabX = dstX - dabRect.x(); + const int dabY = dstY - dabRect.y(); + + localParamInfo.srcRowStart = dab.device->constData() + dabX * pixelSize + dabY * dabRowStride; + localParamInfo.srcRowStride = dabRowStride; + localParamInfo.setOpacityAndAverage(dab.opacity, dab.averageOpacity); + localParamInfo.flow = dab.flow; + colorSpace->bitBlt(srcColorSpace, localParamInfo, compositeOp, renderingIntent, conversionFlags); + + dstX += columns; + columnsRemaining -= columns; + } + + dstY += rows; + rowsRemaining -= rows; + } + +} + +void KisPainter::bltFixed(const QRect &applyRect, const QList allSrcDevices) +{ + const KoColorSpace *srcColorSpace = 0; + QList devices; + QRect rc = applyRect; + + if (d->selection) { + rc &= d->selection->selectedRect(); + } + + QRect totalDevicesRect; + + Q_FOREACH (const KisRenderedDab &dab, allSrcDevices) { + if (rc.intersects(dab.realBounds())) { + devices.append(dab); + totalDevicesRect |= dab.realBounds(); + } + + if (!srcColorSpace) { + srcColorSpace = dab.device->colorSpace(); + } else { + KIS_SAFE_ASSERT_RECOVER_RETURN(*srcColorSpace == *dab.device->colorSpace()); + } + } + + rc &= totalDevicesRect; + + if (devices.isEmpty() || rc.isEmpty()) return; + + KoCompositeOp::ParameterInfo localParamInfo = d->paramInfo; + KisRandomAccessorSP dstIt = d->device->createRandomAccessorNG(rc.left(), rc.top()); + KisRandomConstAccessorSP maskIt = d->selection ? d->selection->projection()->createRandomConstAccessorNG(rc.left(), rc.top()) : 0; + + if (maskIt) { + Q_FOREACH (const KisRenderedDab &dab, devices) { + d->applyDeviceWithSelection(rc, dab, dstIt, maskIt, srcColorSpace, localParamInfo); + } + } else { + Q_FOREACH (const KisRenderedDab &dab, devices) { + d->applyDevice(rc, dab, dstIt, srcColorSpace, localParamInfo); + } + } + + +#if 0 + // the code above does basically the same thing as this one, + // but more efficiently :) + + Q_FOREACH (KisFixedPaintDeviceSP dev, devices) { + const QRect copyRect = dev->bounds() & rc; + if (copyRect.isEmpty()) continue; + + bltFixed(copyRect.topLeft(), dev, copyRect); + } +#endif +} + diff --git a/libs/image/kis_painter_p.h b/libs/image/kis_painter_p.h new file mode 100644 --- /dev/null +++ b/libs/image/kis_painter_p.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2017 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 KISPAINTERPRIVATE_H +#define KISPAINTERPRIVATE_H + +#include +#include +#include +#include +#include "kis_paintop.h" +#include "kis_selection.h" +#include "kis_fill_painter.h" +#include "kis_painter.h" +#include "kis_paintop_preset.h" +#include + +struct Q_DECL_HIDDEN KisPainter::Private { + Private(KisPainter *_q) : q(_q) {} + Private(KisPainter *_q, const KoColorSpace *cs) + : q(_q), paintColor(cs), backgroundColor(cs) {} + + KisPainter *q; + + KisPaintDeviceSP device; + KisSelectionSP selection; + KisTransaction* transaction; + KoUpdater* progressUpdater; + + QVector dirtyRects; + KisPaintOp* paintOp; + KoColor paintColor; + KoColor backgroundColor; + KoColor customColor; + KisFilterConfigurationSP generator; + KisPaintLayer* sourceLayer; + FillStyle fillStyle; + StrokeStyle strokeStyle; + bool antiAliasPolygonFill; + const KoPattern* pattern; + QPointF duplicateOffset; + quint32 pixelSize; + const KoColorSpace* colorSpace; + KoColorProfile* profile; + const KoCompositeOp* compositeOp; + const KoAbstractGradient* gradient; + KisPaintOpPresetSP paintOpPreset; + QImage polygonMaskImage; + QPainter* maskPainter; + KisFillPainter* fillPainter; + KisPaintDeviceSP polygon; + qint32 maskImageWidth; + qint32 maskImageHeight; + QPointF axesCenter; + bool mirrorHorizontally; + bool mirrorVertically; + bool isOpacityUnit; // TODO: move into ParameterInfo + KoCompositeOp::ParameterInfo paramInfo; + KoColorConversionTransformation::Intent renderingIntent; + KoColorConversionTransformation::ConversionFlags conversionFlags; + KisRunnableStrokeJobsInterface *runnableStrokeJobsInterface = 0; + QScopedPointer fakeRunnableStrokeJobsInterface; + + bool tryReduceSourceRect(const KisPaintDevice *srcDev, + QRect *srcRect, + qint32 *srcX, + qint32 *srcY, + qint32 *srcWidth, + qint32 *srcHeight, + qint32 *dstX, + qint32 *dstY); + + void fillPainterPathImpl(const QPainterPath& path, const QRect &requestedRect); + + void applyDevice(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo); + + void applyDeviceWithSelection(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + KisRandomConstAccessorSP maskIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo); + +}; + +#endif // KISPAINTERPRIVATE_H diff --git a/libs/image/kis_projection_updates_filter.h b/libs/image/kis_projection_updates_filter.h --- a/libs/image/kis_projection_updates_filter.h +++ b/libs/image/kis_projection_updates_filter.h @@ -33,7 +33,7 @@ /** * \return true if an update should be dropped by the image */ - virtual bool filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) = 0; + virtual bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) = 0; }; @@ -44,7 +44,7 @@ class KisDropAllProjectionUpdatesFilter : public KisProjectionUpdatesFilter { public: - bool filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) override; + bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) override; }; #endif /* __KIS_PROJECTION_UPDATES_FILTER_H */ diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/kis_projection_updates_filter.cpp --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/kis_projection_updates_filter.cpp @@ -26,11 +26,11 @@ { } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) { Q_UNUSED(image); Q_UNUSED(node); - Q_UNUSED(rect); + Q_UNUSED(rects); Q_UNUSED(resetAnimationCache); return true; } diff --git a/libs/image/kis_simple_update_queue.h b/libs/image/kis_simple_update_queue.h --- a/libs/image/kis_simple_update_queue.h +++ b/libs/image/kis_simple_update_queue.h @@ -39,7 +39,8 @@ void processQueue(KisUpdaterContext &updaterContext); - void addUpdateJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail); + void addUpdateJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail); + void addUpdateJob(KisNodeSP node, const QRect &rc, const QRect& cropRect, int levelOfDetail); void addUpdateNoFilthyJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail); void addFullRefreshJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail); void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); @@ -55,7 +56,7 @@ int overrideLevelOfDetail() const; protected: - void addJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type); + void addJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type); bool processOneJob(KisUpdaterContext &updaterContext); diff --git a/libs/image/kis_simple_update_queue.cpp b/libs/image/kis_simple_update_queue.cpp --- a/libs/image/kis_simple_update_queue.cpp +++ b/libs/image/kis_simple_update_queue.cpp @@ -19,6 +19,7 @@ #include "kis_simple_update_queue.h" #include +#include #include "kis_image_config.h" #include "kis_full_refresh_walker.h" @@ -154,47 +155,62 @@ return jobAdded; } -void KisSimpleUpdateQueue::addUpdateJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail) +void KisSimpleUpdateQueue::addUpdateJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail) { - addJob(node, rc, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE); + addJob(node, rects, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE); } +void KisSimpleUpdateQueue::addUpdateJob(KisNodeSP node, const QRect &rc, const QRect& cropRect, int levelOfDetail) +{ + addJob(node, {rc}, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE); +} + + void KisSimpleUpdateQueue::addUpdateNoFilthyJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail) { - addJob(node, rc, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE_NO_FILTHY); + addJob(node, {rc}, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE_NO_FILTHY); } void KisSimpleUpdateQueue::addFullRefreshJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail) { - addJob(node, rc, cropRect, levelOfDetail, KisBaseRectsWalker::FULL_REFRESH); + addJob(node, {rc}, cropRect, levelOfDetail, KisBaseRectsWalker::FULL_REFRESH); } -void KisSimpleUpdateQueue::addJob(KisNodeSP node, const QRect& rc, +void KisSimpleUpdateQueue::addJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type) { - if(trySplitJob(node, rc, cropRect, levelOfDetail, type)) return; - if(tryMergeJob(node, rc, cropRect, levelOfDetail, type)) return; + QList walkers; - KisBaseRectsWalkerSP walker; + Q_FOREACH (const QRect &rc, rects) { + if (rc.isEmpty()) continue; - if (type == KisBaseRectsWalker::UPDATE) { - walker = new KisMergeWalker(cropRect, KisMergeWalker::DEFAULT); - } - else if (type == KisBaseRectsWalker::FULL_REFRESH) { - walker = new KisFullRefreshWalker(cropRect); - } - else if (type == KisBaseRectsWalker::UPDATE_NO_FILTHY) { - walker = new KisMergeWalker(cropRect, KisMergeWalker::NO_FILTHY); - } - /* else if(type == KisBaseRectsWalker::UNSUPPORTED) fatalKrita; */ + KisBaseRectsWalkerSP walker; - walker->collectRects(node, rc); + if(trySplitJob(node, rc, cropRect, levelOfDetail, type)) continue; + if(tryMergeJob(node, rc, cropRect, levelOfDetail, type)) continue; - m_lock.lock(); - m_updatesList.append(walker); - m_lock.unlock(); + if (type == KisBaseRectsWalker::UPDATE) { + walker = new KisMergeWalker(cropRect, KisMergeWalker::DEFAULT); + } + else if (type == KisBaseRectsWalker::FULL_REFRESH) { + walker = new KisFullRefreshWalker(cropRect); + } + else if (type == KisBaseRectsWalker::UPDATE_NO_FILTHY) { + walker = new KisMergeWalker(cropRect, KisMergeWalker::NO_FILTHY); + } + /* else if(type == KisBaseRectsWalker::UNSUPPORTED) fatalKrita; */ + + walker->collectRects(node, rc); + walkers.append(walker); + } + + if (!walkers.isEmpty()) { + m_lock.lock(); + m_updatesList.append(walkers); + m_lock.unlock(); + } } void KisSimpleUpdateQueue::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) @@ -246,14 +262,20 @@ qint32 lastCol = (rc.x() + rc.width()) / m_patchWidth; qint32 lastRow = (rc.y() + rc.height()) / m_patchHeight; + QVector splitRects; + for(qint32 i = firstRow; i <= lastRow; i++) { for(qint32 j = firstCol; j <= lastCol; j++) { QRect maxPatchRect(j * m_patchWidth, i * m_patchHeight, m_patchWidth, m_patchHeight); QRect patchRect = rc & maxPatchRect; - addJob(node, patchRect, cropRect, levelOfDetail, type); + splitRects.append(patchRect); } } + + KIS_SAFE_ASSERT_RECOVER_NOOP(!splitRects.isEmpty()); + addJob(node, splitRects, cropRect, levelOfDetail, type); + return true; } diff --git a/libs/image/kis_stroke.h b/libs/image/kis_stroke.h --- a/libs/image/kis_stroke.h +++ b/libs/image/kis_stroke.h @@ -46,6 +46,7 @@ ~KisStroke(); void addJob(KisStrokeJobData *data); + void addMutatedJobs(const QVector list); KUndo2MagicString name() const; bool hasJobs() const; @@ -68,11 +69,9 @@ bool supportsWrapAroundMode() const; int worksOnLevelOfDetail() const; bool canForgetAboutMe() const; + qreal balancingRatioOverride() const; - bool prevJobSequential() const; - bool nextJobSequential() const; - - bool nextJobBarrier() const; + KisStrokeJobData::Sequentiality nextJobSequentiality() const; void setLodBuddy(KisStrokeSP buddy); KisStrokeSP lodBuddy() const; @@ -87,7 +86,7 @@ void prepend(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, - bool isCancellable); + bool isOwnJob); KisStrokeJob* dequeue(); @@ -117,7 +116,6 @@ bool m_strokeEnded; bool m_strokeSuspended; bool m_isCancelled; // cancelled strokes are always 'ended' as well - bool m_prevJobSequential; int m_worksOnLevelOfDetail; Type m_type; diff --git a/libs/image/kis_stroke.cpp b/libs/image/kis_stroke.cpp --- a/libs/image/kis_stroke.cpp +++ b/libs/image/kis_stroke.cpp @@ -27,7 +27,6 @@ m_strokeEnded(false), m_strokeSuspended(false), m_isCancelled(false), - m_prevJobSequential(false), m_worksOnLevelOfDetail(levelOfDetail), m_type(type) { @@ -38,6 +37,8 @@ m_suspendStrategy.reset(m_strokeStrategy->createSuspendStrategy()); m_resumeStrategy.reset(m_strokeStrategy->createResumeStrategy()); + m_strokeStrategy->notifyUserStartedStroke(); + if(!m_initStrategy) { m_strokeInitialized = true; } @@ -84,13 +85,38 @@ enqueue(m_dabStrategy.data(), data); } +void KisStroke::addMutatedJobs(const QVector list) +{ + // factory methods can return null, if no action is needed + if (!m_dabStrategy) { + qDeleteAll(list); + return; + } + + // Find first non-alien (non-suspend/non-resume) job + // + // Please note that this algorithm will stop working at the day we start + // adding alien jobs not to the beginning of the stroke, but to other places. + // Right now both suspend and resume jobs are added to the beginning of + // the stroke. + + auto it = std::find_if(m_jobsQueue.begin(), m_jobsQueue.end(), + [] (KisStrokeJob *job) { + return job->isOwnJob(); + }); + + + Q_FOREACH (KisStrokeJobData *data, list) { + it = m_jobsQueue.insert(it, new KisStrokeJob(m_dabStrategy.data(), data, worksOnLevelOfDetail(), true)); + ++it; + } +} + KisStrokeJob* KisStroke::popOneJob() { KisStrokeJob *job = dequeue(); if(job) { - m_prevJobSequential = job->isSequential() || job->isBarrier(); - m_strokeInitialized = true; m_strokeSuspended = false; } @@ -119,6 +145,7 @@ m_strokeEnded = true; enqueue(m_finishStrategy.data(), m_strokeStrategy->createFinishData()); + m_strokeStrategy->notifyUserEndedStroke(); } /** @@ -240,21 +267,15 @@ return m_strokeStrategy->canForgetAboutMe(); } -bool KisStroke::prevJobSequential() const +qreal KisStroke::balancingRatioOverride() const { - return m_prevJobSequential; -} - -bool KisStroke::nextJobSequential() const -{ - return !m_jobsQueue.isEmpty() ? - m_jobsQueue.head()->isSequential() : false; + return m_strokeStrategy->balancingRatioOverride(); } -bool KisStroke::nextJobBarrier() const +KisStrokeJobData::Sequentiality KisStroke::nextJobSequentiality() const { return !m_jobsQueue.isEmpty() ? - m_jobsQueue.head()->isBarrier() : false; + m_jobsQueue.head()->sequentiality() : KisStrokeJobData::SEQUENTIAL; } void KisStroke::enqueue(KisStrokeJobStrategy *strategy, @@ -272,7 +293,7 @@ void KisStroke::prepend(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, - bool isCancellable) + bool isOwnJob) { // factory methods can return null, if no action is needed if(!strategy) { @@ -283,7 +304,7 @@ // LOG_MERGE_FIXME: Q_UNUSED(levelOfDetail); - m_jobsQueue.prepend(new KisStrokeJob(strategy, data, worksOnLevelOfDetail(), isCancellable)); + m_jobsQueue.prepend(new KisStrokeJob(strategy, data, worksOnLevelOfDetail(), isOwnJob)); } KisStrokeJob* KisStroke::dequeue() diff --git a/libs/image/kis_stroke_job.h b/libs/image/kis_stroke_job.h --- a/libs/image/kis_stroke_job.h +++ b/libs/image/kis_stroke_job.h @@ -28,11 +28,11 @@ KisStrokeJob(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, - bool isCancellable) + bool isOwnJob) : m_dabStrategy(strategy), m_dabData(data), m_levelOfDetail(levelOfDetail), - m_isCancellable(isCancellable) + m_isOwnJob(isOwnJob) { } @@ -44,6 +44,10 @@ m_dabStrategy->run(m_dabData); } + KisStrokeJobData::Sequentiality sequentiality() const { + return m_dabData ? m_dabData->sequentiality() : KisStrokeJobData::SEQUENTIAL; + } + bool isSequential() const { // Default value is 'SEQUENTIAL' return m_dabData ? m_dabData->isSequential() : true; @@ -64,7 +68,11 @@ } bool isCancellable() const { - return m_isCancellable; + return m_isOwnJob; + } + + bool isOwnJob() const { + return m_isOwnJob; } private: @@ -89,7 +97,7 @@ KisStrokeJobData *m_dabData; int m_levelOfDetail; - bool m_isCancellable; + bool m_isOwnJob; }; #endif /* __KIS_STROKE_JOB_H */ diff --git a/libs/image/kis_stroke_job_strategy.h b/libs/image/kis_stroke_job_strategy.h --- a/libs/image/kis_stroke_job_strategy.h +++ b/libs/image/kis_stroke_job_strategy.h @@ -28,7 +28,8 @@ enum Sequentiality { CONCURRENT, SEQUENTIAL, - BARRIER + BARRIER, + UNIQUELY_CONCURRENT }; enum Exclusivity { @@ -45,8 +46,8 @@ bool isSequential() const; bool isExclusive() const; - Sequentiality sequentiality() { return m_sequentiality; }; - Exclusivity exclusivity() { return m_exclusivity; }; + Sequentiality sequentiality() { return m_sequentiality; } + Exclusivity exclusivity() { return m_exclusivity; } virtual KisStrokeJobData* createLodClone(int levelOfDetail); diff --git a/libs/image/kis_stroke_strategy.h b/libs/image/kis_stroke_strategy.h --- a/libs/image/kis_stroke_strategy.h +++ b/libs/image/kis_stroke_strategy.h @@ -24,15 +24,37 @@ #include "kundo2magicstring.h" #include "kritaimage_export.h" + class KisStrokeJobStrategy; class KisStrokeJobData; +class KisStrokesQueueMutatedJobInterface; class KRITAIMAGE_EXPORT KisStrokeStrategy { public: KisStrokeStrategy(QString id = QString(), const KUndo2MagicString &name = KUndo2MagicString()); virtual ~KisStrokeStrategy(); + /** + * notifyUserStartedStroke() is a callback used by the strokes system to notify + * when the user adds the stroke to the strokes queue. That moment corresponds + * to the user calling strokesFacade->startStroke(strategy) and might happen much + * earlier than the first job being executed. + * + * NOTE: this method will be executed in the context of the GUI thread! + */ + virtual void notifyUserStartedStroke(); + + /** + * notifyUserEndedStroke() is a callback used by the strokes system to notify + * when the user ends the stroke. That moment corresponds to the user calling + * strokesFacade->endStroke(id) and might happen much earlier when the stroke + * even started its execution. + * + * NOTE: this method will be executed in the context of the GUI thread! + */ + virtual void notifyUserEndedStroke(); + virtual KisStrokeJobStrategy* createInitStrategy(); virtual KisStrokeJobStrategy* createFinishStrategy(); virtual KisStrokeJobStrategy* createCancelStrategy(); @@ -83,15 +105,25 @@ bool needsExplicitCancel() const; + /** + * \see setBalancingRatioOverride() for details + */ + qreal balancingRatioOverride() const; + QString id() const; KUndo2MagicString name() const; /** * Set up by the strokes queue during the stroke initialization */ void setCancelStrokeId(KisStrokeId id) { m_cancelStrokeId = id; } + void setMutatedJobsInterface(KisStrokesQueueMutatedJobInterface *mutatedJobsInterface); + protected: + // testing surrogate class + friend class KisMutatableDabStrategy; + /** * The cancel job may populate the stroke with some new jobs * for cancelling. To achieve this it needs the stroke id. @@ -102,6 +134,32 @@ */ KisStrokeId cancelStrokeId() { return m_cancelStrokeId; } + /** + * This function is supposed to be called by internal asynchronous + * jobs. It allows adding subtasks that may be executed concurrently. + * + * Requirements: + * * must be called *only* from within the context of the strokes + * worker thread during exectution one of its jobs + * + * Guarantees: + * * the added job is guaranteed to be executed in some time after + * the currently executed job, *before* the next SEQUENTIAL or + * BARRIER job + * * if the currently executed job is CUNCURRENTthe mutated job *may* + * start execution right after adding to the queue without waiting for + * its parent to complete. Though this behavior is *not* guaranteed, + * because addMutatedJob does not initiate processQueues(), because + * it may lead to a deadlock. + */ + void addMutatedJobs(const QVector list); + + /** + * Convenience override for addMutatedJobs() + */ + void addMutatedJob(KisStrokeJobData *data); + + // you are not supposed to change these parameters // after the KisStroke object has been created @@ -114,6 +172,20 @@ void setCanForgetAboutMe(bool value); void setNeedsExplicitCancel(bool value); + /** + * Set override for the desired scheduler balancing ratio: + * + * ratio = stroke jobs / update jobs + * + * If \p value < 1.0, then the priority is given to updates, if + * the value is higher than 1.0, then the priority is given + * to stroke jobs. + * + * Special value -1.0, suggests the scheduler to use the default value + * set by the user's config file (which is 100.0 by default). + */ + void setBalancingRatioOverride(qreal value); + protected: /** * Protected c-tor, used for cloning of hi-level strategies @@ -129,11 +201,13 @@ bool m_requestsOtherStrokesToEnd; bool m_canForgetAboutMe; bool m_needsExplicitCancel; + qreal m_balancingRatioOverride; QString m_id; KUndo2MagicString m_name; KisStrokeId m_cancelStrokeId; + KisStrokesQueueMutatedJobInterface *m_mutatedJobsInterface; }; #endif /* __KIS_STROKE_STRATEGY_H */ diff --git a/libs/image/kis_stroke_strategy.cpp b/libs/image/kis_stroke_strategy.cpp --- a/libs/image/kis_stroke_strategy.cpp +++ b/libs/image/kis_stroke_strategy.cpp @@ -19,6 +19,7 @@ #include "kis_stroke_strategy.h" #include #include "kis_stroke_job_strategy.h" +#include "KisStrokesQueueMutatedJobInterface.h" KisStrokeStrategy::KisStrokeStrategy(QString id, const KUndo2MagicString &name) @@ -30,8 +31,10 @@ m_requestsOtherStrokesToEnd(true), m_canForgetAboutMe(false), m_needsExplicitCancel(false), + m_balancingRatioOverride(-1.0), m_id(id), - m_name(name) + m_name(name), + m_mutatedJobsInterface(0) { } @@ -44,17 +47,26 @@ m_requestsOtherStrokesToEnd(rhs.m_requestsOtherStrokesToEnd), m_canForgetAboutMe(rhs.m_canForgetAboutMe), m_needsExplicitCancel(rhs.m_needsExplicitCancel), + m_balancingRatioOverride(rhs.m_balancingRatioOverride), m_id(rhs.m_id), - m_name(rhs.m_name) + m_name(rhs.m_name), + m_mutatedJobsInterface(0) { - KIS_ASSERT_RECOVER_NOOP(!rhs.m_cancelStrokeId && + KIS_ASSERT_RECOVER_NOOP(!rhs.m_cancelStrokeId && !m_mutatedJobsInterface && "After the stroke has been started, no copying must happen"); } KisStrokeStrategy::~KisStrokeStrategy() { } +void KisStrokeStrategy::notifyUserStartedStroke() +{ +} + +void KisStrokeStrategy::notifyUserEndedStroke() +{ +} KisStrokeJobStrategy* KisStrokeStrategy::createInitStrategy() { @@ -147,6 +159,26 @@ return m_name; } +void KisStrokeStrategy::setMutatedJobsInterface(KisStrokesQueueMutatedJobInterface *mutatedJobsInterface) +{ + m_mutatedJobsInterface = mutatedJobsInterface; +} + +void KisStrokeStrategy::addMutatedJobs(const QVector list) +{ + KIS_SAFE_ASSERT_RECOVER(m_mutatedJobsInterface && m_cancelStrokeId) { + qDeleteAll(list); + return; + } + + m_mutatedJobsInterface->addMutatedJobs(m_cancelStrokeId, list); +} + +void KisStrokeStrategy::addMutatedJob(KisStrokeJobData *data) +{ + addMutatedJobs({data}); +} + void KisStrokeStrategy::setExclusive(bool value) { m_exclusive = value; @@ -206,3 +238,13 @@ { m_needsExplicitCancel = value; } + +qreal KisStrokeStrategy::balancingRatioOverride() const +{ + return m_balancingRatioOverride; +} + +void KisStrokeStrategy::setBalancingRatioOverride(qreal value) +{ + m_balancingRatioOverride = value; +} diff --git a/libs/image/kis_strokes_queue.h b/libs/image/kis_strokes_queue.h --- a/libs/image/kis_strokes_queue.h +++ b/libs/image/kis_strokes_queue.h @@ -26,7 +26,8 @@ #include "kis_stroke_strategy.h" #include "kis_stroke_strategy_factory.h" #include "kis_strokes_queue_undo_result.h" - +#include "KisStrokesQueueMutatedJobInterface.h" +#include "KisUpdaterContextSnapshotEx.h" class KisUpdaterContext; @@ -36,7 +37,7 @@ class KisPostExecutionUndoAdapter; -class KRITAIMAGE_EXPORT KisStrokesQueue +class KRITAIMAGE_EXPORT KisStrokesQueue : public KisStrokesQueueMutatedJobInterface { public: KisStrokesQueue(); @@ -62,6 +63,7 @@ bool hasOpenedStrokes() const; bool wrapAroundModeSupported() const; + qreal balancingRatioOverride() const; void setDesiredLevelOfDetail(int lod); void explicitRegenerateLevelOfDetail(); @@ -79,14 +81,17 @@ void debugDumpAllStrokes(); + // interface for KisStrokeStrategy only! + void addMutatedJobs(KisStrokeId id, const QVector list) final; + private: bool processOneJob(KisUpdaterContext &updaterContext, bool externalJobsPending); bool checkStrokeState(bool hasStrokeJobsRunning, int runningLevelOfDetail); - bool checkExclusiveProperty(qint32 numMergeJobs, qint32 numStrokeJobs); - bool checkSequentialProperty(qint32 numMergeJobs, qint32 numStrokeJobs); - bool checkBarrierProperty(qint32 numMergeJobs, qint32 numStrokeJobs, + bool checkExclusiveProperty(bool hasMergeJobs, bool hasStrokeJobs); + bool checkSequentialProperty(KisUpdaterContextSnapshotEx snapshot, bool externalJobsPending); + bool checkBarrierProperty(bool hasMergeJobs, bool hasStrokeJobs, bool externalJobsPending); bool checkLevelOfDetailProperty(int runningLevelOfDetail); diff --git a/libs/image/kis_strokes_queue.cpp b/libs/image/kis_strokes_queue.cpp --- a/libs/image/kis_strokes_queue.cpp +++ b/libs/image/kis_strokes_queue.cpp @@ -76,6 +76,7 @@ openedStrokesCounter(0), needsExclusiveAccess(false), wrapAroundModeSupported(false), + balancingRatioOverride(-1.0), currentStrokeLoaded(false), lodNNeedsSynchronization(true), desiredLevelOfDetail(0), @@ -88,6 +89,7 @@ int openedStrokesCounter; bool needsExclusiveAccess; bool wrapAroundModeSupported; + qreal balancingRatioOverride; bool currentStrokeLoaded; bool lodNNeedsSynchronization; @@ -131,12 +133,13 @@ template typename StrokesQueue::iterator -executeStrokePair(const StrokePair &pair, StrokesQueue &queue, typename StrokesQueue::iterator it, KisStroke::Type type, int levelOfDetail) { +executeStrokePair(const StrokePair &pair, StrokesQueue &queue, typename StrokesQueue::iterator it, KisStroke::Type type, int levelOfDetail, KisStrokesQueueMutatedJobInterface *mutatedJobsInterface) { KisStrokeStrategy *strategy = pair.first; QList jobsData = pair.second; KisStrokeSP stroke(new KisStroke(strategy, type, levelOfDetail)); strategy->setCancelStrokeId(stroke); + strategy->setMutatedJobsInterface(mutatedJobsInterface); it = queue.insert(it, stroke); Q_FOREACH (KisStrokeJobData *jobData, jobsData) { stroke->addJob(jobData); @@ -155,7 +158,7 @@ if (!this->lod0ToNStrokeStrategyFactory) return; KisLodSyncPair syncPair = this->lod0ToNStrokeStrategyFactory(forgettable); - executeStrokePair(syncPair, this->strokesQueue, this->strokesQueue.end(), KisStroke::LODN, levelOfDetail); + executeStrokePair(syncPair, this->strokesQueue, this->strokesQueue.end(), KisStroke::LODN, levelOfDetail, q); this->lodNNeedsSynchronization = false; } @@ -269,6 +272,7 @@ KisStrokeSP buddy(new KisStroke(strokeStrategy, KisStroke::LODN, m_d->desiredLevelOfDetail)); strokeStrategy->setCancelStrokeId(buddy); + strokeStrategy->setMutatedJobsInterface(this); m_d->strokesQueue.insert(m_d->findNewLodNPos(buddy), buddy); KisStrokeId id(buddy); @@ -299,6 +303,7 @@ KisStrokeSP buddy(new KisStroke(lodBuddyStrategy, KisStroke::LODN, m_d->desiredLevelOfDetail)); lodBuddyStrategy->setCancelStrokeId(buddy); + lodBuddyStrategy->setMutatedJobsInterface(this); stroke->setLodBuddy(buddy); m_d->strokesQueue.insert(m_d->findNewLodNPos(buddy), buddy); @@ -309,9 +314,9 @@ StrokesQueueIterator it = m_d->findNewLod0Pos(); - it = executeStrokePair(resumePair, m_d->strokesQueue, it, KisStroke::RESUME, 0); + it = executeStrokePair(resumePair, m_d->strokesQueue, it, KisStroke::RESUME, 0, this); it = m_d->strokesQueue.insert(it, stroke); - it = executeStrokePair(suspendPair, m_d->strokesQueue, it, KisStroke::SUSPEND, 0); + it = executeStrokePair(suspendPair, m_d->strokesQueue, it, KisStroke::SUSPEND, 0, this); } else { m_d->strokesQueue.insert(m_d->findNewLod0Pos(), stroke); @@ -324,6 +329,7 @@ KisStrokeId id(stroke); strokeStrategy->setCancelStrokeId(id); + strokeStrategy->setMutatedJobsInterface(this); m_d->openedStrokesCounter++; @@ -353,6 +359,16 @@ stroke->addJob(data); } +void KisStrokesQueue::addMutatedJobs(KisStrokeId id, const QVector list) +{ + QMutexLocker locker(&m_d->mutex); + + KisStrokeSP stroke = id.toStrongRef(); + Q_ASSERT(stroke); + + stroke->addMutatedJobs(list); +} + void KisStrokesQueue::endStroke(KisStrokeId id) { QMutexLocker locker(&m_d->mutex); @@ -557,6 +573,11 @@ return m_d->wrapAroundModeSupported; } +qreal KisStrokesQueue::balancingRatioOverride() const +{ + return m_d->balancingRatioOverride; +} + bool KisStrokesQueue::isEmpty() const { QMutexLocker locker(&m_d->mutex); @@ -667,17 +688,17 @@ if(m_d->strokesQueue.isEmpty()) return false; bool result = false; - qint32 numMergeJobs; - qint32 numStrokeJobs; - updaterContext.getJobsSnapshot(numMergeJobs, numStrokeJobs); + const int levelOfDetail = updaterContext.currentLevelOfDetail(); + + const KisUpdaterContextSnapshotEx snapshot = updaterContext.getContextSnapshotEx(); - int levelOfDetail = updaterContext.currentLevelOfDetail(); + const bool hasStrokeJobs = !(snapshot == ContextEmpty || + snapshot == HasMergeJob); + const bool hasMergeJobs = snapshot & HasMergeJob; - if(checkStrokeState(numStrokeJobs, levelOfDetail) && - checkExclusiveProperty(numMergeJobs, numStrokeJobs) && - checkSequentialProperty(numMergeJobs, numStrokeJobs) && - checkBarrierProperty(numMergeJobs, numStrokeJobs, - externalJobsPending)) { + if(checkStrokeState(hasStrokeJobs, levelOfDetail) && + checkExclusiveProperty(hasMergeJobs, hasStrokeJobs) && + checkSequentialProperty(snapshot, externalJobsPending)) { KisStrokeSP stroke = m_d->strokesQueue.head(); updaterContext.addStrokeJob(stroke->popOneJob()); @@ -718,6 +739,7 @@ if (!m_d->currentStrokeLoaded) { m_d->needsExclusiveAccess = stroke->isExclusive(); m_d->wrapAroundModeSupported = stroke->supportsWrapAroundMode(); + m_d->balancingRatioOverride = stroke->balancingRatioOverride(); m_d->currentStrokeLoaded = true; } @@ -731,6 +753,7 @@ if (!m_d->currentStrokeLoaded) { m_d->needsExclusiveAccess = stroke->isExclusive(); m_d->wrapAroundModeSupported = stroke->supportsWrapAroundMode(); + m_d->balancingRatioOverride = stroke->balancingRatioOverride(); m_d->currentStrokeLoaded = true; } @@ -742,6 +765,7 @@ m_d->strokesQueue.dequeue(); // deleted by shared pointer m_d->needsExclusiveAccess = false; m_d->wrapAroundModeSupported = false; + m_d->balancingRatioOverride = -1.0; m_d->currentStrokeLoaded = false; m_d->switchDesiredLevelOfDetail(false); @@ -754,36 +778,51 @@ return result; } -bool KisStrokesQueue::checkExclusiveProperty(qint32 numMergeJobs, - qint32 numStrokeJobs) +bool KisStrokesQueue::checkExclusiveProperty(bool hasMergeJobs, + bool hasStrokeJobs) { + Q_UNUSED(hasStrokeJobs); + if(!m_d->strokesQueue.head()->isExclusive()) return true; - Q_UNUSED(numMergeJobs); - Q_UNUSED(numStrokeJobs); - Q_ASSERT(!(numMergeJobs && numStrokeJobs)); - return numMergeJobs == 0; + return hasMergeJobs == 0; } -bool KisStrokesQueue::checkSequentialProperty(qint32 numMergeJobs, - qint32 numStrokeJobs) +bool KisStrokesQueue::checkSequentialProperty(KisUpdaterContextSnapshotEx snapshot, + bool externalJobsPending) { - Q_UNUSED(numMergeJobs); - KisStrokeSP stroke = m_d->strokesQueue.head(); - if(!stroke->prevJobSequential() && !stroke->nextJobSequential()) return true; - Q_ASSERT(!stroke->prevJobSequential() || numStrokeJobs <= 1); - return numStrokeJobs == 0; -} + if (snapshot & HasSequentialJob || + snapshot & HasBarrierJob) { + return false; + } -bool KisStrokesQueue::checkBarrierProperty(qint32 numMergeJobs, - qint32 numStrokeJobs, - bool externalJobsPending) -{ - KisStrokeSP stroke = m_d->strokesQueue.head(); - if(!stroke->nextJobBarrier()) return true; + KisStrokeJobData::Sequentiality nextSequentiality = + stroke->nextJobSequentiality(); + + if (nextSequentiality == KisStrokeJobData::UNIQUELY_CONCURRENT && + snapshot & HasUniquelyConcurrentJob) { + + return false; + } + + if (nextSequentiality == KisStrokeJobData::SEQUENTIAL && + (snapshot & HasUniquelyConcurrentJob || + snapshot & HasConcurrentJob)) { - return !numMergeJobs && !numStrokeJobs && !externalJobsPending; + return false; + } + + if (nextSequentiality == KisStrokeJobData::BARRIER && + (snapshot & HasUniquelyConcurrentJob || + snapshot & HasConcurrentJob || + snapshot & HasMergeJob || + externalJobsPending)) { + + return false; + } + + return true; } bool KisStrokesQueue::checkLevelOfDetailProperty(int runningLevelOfDetail) diff --git a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp --- a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp +++ b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp @@ -55,11 +55,15 @@ { } - bool filter(KisImage *image, KisNode *node, const QRect &rect, bool resetAnimationCache) override { + bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) override { if (image->currentLevelOfDetail() > 0) return false; QMutexLocker l(&m_mutex); - m_requestsHash[KisNodeSP(node)].append(Request(rect, resetAnimationCache)); + + Q_FOREACH(const QRect &rc, rects) { + m_requestsHash[KisNodeSP(node)].append(Request(rc, resetAnimationCache)); + } + return true; } @@ -97,10 +101,8 @@ resetAnimationCache |= req.resetAnimationCache; } - Q_FOREACH (const QRect &rc, region.rects()) { - // FIXME: constness: port rPU to SP - listener->requestProjectionUpdate(const_cast(node.data()), rc, resetAnimationCache); - } + // FIXME: constness: port rPU to SP + listener->requestProjectionUpdate(const_cast(node.data()), region.rects(), resetAnimationCache); } } diff --git a/libs/image/kis_update_job_item.h b/libs/image/kis_update_job_item.h --- a/libs/image/kis_update_job_item.h +++ b/libs/image/kis_update_job_item.h @@ -19,6 +19,8 @@ #ifndef __KIS_UPDATE_JOB_ITEM_H #define __KIS_UPDATE_JOB_ITEM_H +#include + #include #include @@ -32,20 +34,22 @@ { Q_OBJECT public: - enum Type { - EMPTY, + enum class Type : int { + EMPTY = 0, + WAITING, MERGE, STROKE, SPONTANEOUS }; public: KisUpdateJobItem(QReadWriteLock *exclusiveJobLock) : m_exclusiveJobLock(exclusiveJobLock), - m_type(EMPTY), + m_atomicType(Type::EMPTY), m_runnableJob(0) { setAutoDelete(false); + KIS_SAFE_ASSERT_RECOVER_NOOP(m_atomicType.is_lock_free()); } ~KisUpdateJobItem() override { @@ -61,93 +65,117 @@ * That is a nice idea, but it doesn't work well when the jobs are small enough * and the number of available cores is high (>4 cores). It this case the * threads just tend to execute the job very quickly and go to sleep, which is - * an expencive operation. + * an expensive operation. * * To overcome this problem we try to bulk-process the jobs. In sigJobFinished() * signal (which is DirectConnection), the context may add the job to ourselves(!!!), * so we switch from "done" state into "running" again. */ - while (isRunning()) { - m_isExecuting.ref(); + while (1) { + KIS_SAFE_ASSERT_RECOVER_RETURN(isRunning()); if(m_exclusive) { m_exclusiveJobLock->lockForWrite(); } else { m_exclusiveJobLock->lockForRead(); } - if(m_type == MERGE) { + if(m_atomicType == Type::MERGE) { runMergeJob(); } else { - Q_ASSERT(m_type == STROKE || m_type == SPONTANEOUS); + KIS_ASSERT(m_atomicType == Type::STROKE || + m_atomicType == Type::SPONTANEOUS); + m_runnableJob->run(); - delete m_runnableJob; - m_runnableJob = 0; } setDone(); emit sigDoSomeUsefulWork(); + + // may flip the current state from Waiting -> Running again emit sigJobFinished(); m_exclusiveJobLock->unlock(); - m_isExecuting.deref(); + // try to exit the loop. Please note, that noone can flip the state from + // WAITING to EMPTY except ourselves! + Type expectedValue = Type::WAITING; + if (m_atomicType.compare_exchange_strong(expectedValue, Type::EMPTY)) { + break; + } } } inline void runMergeJob() { - Q_ASSERT(m_type == MERGE); + Q_ASSERT(m_atomicType == Type::MERGE); // dbgKrita << "Executing merge job" << m_walker->changeRect() // << "on thread" << QThread::currentThreadId(); m_merger.startMerge(*m_walker); QRect changeRect = m_walker->changeRect(); emit sigContinueUpdate(changeRect); } - inline void setWalker(KisBaseRectsWalkerSP walker) { - m_type = MERGE; + // return true if the thread should actually be started + inline bool setWalker(KisBaseRectsWalkerSP walker) { + KIS_ASSERT(m_atomicType <= Type::WAITING); + m_accessRect = walker->accessRect(); m_changeRect = walker->changeRect(); m_walker = walker; m_exclusive = false; m_runnableJob = 0; + + const Type oldState = m_atomicType.exchange(Type::MERGE); + return oldState == Type::EMPTY; } - inline void setStrokeJob(KisStrokeJob *strokeJob) { - m_type = STROKE; + // return true if the thread should actually be started + inline bool setStrokeJob(KisStrokeJob *strokeJob) { + KIS_ASSERT(m_atomicType <= Type::WAITING); + m_runnableJob = strokeJob; + m_strokeJobSequentiality = strokeJob->sequentiality(); m_exclusive = strokeJob->isExclusive(); m_walker = 0; m_accessRect = m_changeRect = QRect(); + + const Type oldState = m_atomicType.exchange(Type::STROKE); + return oldState == Type::EMPTY; } - inline void setSpontaneousJob(KisSpontaneousJob *spontaneousJob) { - m_type = SPONTANEOUS; + // return true if the thread should actually be started + inline bool setSpontaneousJob(KisSpontaneousJob *spontaneousJob) { + KIS_ASSERT(m_atomicType <= Type::WAITING); + m_runnableJob = spontaneousJob; m_exclusive = false; m_walker = 0; m_accessRect = m_changeRect = QRect(); + + const Type oldState = m_atomicType.exchange(Type::SPONTANEOUS); + return oldState == Type::EMPTY; } inline void setDone() { m_walker = 0; + delete m_runnableJob; m_runnableJob = 0; - m_type = EMPTY; + m_atomicType = Type::WAITING; } inline bool isRunning() const { - return m_type != EMPTY; + return m_atomicType >= Type::MERGE; } inline Type type() const { - return m_type; + return m_atomicType; } inline const QRect& accessRect() const { @@ -158,8 +186,8 @@ return m_changeRect; } - inline bool hasThreadAttached() const { - return m_isExecuting; + inline KisStrokeJobData::Sequentiality strokeJobSequentiality() const { + return m_strokeJobSequentiality; } Q_SIGNALS: @@ -189,9 +217,6 @@ } inline void testingSetDone() { - if(m_type == STROKE) { - delete m_runnableJob; - } setDone(); } @@ -203,7 +228,9 @@ bool m_exclusive; - volatile Type m_type; + std::atomic m_atomicType; + + volatile KisStrokeJobData::Sequentiality m_strokeJobSequentiality; /** * Runnable jobs part @@ -224,8 +251,6 @@ */ QRect m_accessRect; QRect m_changeRect; - - QAtomicInt m_isExecuting; }; diff --git a/libs/image/kis_update_scheduler.h b/libs/image/kis_update_scheduler.h --- a/libs/image/kis_update_scheduler.h +++ b/libs/image/kis_update_scheduler.h @@ -135,7 +135,8 @@ */ void unblockUpdates(); - void updateProjection(KisNodeSP node, const QRect& rc, const QRect &cropRect); + void updateProjection(KisNodeSP node, const QVector &rects, const QRect &cropRect); + void updateProjection(KisNodeSP node, const QRect &rc, const QRect &cropRect); void updateProjectionNoFilthy(KisNodeSP node, const QRect& rc, const QRect &cropRect); void fullRefreshAsync(KisNodeSP root, const QRect& rc, const QRect &cropRect); void fullRefresh(KisNodeSP root, const QRect& rc, const QRect &cropRect); diff --git a/libs/image/kis_update_scheduler.cpp b/libs/image/kis_update_scheduler.cpp --- a/libs/image/kis_update_scheduler.cpp +++ b/libs/image/kis_update_scheduler.cpp @@ -60,13 +60,18 @@ KisStrokesQueue strokesQueue; KisUpdaterContext updaterContext; bool processingBlocked = false; - qreal balancingRatio = 1.0; // updates-queue-size/strokes-queue-size + qreal defaultBalancingRatio = 1.0; // desired strokes-queue-size / updates-queue-size KisProjectionUpdateListener *projectionUpdateListener; KisQueuesProgressUpdater *progressUpdater = 0; QAtomicInt updatesLockCounter; QReadWriteLock updatesStartLock; KisLazyWaitCondition updatesFinishedCondition; + + qreal balancingRatio() const { + const qreal strokeRatioOverride = strokesQueue.balancingRatioOverride(); + return strokeRatioOverride > 0 ? strokeRatioOverride : defaultBalancingRatio; + } }; KisUpdateScheduler::KisUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener, QObject *parent) @@ -150,7 +155,13 @@ } } -void KisUpdateScheduler::updateProjection(KisNodeSP node, const QRect& rc, const QRect &cropRect) +void KisUpdateScheduler::updateProjection(KisNodeSP node, const QVector &rects, const QRect &cropRect) +{ + m_d->updatesQueue.addUpdateJob(node, rects, cropRect, currentLevelOfDetail()); + processQueues(); +} + +void KisUpdateScheduler::updateProjection(KisNodeSP node, const QRect &rc, const QRect &cropRect) { m_d->updatesQueue.addUpdateJob(node, rc, cropRect, currentLevelOfDetail()); processQueues(); @@ -304,7 +315,7 @@ m_d->updatesQueue.updateSettings(); KisImageConfig config; - m_d->balancingRatio = config.schedulerBalancingRatio(); + m_d->defaultBalancingRatio = config.schedulerBalancingRatio(); setThreadsLimit(config.maxNumberOfThreads()); } @@ -391,7 +402,7 @@ tryProcessUpdatesQueue(); } } - else if(m_d->balancingRatio * m_d->strokesQueue.sizeMetric() > m_d->updatesQueue.sizeMetric()) { + else if(m_d->balancingRatio() * m_d->strokesQueue.sizeMetric() > m_d->updatesQueue.sizeMetric()) { DEBUG_BALANCING_METRICS("STROKES", "N"); m_d->strokesQueue.processQueue(m_d->updaterContext, !m_d->updatesQueue.isEmpty()); diff --git a/libs/image/kis_updater_context.h b/libs/image/kis_updater_context.h --- a/libs/image/kis_updater_context.h +++ b/libs/image/kis_updater_context.h @@ -28,6 +28,7 @@ #include "kis_async_merger.h" #include "kis_lock_free_lod_counter.h" +#include "KisUpdaterContextSnapshotEx.h" class KisUpdateJobItem; class KisSpontaneousJob; @@ -52,6 +53,8 @@ */ void getJobsSnapshot(qint32 &numMergeJobs, qint32 &numStrokeJobs); + KisUpdaterContextSnapshotEx getContextSnapshotEx() const; + /** * Returns the current level of detail of all the running jobs in the * context. If there are no jobs, returns -1. @@ -186,5 +189,7 @@ }; + + #endif /* __KIS_UPDATER_CONTEXT_H */ diff --git a/libs/image/kis_updater_context.cpp b/libs/image/kis_updater_context.cpp --- a/libs/image/kis_updater_context.cpp +++ b/libs/image/kis_updater_context.cpp @@ -51,16 +51,45 @@ numStrokeJobs = 0; Q_FOREACH (const KisUpdateJobItem *item, m_jobs) { - if(item->type() == KisUpdateJobItem::MERGE || - item->type() == KisUpdateJobItem::SPONTANEOUS) { + if(item->type() == KisUpdateJobItem::Type::MERGE || + item->type() == KisUpdateJobItem::Type::SPONTANEOUS) { numMergeJobs++; } - else if(item->type() == KisUpdateJobItem::STROKE) { + else if(item->type() == KisUpdateJobItem::Type::STROKE) { numStrokeJobs++; } } } +KisUpdaterContextSnapshotEx KisUpdaterContext::getContextSnapshotEx() const +{ + KisUpdaterContextSnapshotEx state = ContextEmpty; + + Q_FOREACH (const KisUpdateJobItem *item, m_jobs) { + if (item->type() == KisUpdateJobItem::Type::MERGE || + item->type() == KisUpdateJobItem::Type::SPONTANEOUS) { + state |= HasMergeJob; + } else if(item->type() == KisUpdateJobItem::Type::STROKE) { + switch (item->strokeJobSequentiality()) { + case KisStrokeJobData::SEQUENTIAL: + state |= HasSequentialJob; + break; + case KisStrokeJobData::CONCURRENT: + state |= HasConcurrentJob; + break; + case KisStrokeJobData::BARRIER: + state |= HasBarrierJob; + break; + case KisStrokeJobData::UNIQUELY_CONCURRENT: + state |= HasUniquelyConcurrentJob; + break; + } + } + } + + return state; +} + int KisUpdaterContext::currentLevelOfDetail() const { return m_lodCounter.readLod(); @@ -110,11 +139,11 @@ qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setWalker(walker); + const bool shouldStartThread = m_jobs[jobIndex]->setWalker(walker); // it might happen that we call this function from within // the thread itself, right when it finished its work - if (!m_jobs[jobIndex]->hasThreadAttached()) { + if (shouldStartThread) { m_threadPool.start(m_jobs[jobIndex]); } } @@ -128,21 +157,23 @@ qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setWalker(walker); + const bool shouldStartThread = m_jobs[jobIndex]->setWalker(walker); + // HINT: Not calling start() here + Q_UNUSED(shouldStartThread); } void KisUpdaterContext::addStrokeJob(KisStrokeJob *strokeJob) { m_lodCounter.addLod(strokeJob->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setStrokeJob(strokeJob); + const bool shouldStartThread = m_jobs[jobIndex]->setStrokeJob(strokeJob); // it might happen that we call this function from within // the thread itself, right when it finished its work - if (!m_jobs[jobIndex]->hasThreadAttached()) { + if (shouldStartThread) { m_threadPool.start(m_jobs[jobIndex]); } } @@ -156,21 +187,23 @@ qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setStrokeJob(strokeJob); + const bool shouldStartThread = m_jobs[jobIndex]->setStrokeJob(strokeJob); + // HINT: Not calling start() here + Q_UNUSED(shouldStartThread); } void KisUpdaterContext::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_lodCounter.addLod(spontaneousJob->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); + const bool shouldStartThread = m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); // it might happen that we call this function from within // the thread itself, right when it finished its work - if (!m_jobs[jobIndex]->hasThreadAttached()) { + if (shouldStartThread) { m_threadPool.start(m_jobs[jobIndex]); } } @@ -184,8 +217,10 @@ qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); + const bool shouldStartThread = m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); + // HINT: Not calling start() here + Q_UNUSED(shouldStartThread); } void KisUpdaterContext::waitForDone() diff --git a/libs/image/krita_utils.h b/libs/image/krita_utils.h --- a/libs/image/krita_utils.h +++ b/libs/image/krita_utils.h @@ -27,6 +27,7 @@ class QPainterPath; class QBitArray; class QPainter; +class KisRenderedDab; #include #include "kritaimage_export.h" @@ -96,6 +97,9 @@ void KRITAIMAGE_EXPORT filterAlpha8Device(KisPaintDeviceSP dev, const QRect &rc, std::function func); qreal KRITAIMAGE_EXPORT estimatePortionOfTransparentPixels(KisPaintDeviceSP dev, const QRect &rect, qreal samplePortion); + + void KRITAIMAGE_EXPORT mirrorDab(Qt::Orientation dir, const QPoint ¢er, KisRenderedDab *dab); + void KRITAIMAGE_EXPORT mirrorRect(Qt::Orientation dir, const QPoint ¢er, QRect *rc); } #endif /* __KRITA_UTILS_H */ diff --git a/libs/image/krita_utils.cpp b/libs/image/krita_utils.cpp --- a/libs/image/krita_utils.cpp +++ b/libs/image/krita_utils.cpp @@ -37,6 +37,8 @@ #include "kis_sequential_iterator.h" #include "kis_random_accessor_ng.h" +#include + namespace KritaUtils { @@ -441,4 +443,32 @@ return qreal(numTransparentPixels) / numPixels; } + + void mirrorDab(Qt::Orientation dir, const QPoint ¢er, KisRenderedDab *dab) + { + const QRect rc = dab->realBounds(); + + if (dir == Qt::Horizontal) { + const int mirrorX = -((rc.x() + rc.width()) - center.x()) + center.x(); + + dab->device->mirror(true, false); + dab->offset.rx() = mirrorX; + } else /* if (dir == Qt::Vertical) */ { + const int mirrorY = -((rc.y() + rc.height()) - center.y()) + center.y(); + + dab->device->mirror(false, true); + dab->offset.ry() = mirrorY; + } + } + + void mirrorRect(Qt::Orientation dir, const QPoint ¢er, QRect *rc) + { + if (dir == Qt::Horizontal) { + const int mirrorX = -((rc->x() + rc->width()) - center.x()) + center.x(); + rc->moveLeft(mirrorX); + } else /* if (dir == Qt::Vertical) */ { + const int mirrorY = -((rc->y() + rc->height()) - center.y()) + center.y(); + rc->moveTop(mirrorY); + } + } } diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_3.png b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_3.png new file mode 100644 index 0000000000000000000000000000000000000000..239c08da0690f751b001fcc8a00595de06884168 GIT binary patch literal 322 zc%17D@N?(olHy`uVBq!ia0vp^ZXnFT1|$ph9<=}|&H|6fVg?4jBOuH;Rhv&5DEP(G z#WAE}&f8lTIS(7~Fa+){`9Jk8<0@v>l^ea^)oxdjabi(V_L!vN>2I*vJ|=lh{5_7D z;_o(I&OYw?zcH}^2oLQ3^8V_c{^!$drHhgpz(NkWKiSfrI-1^lx|274*9UWTHVH7? zqJRHHmfgmTdz%$uLU%URC~p;RE10QnUi0phACCbTuF$=o(Jv}j4)D03CARs&Yv;A-WGqJXaDc&KRF{s!=@t~LdqUzZUxC(gvqTr z{+*+^oX@*{$Ke*Rc#neN?diWx?EHE5ukW|-6F55sRopd#2p7G~Wp*0J3$)X}(jvIsaV#tNY)b4N)hk^6b!3 zyT9kE-wW6Nxw04Tx*-2om-A{~-Glq#=hC{HD}Q&-fN4|PemaVDtDnm{r-UW|pf1a9 literal 0 Hc$@@&t*-BOEHv zzIJuLalSpp{@=uZdzUyRu|OnZE?o&1d)~nx@z2xZ$N7hMrtY83)hX!F;{a4(eEG`# z?wDl-(;0>&9#3u>HkhU@vVKo?xQlorb&;#e>E4eQ z?_c!tcjxCnUJX$VlwCRZSK{I5Un||e2=_k=g}7CD!i<{W`UI}pimG>Va5H@Nt(B#P@Xdb6Mw<&;$S@ Cmds)R literal 0 Hc$@e{sif z?$3XuW|8}pzfZq@;%js19sxrZr8Wnkc{47mea+zC(`R+>U(KI;?e(?=d)KXRb~Z1b zzM^Sc<()<8Eggzb#Y@z`ehGe|eo^j6XEvwyhXkhWdEa;3Ey>8=cmMaM)mPp(U6zf^ z>)$tHZu+lYX=N9lMu^Tdt<%I%1ETHhS^kflyocYpqmWxp5P4!ocia>*Fsr(^aj zm(O?lUi~2SitdU7i#cnqmzCejm9J~P-&LEp^VstTyKbMH+L?56?;jZ?uj8UI{GFsCP!Yr>RoxE#n{i(GdksPu_>+6<`x}IqlCX4R4)csKcVZ^7YRZ)+>ADI2X^y|_e%kM8Y zUKzadyeiBoHDRwjKg#`B@?ZXJ29l=k;FZgN?waEz%M6Qqv0Xvug?`PlQg-2mgb@=s b#TZ-=Z}0KY-&+=@0^)kQ`njxgN@xNA(4#s0 literal 0 Hc$@ily>IOU@_+#0=yRiOMjmMQ*%ZC{$J@$i?^>AX66CITY7ht)MF1>LM@-9;(2NHUj}z} W-Ui{;7yE(!X7F_Nb6Mw<&;$U$3vwa= literal 0 Hc$@ugZr+s$L-hHij;s#3@*|!!z-3@)8-|@#CFZ}zg z_0N-(T1;hGHSKOcZ-EueQtk$U#hKMyC zIivgaV%oe4jro literal 0 Hc$@|DW(Q-wAAn+k_cky=(tzZ`)e_ zg+CE4cH3@>wB+5K{kNZ)D;hR|^`GhWuKiW{Gx}|*FvPT`BTL?0S#JB}@6QgH#S940 aAYRM(;Gl5N&6>IXAik%opUXO@geCx|3zh5u literal 0 Hc$@%-!J;QN_+{-E)VPSyWs=On^yV)ZO>C z`PJV(CrUe(`n$WAU0SB%ISGg?iryZvTe|w!%Exy;XMcJ<;b(C9miMeL%$XYv1tgdo zEpNVf^>6<_+W@&M@A)T*G9@b*uz`r#re7T<&)Qu&|9R=$YRP|xPp^`G{w2bc{~Q-Y zpP|5vi?geic*lM{6BOL@BvJjxo`>_q((^Nh|BUSn4h>48wt zbN|Zf`%d{?@AsNt+|vK}!S9dh&C^U&kxY2&y3^xJ$3M$iKQ_Ic`lcV@gdf*W|C%Cq z>fk5${GxN;cSoGx7O7|e^+ofomEqP8;^%F4O5kBR=8(YSG6@pq2*mq^^OlIlH`Wrgb8(); + KisPaintDeviceSP dst = new KisPaintDevice(cs); + + QList colors; + colors << Qt::red; + colors << Qt::green; + colors << Qt::blue; + + QRect devicesRect; + QList devices; + + for (int i = 0; i < numRects; i++) { + const QRect rc(10 + i * 10, 10 + i * 10, 30, 30); + KisFixedPaintDeviceSP dev = new KisFixedPaintDevice(cs); + dev->setRect(rc); + dev->initialize(); + dev->fill(rc, KoColor(colors[i % 3], cs)); + dev->fill(kisGrowRect(rc, -5), KoColor(Qt::white, cs)); + + KisRenderedDab dab; + dab.device = dev; + dab.offset = dev->bounds().topLeft(); + dab.opacity = varyOpacity ? qreal(1 + i) / numRects : 1.0; + dab.flow = 1.0; + + devices << dab; + devicesRect |= rc; + } + + KisSelectionSP selection; + + if (useSelection) { + selection = new KisSelection(); + selection->pixelSelection()->select(kisGrowRect(devicesRect, -7)); + } + + const QString opacityPostfix = varyOpacity ? "_varyop" : ""; + const QString selectionPostfix = useSelection ? "_sel" : ""; + + const QRect fullRect = kisGrowRect(devicesRect, 10); + + { + KisPainter painter(dst); + painter.setSelection(selection); + painter.bltFixed(fullRect, devices); + painter.end(); + QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect), + "kispainter_test", + "massive_bitblt", + QString("full_update_%1%2%3") + .arg(numRects) + .arg(opacityPostfix) + .arg(selectionPostfix))); + } + + dst->clear(); + + { + KisPainter painter(dst); + painter.setSelection(selection); + + for (int i = fullRect.x(); i <= fullRect.center().x(); i += 10) { + const QRect rc(i, fullRect.y(), 10, fullRect.height()); + painter.bltFixed(rc, devices); + } + + painter.end(); + + QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect), + "kispainter_test", + "massive_bitblt", + QString("partial_update_%1%2%3") + .arg(numRects) + .arg(opacityPostfix) + .arg(selectionPostfix))); + + } +} + +void KisPainterTest::testMassiveBltFixedSingleTile() +{ + testMassiveBltFixedImpl(3); +} + +void KisPainterTest::testMassiveBltFixedMultiTile() +{ + testMassiveBltFixedImpl(6); +} + +void KisPainterTest::testMassiveBltFixedMultiTileWithOpacity() +{ + testMassiveBltFixedImpl(6, true); +} + +void KisPainterTest::testMassiveBltFixedMultiTileWithSelection() +{ + testMassiveBltFixedImpl(6, false, true); +} + +void KisPainterTest::testMassiveBltFixedCornerCases() +{ + const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); + KisPaintDeviceSP dst = new KisPaintDevice(cs); + + QList devices; + + QVERIFY(dst->extent().isEmpty()); + + { + // empty devices, shouldn't crash + KisPainter painter(dst); + painter.bltFixed(QRect(60,60,20,20), devices); + painter.end(); + } + + QVERIFY(dst->extent().isEmpty()); + + const QRect rc(10,10,20,20); + KisFixedPaintDeviceSP dev = new KisFixedPaintDevice(cs); + dev->setRect(rc); + dev->initialize(); + dev->fill(rc, KoColor(Qt::white, cs)); + + devices.append(KisRenderedDab(dev)); + + { + // rect outside the devices bounds, shouldn't crash + KisPainter painter(dst); + painter.bltFixed(QRect(60,60,20,20), devices); + painter.end(); + } + + QVERIFY(dst->extent().isEmpty()); +} + + +#include "kis_paintop_utils.h" +#include "kis_algebra_2d.h" + +void benchmarkMassiveBltFixedImpl(int numDabs, int size, qreal spacing, int idealNumPatches, Qt::Orientations direction) +{ + const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); + KisPaintDeviceSP dst = new KisPaintDevice(cs); + + QList colors; + colors << QColor(255, 0, 0, 200); + colors << QColor(0, 255, 0, 200); + colors << QColor(0, 0, 255, 200); + + QRect devicesRect; + QList devices; + + const int step = spacing * size; + + for (int i = 0; i < numDabs; i++) { + const QRect rc = + direction == Qt::Horizontal ? QRect(10 + i * step, 0, size, size) : + direction == Qt::Vertical ? QRect(0, 10 + i * step, size, size) : + QRect(10 + i * step, 10 + i * step, size, size); + + KisFixedPaintDeviceSP dev = new KisFixedPaintDevice(cs); + dev->setRect(rc); + dev->initialize(); + dev->fill(rc, KoColor(colors[i % 3], cs)); + dev->fill(kisGrowRect(rc, -5), KoColor(Qt::white, cs)); + + KisRenderedDab dab; + dab.device = dev; + dab.offset = dev->bounds().topLeft(); + dab.opacity = 1.0; + dab.flow = 1.0; + + devices << dab; + devicesRect |= rc; + } + + const QRect fullRect = kisGrowRect(devicesRect, 10); + + { + KisPainter painter(dst); + painter.bltFixed(fullRect, devices); + painter.end(); + //QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect), + // "kispainter_test", + // "massive_bitblt_benchmark", + // "initial")); + dst->clear(); + } + + QElapsedTimer t; + + qint64 massiveTime = 0; + int massiveTries = 0; + int numRects = 0; + int avgPatchSize = 0; + + for (int i = 0; i < 50 || massiveTime > 5000000; i++) { + QVector rects = KisPaintOpUtils::splitDabsIntoRects(devices, idealNumPatches, size, spacing); + numRects = rects.size(); + + // HACK: please calculate real *average*! + avgPatchSize = KisAlgebra2D::maxDimension(rects.first()); + + t.start(); + + KisPainter painter(dst); + Q_FOREACH (const QRect &rc, rects) { + painter.bltFixed(rc, devices); + } + painter.end(); + + massiveTime += t.nsecsElapsed() / 1000; + massiveTries++; + dst->clear(); + } + + qint64 linearTime = 0; + int linearTries = 0; + + for (int i = 0; i < 50 || linearTime > 5000000; i++) { + t.start(); + + KisPainter painter(dst); + Q_FOREACH (const KisRenderedDab &dab, devices) { + painter.setOpacity(255 * dab.opacity); + painter.setFlow(255 * dab.flow); + painter.bltFixed(dab.offset, dab.device, dab.device->bounds()); + } + painter.end(); + + linearTime += t.nsecsElapsed() / 1000; + linearTries++; + dst->clear(); + } + + const qreal avgMassive = qreal(massiveTime) / massiveTries; + const qreal avgLinear = qreal(linearTime) / linearTries; + + const QString directionMark = + direction == Qt::Horizontal ? "H" : + direction == Qt::Vertical ? "V" : "D"; + + qDebug() + << "D:" << size + << "S:" << spacing + << "N:" << numDabs + << "P (px):" << avgPatchSize + << "R:" << numRects + << "Dir:" << directionMark + << "\t" + << qPrintable(QString("Massive (usec): %1").arg(QString::number(avgMassive, 'f', 2), 8)) + << "\t" + << qPrintable(QString("Linear (usec): %1").arg(QString::number(avgLinear, 'f', 2), 8)) + << (avgMassive < avgLinear ? "*" : " ") + << qPrintable(QString("%1") + .arg(QString::number((avgMassive - avgLinear) / avgLinear * 100.0, 'f', 2), 8)) + << qRound(size + size * spacing * (numDabs - 1)); +} + + +void KisPainterTest::benchmarkMassiveBltFixed() +{ + const qreal sp = 0.14; + const int idealThreadCount = 8; + + for (int d = 50; d < 301; d += 50) { + for (int n = 1; n < 150; n = qCeil(n * 1.5)) { + benchmarkMassiveBltFixedImpl(n, d, sp, idealThreadCount, Qt::Horizontal); + benchmarkMassiveBltFixedImpl(n, d, sp, idealThreadCount, Qt::Vertical); + benchmarkMassiveBltFixedImpl(n, d, sp, idealThreadCount, Qt::Vertical | Qt::Horizontal); + } + } +} QTEST_MAIN(KisPainterTest) diff --git a/libs/image/tests/kis_strokes_queue_test.h b/libs/image/tests/kis_strokes_queue_test.h --- a/libs/image/tests/kis_strokes_queue_test.h +++ b/libs/image/tests/kis_strokes_queue_test.h @@ -20,7 +20,8 @@ #define __KIS_STROKES_QUEUE_TEST_H #include - +#include "kis_types.h" +#include "kis_stroke_job_strategy.h" class KisStrokesQueueTest : public QObject { @@ -38,9 +39,12 @@ void testStrokesLevelOfDetail(); void testLodUndoBase(); void testLodUndoBase2(); + void testMutatedJobs(); + void testUniquelyConcurrentJobs(); private: struct LodStrokesQueueTester; + static void checkJobsOverlapping(LodStrokesQueueTester &t, KisStrokeId id, KisStrokeJobData::Sequentiality first, KisStrokeJobData::Sequentiality second, bool allowed); }; #endif /* __KIS_STROKES_QUEUE_TEST_H */ diff --git a/libs/image/tests/kis_strokes_queue_test.cpp b/libs/image/tests/kis_strokes_queue_test.cpp --- a/libs/image/tests/kis_strokes_queue_test.cpp +++ b/libs/image/tests/kis_strokes_queue_test.cpp @@ -430,6 +430,14 @@ VERIFY_EMPTY(jobs[1]); } + void processQueueNoContextClear() { + queue.processQueue(context, false); + + if (&context == &realContext) { + context.waitForDone(); + } + } + void processQueue() { processQueueNoAdd(); queue.processQueue(context, false); @@ -439,6 +447,31 @@ } } + void checkNothing() { + KIS_ASSERT(&context == &fakeContext); + + jobs = fakeContext.getJobs(); + VERIFY_EMPTY(jobs[0]); + VERIFY_EMPTY(jobs[1]); + } + + void checkJobs(const QStringList &list) { + KIS_ASSERT(&context == &fakeContext); + + jobs = fakeContext.getJobs(); + + for (int i = 0; i < 2; i++) { + if (list.size() <= i) { + VERIFY_EMPTY(jobs[i]); + } else { + QVERIFY(jobs[i]->isRunning()); + COMPARE_NAME(jobs[i], list[i]); + } + } + + QCOMPARE(queue.needsExclusiveAccess(), false); + } + void checkOnlyJob(const QString &name) { KIS_ASSERT(&context == &fakeContext); @@ -456,6 +489,18 @@ QCOMPARE(globalExecutedDabs.size(), 1); globalExecutedDabs.clear(); } + + void checkExecutedJobs(const QStringList &list) { + realContext.waitForDone(); + + QCOMPARE(globalExecutedDabs, list); + globalExecutedDabs.clear(); + } + + void checkNothingExecuted() { + realContext.waitForDone(); + QVERIFY(globalExecutedDabs.isEmpty()); + } }; @@ -466,6 +511,11 @@ // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); + + // process sync-lodn-planes stroke + t.processQueue(); + t.checkOnlyJob("sync_u_init"); + KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("lod_", false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); @@ -557,6 +607,11 @@ // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); + + // process sync-lodn-planes stroke + t.processQueue(); + t.checkOnlyJob("sync_u_init"); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id1); @@ -643,5 +698,141 @@ t.checkOnlyExecutedJob("resu_u_init"); } +void KisStrokesQueueTest::testMutatedJobs() +{ + LodStrokesQueueTester t(true); + KisStrokesQueue &queue = t.queue; + + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true, false, true)); + + queue.addJob(id1, + new KisTestingStrokeJobData( + KisStrokeJobData::CONCURRENT, + KisStrokeJobData::NORMAL, + true, "1")); + + queue.addJob(id1, + new KisTestingStrokeJobData( + KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::NORMAL, + false, "2")); + + queue.endStroke(id1); + + t.processQueue(); + + t.checkOnlyExecutedJob("str1_dab_1"); + + t.processQueue(); + + QStringList refList; + refList << "str1_dab_mutated" << "str1_dab_mutated"; + t.checkExecutedJobs(refList); + + t.processQueue(); + t.checkOnlyExecutedJob("str1_dab_mutated"); + + t.processQueue(); + t.checkOnlyExecutedJob("str1_dab_2"); + + t.processQueue(); + t.checkNothingExecuted(); +} + +QString sequentialityToString(KisStrokeJobData::Sequentiality seq) { + QString result = ""; + + switch (seq) { + case KisStrokeJobData::SEQUENTIAL: + result = "SEQUENTIAL"; + break; + case KisStrokeJobData::UNIQUELY_CONCURRENT: + result = "UNIQUELY_CONCURRENT"; + break; + case KisStrokeJobData::BARRIER: + result = "BARRIER"; + break; + case KisStrokeJobData::CONCURRENT: + result = "CONCURRENT"; + break; + } + + return result; +} + +void KisStrokesQueueTest::checkJobsOverlapping(LodStrokesQueueTester &t, + KisStrokeId id, + KisStrokeJobData::Sequentiality first, + KisStrokeJobData::Sequentiality second, + bool allowed) +{ + t.queue.addJob(id, new KisTestingStrokeJobData(first, + KisStrokeJobData::NORMAL, false, "first")); + t.processQueue(); + t.checkJobs({"str1_dab_first"}); + + t.queue.addJob(id, new KisTestingStrokeJobData(second, + KisStrokeJobData::NORMAL, false, "second")); + + qDebug() << QString(" test %1 after %2 allowed: %3 ") + .arg(sequentialityToString(second), 24) + .arg(sequentialityToString(first), 24) + .arg(allowed); + + if (allowed) { + t.processQueueNoContextClear(); + t.checkJobs({"str1_dab_first", "str1_dab_second"}); + } else { + t.processQueueNoContextClear(); + t.checkJobs({"str1_dab_first"}); + + t.processQueue(); + t.checkJobs({"str1_dab_second"}); + } + + t.processQueueNoAdd(); + t.checkNothing(); +} + +void KisStrokesQueueTest::testUniquelyConcurrentJobs() +{ + LodStrokesQueueTester t; + KisStrokesQueue &queue = t.queue; + + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true)); + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); + queue.endStroke(id1); + + { // manual test + t.processQueue(); + t.checkJobs({"str1_dab", "str1_dab"}); + + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); + t.processQueue(); + t.checkJobs({"str1_dab"}); + + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT, + KisStrokeJobData::NORMAL, false, "ucon")); + t.processQueueNoContextClear(); + t.checkJobs({"str1_dab", "str1_dab_ucon"}); + + t.processQueueNoAdd(); + t.checkNothing(); + } + + // Test various cases of overlapping + + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::CONCURRENT, true); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::SEQUENTIAL, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::BARRIER, false); + + checkJobsOverlapping(t, id1, KisStrokeJobData::CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT , true); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::UNIQUELY_CONCURRENT, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::BARRIER, KisStrokeJobData::UNIQUELY_CONCURRENT, false); +} + QTEST_MAIN(KisStrokesQueueTest) diff --git a/libs/image/tests/scheduler_utils.h b/libs/image/tests/scheduler_utils.h --- a/libs/image/tests/scheduler_utils.h +++ b/libs/image/tests/scheduler_utils.h @@ -101,7 +101,8 @@ globalExecutedDabs << m_name; } - QString name() { + virtual QString name(KisStrokeJobData *data) const { + Q_UNUSED(data); return m_name; } @@ -122,22 +123,71 @@ { public: KisTestingStrokeJobData(Sequentiality sequentiality = SEQUENTIAL, - Exclusivity exclusivity = NORMAL) - : KisStrokeJobData(sequentiality, exclusivity) + Exclusivity exclusivity = NORMAL, + bool addMutatedJobs = false, + const QString &customSuffix = QString()) + : KisStrokeJobData(sequentiality, exclusivity), + m_addMutatedJobs(addMutatedJobs), + m_customSuffix(customSuffix) { } KisTestingStrokeJobData(const KisTestingStrokeJobData &rhs) - : KisStrokeJobData(rhs) + : KisStrokeJobData(rhs), + m_addMutatedJobs(rhs.m_addMutatedJobs) { } KisStrokeJobData* createLodClone(int levelOfDetail) override { Q_UNUSED(levelOfDetail); return new KisTestingStrokeJobData(*this); } + + bool m_addMutatedJobs = false; + bool m_isMutated = false; + QString m_customSuffix; +}; + +class KisMutatableDabStrategy : public KisNoopDabStrategy +{ +public: + KisMutatableDabStrategy(const QString &name, KisStrokeStrategy *parentStrokeStrategy) + : KisNoopDabStrategy(name), + m_parentStrokeStrategy(parentStrokeStrategy) + { + } + + void run(KisStrokeJobData *data) override { + KisTestingStrokeJobData *td = dynamic_cast(data); + + if (td && td->m_isMutated) { + globalExecutedDabs << QString("%1_mutated").arg(name(data)); + } else if (td && td->m_addMutatedJobs) { + globalExecutedDabs << name(data); + + for (int i = 0; i < 3; i++) { + KisTestingStrokeJobData *newData = + new KisTestingStrokeJobData(td->sequentiality(), td->exclusivity(), false); + newData->m_isMutated = true; + m_parentStrokeStrategy->addMutatedJob(newData); + } + } else { + globalExecutedDabs << name(data); + } + } + + virtual QString name(KisStrokeJobData *data) const { + const QString baseName = KisNoopDabStrategy::name(data); + + KisTestingStrokeJobData *td = dynamic_cast(data); + return !td || td->m_customSuffix.isEmpty() ? baseName : QString("%1_%2").arg(baseName).arg(td->m_customSuffix); + } + +private: + KisStrokeStrategy *m_parentStrokeStrategy = 0; }; + class KisTestingStrokeStrategy : public KisStrokeStrategy { public: @@ -182,7 +232,7 @@ } KisStrokeJobStrategy* createDabStrategy() override { - return new KisNoopDabStrategy(m_prefix + "dab"); + return new KisMutatableDabStrategy(m_prefix + "dab", this); } KisStrokeStrategy* createLodClone(int levelOfDetail) override { @@ -214,9 +264,9 @@ inline QString getJobName(KisStrokeJob *job) { KisNoopDabStrategy *pointer = dynamic_cast(job->testingGetDabStrategy()); - Q_ASSERT(pointer); + KIS_ASSERT(pointer); - return pointer->name(); + return pointer->name(job->testingGetDabData()); } inline int cancelSeqNo(KisStrokeJob *job) { diff --git a/libs/image/tiles3/kis_tiled_data_manager.cc b/libs/image/tiles3/kis_tiled_data_manager.cc --- a/libs/image/tiles3/kis_tiled_data_manager.cc +++ b/libs/image/tiles3/kis_tiled_data_manager.cc @@ -385,7 +385,13 @@ if (clearTileRect == tileRect) { // Clear whole tile m_hashTable->deleteTile(column, row); - needsRecalculateExtent = true; + + if (!needsRecalculateExtent && + (m_extentMinX == tileRect.left() || m_extentMaxX == tileRect.right() || + m_extentMinY == tileRect.top() || m_extentMaxY == tileRect.bottom())) { + + needsRecalculateExtent = true; + } if (!pixelBytesAreDefault) { KisTileSP clearedTile = KisTileSP(new KisTile(column, row, td, m_mementoManager)); diff --git a/libs/pigment/KoColor.h b/libs/pigment/KoColor.h --- a/libs/pigment/KoColor.h +++ b/libs/pigment/KoColor.h @@ -44,17 +44,8 @@ { public: - static void init(); - /// Create an empty KoColor. It will be valid, but also black and transparent - KoColor() { - const KoColor * const prefab = s_prefab; - - // assert that KoColor::init was called and everything is set up properly. - KIS_ASSERT_X(prefab != nullptr, "KoColor::KoColor()", "KoColor not initialized yet."); - - *this = *prefab; - } + KoColor(); /// Create a null KoColor. It will be valid, but all channels will be set to 0 explicit KoColor(const KoColorSpace * colorSpace); @@ -230,8 +221,6 @@ const KoColorSpace *m_colorSpace; quint8 m_data[MAX_PIXEL_SIZE]; quint8 m_size; - - static const KoColor *s_prefab; }; Q_DECLARE_METATYPE(KoColor) diff --git a/libs/pigment/KoColor.cpp b/libs/pigment/KoColor.cpp --- a/libs/pigment/KoColor.cpp +++ b/libs/pigment/KoColor.cpp @@ -33,21 +33,35 @@ #include "KoColorSpaceRegistry.h" #include "KoChannelInfo.h" -const KoColor *KoColor::s_prefab = nullptr; +#include -void KoColor::init() +namespace { + +struct DeafultKoColorInitializer { - KIS_ASSERT(s_prefab == nullptr); - KoColor *prefab = new KoColor(KoColorSpaceRegistry::instance()->rgb16(0)); - prefab->m_colorSpace->fromQColor(Qt::black, prefab->m_data); - prefab->m_colorSpace->setOpacity(prefab->m_data, OPACITY_OPAQUE_U8, 1); - s_prefab = prefab; + DeafultKoColorInitializer() { + const KoColorSpace *defaultColorSpace = KoColorSpaceRegistry::instance()->rgb16(0); + KIS_ASSERT(defaultColorSpace); + + value = new KoColor(Qt::black, defaultColorSpace); #ifndef NODEBUG #ifndef QT_NO_DEBUG - // warn about rather expensive checks in assertPermanentColorspace(). - qWarning() << "KoColor debug runtime checks are active."; + // warn about rather expensive checks in assertPermanentColorspace(). + qWarning() << "KoColor debug runtime checks are active."; #endif #endif + } + + KoColor *value = 0; +}; + +Q_GLOBAL_STATIC(DeafultKoColorInitializer, s_defaultKoColor); + +} + + +KoColor::KoColor() { + *this = *s_defaultKoColor->value; } KoColor::KoColor(const KoColorSpace * colorSpace) diff --git a/libs/pigment/KoCompositeOp.h b/libs/pigment/KoCompositeOp.h --- a/libs/pigment/KoCompositeOp.h +++ b/libs/pigment/KoCompositeOp.h @@ -25,6 +25,8 @@ #include #include +#include + #include "kritapigment_export.h" class KoColorSpace; @@ -70,6 +72,8 @@ float* lastOpacity; QBitArray channelFlags; + void setOpacityAndAverage(float _opacity, float _averageOpacity); + void updateOpacityAndAverage(float value); private: inline void copy(const ParameterInfo &rhs); diff --git a/libs/pigment/KoCompositeOp.cpp b/libs/pigment/KoCompositeOp.cpp --- a/libs/pigment/KoCompositeOp.cpp +++ b/libs/pigment/KoCompositeOp.cpp @@ -60,6 +60,18 @@ return *this; } +void KoCompositeOp::ParameterInfo::setOpacityAndAverage(float _opacity, float _averageOpacity) +{ + if (qFuzzyCompare(_opacity, _averageOpacity)) { + opacity = _opacity; + lastOpacity = &opacity; + } else { + opacity = _opacity; + _lastOpacityData = _averageOpacity; + lastOpacity = &_lastOpacityData; + } +} + void KoCompositeOp::ParameterInfo::copy(const ParameterInfo &rhs) { dstRowStart = rhs.dstRowStart; diff --git a/libs/ui/CMakeLists.txt b/libs/ui/CMakeLists.txt --- a/libs/ui/CMakeLists.txt +++ b/libs/ui/CMakeLists.txt @@ -195,7 +195,9 @@ tool/kis_resources_snapshot.cpp tool/kis_smoothing_options.cpp tool/KisStabilizerDelayedPaintHelper.cpp + tool/KisStrokeSpeedMonitor.cpp tool/strokes/freehand_stroke.cpp + tool/strokes/KisStrokeEfficiencyMeasurer.cpp tool/strokes/kis_painter_based_stroke_strategy.cpp tool/strokes/kis_filter_stroke_strategy.cpp tool/strokes/kis_color_picker_stroke_strategy.cpp diff --git a/libs/ui/KisApplication.cpp b/libs/ui/KisApplication.cpp --- a/libs/ui/KisApplication.cpp +++ b/libs/ui/KisApplication.cpp @@ -421,10 +421,6 @@ processEvents(); addResourceTypes(); - // now we're set up, and the LcmsEnginePlugin will have access to resource paths for color management, - // we can finally initialize KoColor. - KoColor::init(); - // Load all resources and tags before the plugins do that loadResources(); diff --git a/libs/ui/KisViewManager.h b/libs/ui/KisViewManager.h --- a/libs/ui/KisViewManager.h +++ b/libs/ui/KisViewManager.h @@ -212,6 +212,8 @@ /// with a non-null value. To make it return shell() again, simply pass null to this function. void setQtMainWindow(QMainWindow* newMainWindow); + static void initializeResourceManager(KoCanvasResourceManager *resourceManager); + public Q_SLOTS: void switchCanvasOnly(bool toggled); diff --git a/libs/ui/KisViewManager.cpp b/libs/ui/KisViewManager.cpp --- a/libs/ui/KisViewManager.cpp +++ b/libs/ui/KisViewManager.cpp @@ -194,14 +194,7 @@ , actionAuthor(0) , showPixelGrid(0) { - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisCompositeOpResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisEffectiveCompositeOpResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisOpacityResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisFlowResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisSizeResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisLodAvailabilityResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisEraserModeResourceConverter)); - canvasResourceManager.addResourceUpdateMediator(toQShared(new KisPresetUpdateMediator)); + KisViewManager::initializeResourceManager(&canvasResourceManager); } public: @@ -264,7 +257,6 @@ bool blockUntilOperationsFinishedImpl(KisImageSP image, bool force); }; - KisViewManager::KisViewManager(QWidget *parent, KActionCollection *_actionCollection) : d(new KisViewManagerPrivate(this, _actionCollection, parent)) { @@ -345,6 +337,18 @@ delete d; } +void KisViewManager::initializeResourceManager(KoCanvasResourceManager *resourceManager) +{ + resourceManager->addDerivedResourceConverter(toQShared(new KisCompositeOpResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisEffectiveCompositeOpResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisOpacityResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisFlowResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisSizeResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisLodAvailabilityResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisEraserModeResourceConverter)); + resourceManager->addResourceUpdateMediator(toQShared(new KisPresetUpdateMediator)); +} + KActionCollection *KisViewManager::actionCollection() const { return d->actionCollection; @@ -875,7 +879,6 @@ d->mainWindow = newMainWindow; } - void KisViewManager::slotDocumentSaved() { d->saveIncremental->setEnabled(true); diff --git a/libs/ui/canvas/kis_abstract_canvas_widget.h b/libs/ui/canvas/kis_abstract_canvas_widget.h --- a/libs/ui/canvas/kis_abstract_canvas_widget.h +++ b/libs/ui/canvas/kis_abstract_canvas_widget.h @@ -52,6 +52,7 @@ virtual void drawDecorations(QPainter & gc, const QRect &updateWidgetRect) const = 0; virtual void addDecoration(KisCanvasDecorationSP deco) = 0; + virtual void removeDecoration(const QString& id) = 0; virtual KisCanvasDecorationSP decoration(const QString& id) const = 0; diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_animation_player.cpp --- a/libs/ui/canvas/kis_animation_player.cpp +++ b/libs/ui/canvas/kis_animation_player.cpp @@ -37,33 +37,25 @@ #include "kis_signal_compressor.h" #include #include - -#include -#include -#include - #include "KisSyncedAudioPlayback.h" #include "kis_signal_compressor_with_param.h" #include "KisViewManager.h" #include "kis_icon_utils.h" #include "KisPart.h" #include "dialogs/KisAsyncAnimationCacheRenderDialog.h" - - -using namespace boost::accumulators; -typedef accumulator_set > FpsAccumulator; +#include "KisRollingMeanAccumulatorWrapper.h" struct KisAnimationPlayer::Private { public: Private(KisAnimationPlayer *_q) : q(_q), - realFpsAccumulator(tag::rolling_window::window_size = 24), - droppedFpsAccumulator(tag::rolling_window::window_size = 24), - droppedFramesPortion(tag::rolling_window::window_size = 24), + realFpsAccumulator(24), + droppedFpsAccumulator(24), + droppedFramesPortion(24), dropFramesMode(true), nextFrameExpectedTime(0), expectedInterval(0), @@ -92,9 +84,9 @@ KisSignalAutoConnectionsStore cancelStrokeConnections; QElapsedTimer realFpsTimer; - FpsAccumulator realFpsAccumulator; - FpsAccumulator droppedFpsAccumulator; - FpsAccumulator droppedFramesPortion; + KisRollingMeanAccumulatorWrapper realFpsAccumulator; + KisRollingMeanAccumulatorWrapper droppedFpsAccumulator; + KisRollingMeanAccumulatorWrapper droppedFramesPortion; bool dropFramesMode; @@ -491,8 +483,8 @@ } #ifdef PLAYER_DEBUG_FRAMERATE - qDebug() << " RFPS:" << 1000.0 / rolling_mean(m_d->realFpsAccumulator) - << "DFPS:" << 1000.0 / rolling_mean(m_d->droppedFpsAccumulator) << ppVar(numFrames); + qDebug() << " RFPS:" << 1000.0 / m_d->realFpsAccumulator.rollingMean() + << "DFPS:" << 1000.0 / m_d->droppedFpsAccumulator.rollingMean() << ppVar(numFrames); #endif /* PLAYER_DEBUG_FRAMERATE */ } @@ -502,17 +494,17 @@ qreal KisAnimationPlayer::effectiveFps() const { - return 1000.0 / rolling_mean(m_d->droppedFpsAccumulator); + return 1000.0 / m_d->droppedFpsAccumulator.rollingMean(); } qreal KisAnimationPlayer::realFps() const { - return 1000.0 / rolling_mean(m_d->realFpsAccumulator); + return 1000.0 / m_d->realFpsAccumulator.rollingMean(); } qreal KisAnimationPlayer::framesDroppedPortion() const { - return rolling_mean(m_d->droppedFramesPortion); + return m_d->droppedFramesPortion.rollingMean(); } void KisAnimationPlayer::slotCancelPlayback() diff --git a/libs/ui/canvas/kis_canvas2.h b/libs/ui/canvas/kis_canvas2.h --- a/libs/ui/canvas/kis_canvas2.h +++ b/libs/ui/canvas/kis_canvas2.h @@ -295,6 +295,8 @@ // TODO: see to avoid that void setup(); + void initializeFpsDecoration(); + private: friend class KisView; // calls setup() class KisCanvas2Private; diff --git a/libs/ui/canvas/kis_canvas2.cpp b/libs/ui/canvas/kis_canvas2.cpp --- a/libs/ui/canvas/kis_canvas2.cpp +++ b/libs/ui/canvas/kis_canvas2.cpp @@ -88,6 +88,8 @@ #include "kis_canvas_updates_compressor.h" #include "KoZoomController.h" +#include +#include "opengl/kis_opengl_canvas_debugger.h" class Q_DECL_HIDDEN KisCanvas2::KisCanvas2Private { @@ -222,6 +224,28 @@ globalShapeManager()->selection(), SIGNAL(currentLayerChanged(const KoShapeLayer*))); connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDoCanvasUpdate())); + + initializeFpsDecoration(); +} + +void KisCanvas2::initializeFpsDecoration() +{ + KisConfig cfg; + + const bool shouldShowDebugOverlay = + (canvasIsOpenGL() && cfg.enableOpenGLFramerateLogging()) || + cfg.enableBrushSpeedLogging(); + + if (shouldShowDebugOverlay && !decoration(KisFpsDecoration::idTag)) { + addDecoration(new KisFpsDecoration(imageView())); + + if (cfg.enableBrushSpeedLogging()) { + connect(KisStrokeSpeedMonitor::instance(), SIGNAL(sigStatsUpdated()), this, SLOT(updateCanvas())); + } + } else if (!shouldShowDebugOverlay && decoration(KisFpsDecoration::idTag)) { + m_d->canvasWidget->removeDecoration(KisFpsDecoration::idTag); + disconnect(KisStrokeSpeedMonitor::instance(), SIGNAL(sigStatsUpdated()), this, SLOT(updateCanvas())); + } } KisCanvas2::~KisCanvas2() @@ -440,10 +464,6 @@ m_d->frameCache = KisAnimationFrameCache::getFrameCache(canvasWidget->openGLImageTextures()); setCanvasWidget(canvasWidget); - - if (canvasWidget->needsFpsDebugging() && !decoration(KisFpsDecoration::idTag)) { - addDecoration(new KisFpsDecoration(imageView())); - } } void KisCanvas2::createCanvas(bool useOpenGL) @@ -837,6 +857,7 @@ resetCanvas(cfg.useOpenGL()); slotSetDisplayProfile(cfg.displayProfile(QApplication::desktop()->screenNumber(this->canvasWidget()))); + initializeFpsDecoration(); } void KisCanvas2::refetchDataFromImage() diff --git a/libs/ui/canvas/kis_canvas_updates_compressor.cpp b/libs/ui/canvas/kis_canvas_updates_compressor.cpp --- a/libs/ui/canvas/kis_canvas_updates_compressor.cpp +++ b/libs/ui/canvas/kis_canvas_updates_compressor.cpp @@ -25,33 +25,26 @@ if (newUpdateRect.isEmpty()) return false; QMutexLocker l(&m_mutex); - bool updateOverridden = false; - UpdateInfoList::iterator it = m_updatesList.begin(); + while (it != m_updatesList.end()) { if (levelOfDetail == (*it)->levelOfDetail() && newUpdateRect.contains((*it)->dirtyImageRect())) { - if (info) { - *it = info; - info = 0; - ++it; - } else { - it = m_updatesList.erase(it); - } - - updateOverridden = true; + /** + * We should always remove the overridden update and put 'info' to the end + * of the queue. Otherwise, the updates will become reordered and the canvas + * may have tiles artifacts with "outdated" data + */ + it = m_updatesList.erase(it); } else { ++it; } } - if (!updateOverridden) { - Q_ASSERT(info); - m_updatesList.append(info); - } + m_updatesList.append(info); - return !updateOverridden; + return m_updatesList.size() <= 1; } KisUpdateInfoSP KisCanvasUpdatesCompressor::takeUpdateInfo() diff --git a/libs/ui/canvas/kis_canvas_widget_base.h b/libs/ui/canvas/kis_canvas_widget_base.h --- a/libs/ui/canvas/kis_canvas_widget_base.h +++ b/libs/ui/canvas/kis_canvas_widget_base.h @@ -56,6 +56,7 @@ void drawDecorations(QPainter & gc, const QRect &updateWidgetRect) const override; void addDecoration(KisCanvasDecorationSP deco) override; + void removeDecoration(const QString& id) override; KisCanvasDecorationSP decoration(const QString& id) const override; void setDecorations(const QList &) override; diff --git a/libs/ui/canvas/kis_canvas_widget_base.cpp b/libs/ui/canvas/kis_canvas_widget_base.cpp --- a/libs/ui/canvas/kis_canvas_widget_base.cpp +++ b/libs/ui/canvas/kis_canvas_widget_base.cpp @@ -162,6 +162,16 @@ m_d->decorations.push_back(deco); } +void KisCanvasWidgetBase::removeDecoration(const QString &id) +{ + for (auto it = m_d->decorations.begin(); it != m_d->decorations.end(); ++it) { + if ((*it)->id() == id) { + it = m_d->decorations.erase(it); + break; + } + } +} + KisCanvasDecorationSP KisCanvasWidgetBase::decoration(const QString& id) const { Q_FOREACH (KisCanvasDecorationSP deco, m_d->decorations) { diff --git a/libs/ui/dialogs/kis_dlg_preferences.cc b/libs/ui/dialogs/kis_dlg_preferences.cc --- a/libs/ui/dialogs/kis_dlg_preferences.cc +++ b/libs/ui/dialogs/kis_dlg_preferences.cc @@ -739,6 +739,7 @@ { KisConfig cfg2; chkOpenGLFramerateLogging->setChecked(cfg2.enableOpenGLFramerateLogging(requestDefault)); + chkBrushSpeedLogging->setChecked(cfg2.enableBrushSpeedLogging(requestDefault)); chkDisableVectorOptimizations->setChecked(cfg2.enableAmdVectorizationWorkaround(requestDefault)); } } @@ -765,6 +766,7 @@ { KisConfig cfg2; cfg2.setEnableOpenGLFramerateLogging(chkOpenGLFramerateLogging->isChecked()); + cfg2.setEnableBrushSpeedLogging(chkBrushSpeedLogging->isChecked()); cfg2.setEnableAmdVectorizationWorkaround(chkDisableVectorOptimizations->isChecked()); } } diff --git a/libs/ui/forms/wdgperformancesettings.ui b/libs/ui/forms/wdgperformancesettings.ui --- a/libs/ui/forms/wdgperformancesettings.ui +++ b/libs/ui/forms/wdgperformancesettings.ui @@ -6,8 +6,8 @@ 0 0 - 490 - 400 + 505 + 446 @@ -346,6 +346,13 @@ + + + Debug logging for brush rendering speed + + + + Disable vector optimizations (for AMD CPUs) diff --git a/libs/ui/kis_config.h b/libs/ui/kis_config.h --- a/libs/ui/kis_config.h +++ b/libs/ui/kis_config.h @@ -496,6 +496,9 @@ void setEnableOpenGLFramerateLogging(bool value) const; bool enableOpenGLFramerateLogging(bool defaultValue = false) const; + void setEnableBrushSpeedLogging(bool value) const; + bool enableBrushSpeedLogging(bool defaultValue = false) const; + void setEnableAmdVectorizationWorkaround(bool value); bool enableAmdVectorizationWorkaround(bool defaultValue = false) const; diff --git a/libs/ui/kis_config.cc b/libs/ui/kis_config.cc --- a/libs/ui/kis_config.cc +++ b/libs/ui/kis_config.cc @@ -1755,6 +1755,16 @@ m_cfg.writeEntry("enableOpenGLFramerateLogging", value); } +bool KisConfig::enableBrushSpeedLogging(bool defaultValue) const +{ + return (defaultValue ? false : m_cfg.readEntry("enableBrushSpeedLogging", false)); +} + +void KisConfig::setEnableBrushSpeedLogging(bool value) const +{ + m_cfg.writeEntry("enableBrushSpeedLogging", value); +} + void KisConfig::setEnableAmdVectorizationWorkaround(bool value) { m_cfg.writeEntry("amdDisableVectorWorkaround", value); diff --git a/libs/ui/kis_fps_decoration.cpp b/libs/ui/kis_fps_decoration.cpp --- a/libs/ui/kis_fps_decoration.cpp +++ b/libs/ui/kis_fps_decoration.cpp @@ -22,6 +22,7 @@ #include "kis_canvas2.h" #include "kis_coordinates_converter.h" #include "opengl/kis_opengl_canvas_debugger.h" +#include const QString KisFpsDecoration::idTag = "fps_decoration"; @@ -38,7 +39,7 @@ void KisFpsDecoration::drawDecoration(QPainter& gc, const QRectF& /*updateRect*/, const KisCoordinatesConverter */*converter*/, KisCanvas2* /*canvas*/) { #ifdef Q_OS_OSX - QPixmap pixmap(256, 64); + QPixmap pixmap(320, 128); pixmap.fill(Qt::transparent); { QPainter painter(&pixmap); @@ -50,15 +51,49 @@ #endif } + + void KisFpsDecoration::draw(QPainter& gc) { - const qreal value = KisOpenglCanvasDebugger::instance()->accumulatedFps(); - const QString text = QString("FPS: %1").arg(QString::number(value, 'f', 1)); + QStringList lines; + + if (KisOpenglCanvasDebugger::instance()->showFpsOnCanvas()) { + const qreal value = KisOpenglCanvasDebugger::instance()->accumulatedFps(); + lines << QString("Canvas FPS: %1").arg(QString::number(value, 'f', 1)); + } + + KisStrokeSpeedMonitor *monitor = KisStrokeSpeedMonitor::instance(); + + if (monitor->haveStrokeSpeedMeasurement()) { + lines << QString("Last cursor/brush speed (px/ms): %1/%2%3") + .arg(monitor->lastCursorSpeed(), 0, 'f', 1) + .arg(monitor->lastRenderingSpeed(), 0, 'f', 1) + .arg(monitor->lastStrokeSaturated() ? " (!)" : ""); + lines << QString("Last brush framerate: %1 fps") + .arg(monitor->lastFps(), 0, 'f', 1); + + lines << QString("Average cursor/brush speed (px/ms): %1/%2") + .arg(monitor->avgCursorSpeed(), 0, 'f', 1) + .arg(monitor->avgRenderingSpeed(), 0, 'f', 1); + lines << QString("Average brush framerate: %1 fps") + .arg(monitor->avgFps(), 0, 'f', 1); + } + + + QPoint startPoint(20,30); + const int lineSpacing = QFontMetrics(gc.font()).lineSpacing(); + gc.save(); - gc.setPen(QPen(Qt::white)); - gc.drawText(QPoint(21, 31), text); - gc.setPen(QPen(Qt::black)); - gc.drawText(QPoint(20, 30), text); + + Q_FOREACH (const QString &line, lines) { + gc.setPen(QPen(Qt::white)); + gc.drawText(startPoint + QPoint(1,1), line); + gc.setPen(QPen(Qt::black)); + gc.drawText(startPoint, line); + + startPoint += QPoint(0, lineSpacing); + } + gc.restore(); } diff --git a/libs/ui/opengl/kis_opengl_canvas2.h b/libs/ui/opengl/kis_opengl_canvas2.h --- a/libs/ui/opengl/kis_opengl_canvas2.h +++ b/libs/ui/opengl/kis_opengl_canvas2.h @@ -81,8 +81,6 @@ void renderDecorations(QPainter *painter); void paintToolOutline(const QPainterPath &path); - bool needsFpsDebugging() const; - public: // Implement kis_abstract_canvas_widget interface void setDisplayFilter(QSharedPointer displayFilter) override; void setWrapAroundViewingMode(bool value) override; diff --git a/libs/ui/opengl/kis_opengl_canvas2.cpp b/libs/ui/opengl/kis_opengl_canvas2.cpp --- a/libs/ui/opengl/kis_opengl_canvas2.cpp +++ b/libs/ui/opengl/kis_opengl_canvas2.cpp @@ -177,11 +177,6 @@ delete d; } -bool KisOpenGLCanvas2::needsFpsDebugging() const -{ - return KisOpenglCanvasDebugger::instance()->showFpsOnCanvas(); -} - void KisOpenGLCanvas2::setDisplayFilter(QSharedPointer displayFilter) { setDisplayFilterImpl(displayFilter, false); diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/ui/opengl/kis_opengl_canvas_debugger.h --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/ui/opengl/kis_opengl_canvas_debugger.h @@ -20,11 +20,12 @@ #define __KIS_OPENGL_CANVAS_DEBUGGER_H #include +#include - -class KisOpenglCanvasDebugger +class KisOpenglCanvasDebugger : public QObject { + Q_OBJECT public: KisOpenglCanvasDebugger(); ~KisOpenglCanvasDebugger(); @@ -37,6 +38,9 @@ void nofitySyncStatus(bool value); qreal accumulatedFps(); +private Q_SLOTS: + void slotConfigChanged(); + private: struct Private; const QScopedPointer m_d; diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.cpp b/libs/ui/opengl/kis_opengl_canvas_debugger.cpp --- a/libs/ui/opengl/kis_opengl_canvas_debugger.cpp +++ b/libs/ui/opengl/kis_opengl_canvas_debugger.cpp @@ -24,7 +24,7 @@ #include #include "kis_config.h" - +#include struct KisOpenglCanvasDebugger::Private { @@ -52,12 +52,8 @@ KisOpenglCanvasDebugger::KisOpenglCanvasDebugger() : m_d(new Private) { - KisConfig cfg; - m_d->isEnabled = cfg.enableOpenGLFramerateLogging(); - - if (m_d->isEnabled) { - m_d->time.start(); - } + connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); + slotConfigChanged(); } KisOpenglCanvasDebugger::~KisOpenglCanvasDebugger() @@ -86,6 +82,16 @@ return value; } +void KisOpenglCanvasDebugger::slotConfigChanged() +{ + KisConfig cfg; + m_d->isEnabled = cfg.enableOpenGLFramerateLogging(); + + if (m_d->isEnabled) { + m_d->time.start(); + } +} + void KisOpenglCanvasDebugger::nofityPaintRequested() { if (!m_d->isEnabled) return; diff --git a/libs/ui/tests/CMakeLists.txt b/libs/ui/tests/CMakeLists.txt --- a/libs/ui/tests/CMakeLists.txt +++ b/libs/ui/tests/CMakeLists.txt @@ -109,6 +109,11 @@ LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( + FreehandStrokeBenchmark.cpp ${CMAKE_SOURCE_DIR}/sdk/tests/stroke_testing_utils.cpp + TEST_NAME krita-ui-FreehandStrokeBenchmark + LINK_LIBRARIES kritaui kritaimage Qt5::Test) + +krita_add_broken_unit_test( fill_processing_visitor_test.cpp ${CMAKE_SOURCE_DIR}/sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-ui-FillProcessingVisitorTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/ui/tests/FreehandStrokeBenchmark.h copy from libs/image/kis_projection_updates_filter.cpp copy to libs/ui/tests/FreehandStrokeBenchmark.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/ui/tests/FreehandStrokeBenchmark.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,23 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef FREEHANDSTROKEBENCHMARK_H +#define FREEHANDSTROKEBENCHMARK_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class FreehandStrokeBenchmark : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void initTestCase(); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} + void testDefaultTip(); + void testSoftTip(); + void testGaussianTip(); + void testStampTip(); + + void testColorsmudgeDefaultTip(); +}; + +#endif // FREEHANDSTROKEBENCHMARK_H diff --git a/libs/ui/tests/FreehandStrokeBenchmark.cpp b/libs/ui/tests/FreehandStrokeBenchmark.cpp new file mode 100644 --- /dev/null +++ b/libs/ui/tests/FreehandStrokeBenchmark.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2017 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 "FreehandStrokeBenchmark.h" + +#include +#include +#include +#include "stroke_testing_utils.h" +#include "strokes/freehand_stroke.h" +#include "kis_resources_snapshot.h" +#include "kis_image.h" +#include + +class FreehandStrokeBenchmarkTester : public utils::StrokeTester +{ +public: + FreehandStrokeBenchmarkTester(const QString &presetFilename) + : StrokeTester("freehand_benchmark", QSize(5000, 5000), presetFilename) + { + setBaseFuzziness(3); + } + + void setCpuCoresLimit(int value) { + m_cpuCoresLimit = value; + } + +protected: + using utils::StrokeTester::initImage; + void initImage(KisImageWSP image, KisNodeSP activeNode) override { + Q_UNUSED(activeNode); + + if (m_cpuCoresLimit > 0) { + image->setWorkingThreadsLimit(m_cpuCoresLimit); + } + } + + KisStrokeStrategy* createStroke(bool indirectPainting, + KisResourcesSnapshotSP resources, + KisImageWSP image) override { + Q_UNUSED(image); + + FreehandStrokeStrategy::PainterInfo *painterInfo = + new FreehandStrokeStrategy::PainterInfo(); + + QScopedPointer stroke( + new FreehandStrokeStrategy(indirectPainting, COMPOSITE_ALPHA_DARKEN, resources, painterInfo, kundo2_noi18n("Freehand Stroke"))); + + return stroke.take(); + } + + void addPaintingJobs(KisImageWSP image, + KisResourcesSnapshotSP resources) override + { + addPaintingJobs(image, resources, 0); + } + + void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources, int iteration) override { + Q_UNUSED(iteration); + Q_UNUSED(resources); + + for (int y = 100; y < 4900; y += 300) { + KisPaintInformation pi1; + KisPaintInformation pi2; + + pi1 = KisPaintInformation(QPointF(100, y), 0.5); + pi2 = KisPaintInformation(QPointF(4900, y + 100), 1.0); + + QScopedPointer data( + new FreehandStrokeStrategy::Data(0, pi1, pi2)); + + image->addJob(strokeId(), data.take()); + } + + image->addJob(strokeId(), new FreehandStrokeStrategy::UpdateData(true)); + } + +private: + FreehandStrokeStrategy::PainterInfo *m_painterInfo; + int m_cpuCoresLimit = -1; +}; + +void benchmarkBrush(const QString &presetName) +{ + FreehandStrokeBenchmarkTester tester(presetName); + + for (int i = 1; i <= QThread::idealThreadCount(); i++) { + tester.setCpuCoresLimit(i); + tester.benchmark(); + + qDebug() << qPrintable(QString("Cores: %1 Time: %2 (ms)").arg(i).arg(tester.lastStrokeTime())); + } +} + +#include + +void FreehandStrokeBenchmark::initTestCase() +{ + KoResourcePaths::addResourceType("kis_brushes", "data", FILES_DATA_DIR); +} + +void FreehandStrokeBenchmark::testDefaultTip() +{ + benchmarkBrush("testing_1000px_auto_deafult.kpp"); +} + +void FreehandStrokeBenchmark::testSoftTip() +{ + benchmarkBrush("testing_1000px_auto_soft.kpp"); +} + +void FreehandStrokeBenchmark::testGaussianTip() +{ + benchmarkBrush("testing_1000px_auto_gaussian.kpp"); +} + +void FreehandStrokeBenchmark::testStampTip() +{ + benchmarkBrush("testing_1000px_stamp_450_rotated.kpp"); +} + +void FreehandStrokeBenchmark::testColorsmudgeDefaultTip() +{ + benchmarkBrush("testing_200px_colorsmudge_default.kpp"); +} + +QTEST_MAIN(FreehandStrokeBenchmark) diff --git a/libs/ui/tests/data/3_texture.png b/libs/ui/tests/data/3_texture.png new file mode 100644 index 0000000000000000000000000000000000000000..85fa56d26d364b733ce97af0b4547293258c331b GIT binary patch literal 168916 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৹` z=a8zx^}n;$$r}Uvdg;*gyd6AiKNJ9NAxyacBGi*#8|uL0@ow|<(0Zr};*j+CKKvMc z2kruJf2t9z%lcp!8|9mQRyatABZBYPxEsb{bGgFY%GI4mU(Ajo{Y5?>BqQC_9L*F@ z)KrBpWp%q=H#qG=7u#*K!&7%&RbLf#polo;a=VRw5L4ICltq0*JeT1^2fjjTS;Soc ziXywJ$7D+ODMht*`?FI){bJk73S>l*zHNnbD|EVvM%JTC-2|>Bbz(;9!_mOQsPXZaoHd%~$YZ6E@BY zxZK>B>Ur!BT935j9Q>fya=gH%=h`@$UQTRCXg=&?3IBODCz1A%7N;F#+R{|EVOni| zIm!w?pA<5zM+^Os_OM?Ax&`5{$+$LI$nLd77&&CB`buGrgIhdY2-ZWJo=~47U>tkC?MUPan zfrfq{U@V21fh3BVk+^}FsWRekIjG9Cna+X1{_3rU8MFGPUlCq81K(o*^QlS2AG`Hz zd+cXcBr45xb2w?2B&>z8?vJ)8dqu)Y$kf<~5(l7s|u&m@}G>dP! zm;ja-yiku#YQo-}fNX*pr6jR4cZ@qQoJu{TX|kkz9u~V(>4UcnFyQ09-!_+!Rk;Ik z1g|cA1hk4b-_YIe?Y-gOfXi2~2dAu9H;@uQWi!@FCoLddC!W%$Zf-{(x|(xA%Cxl0F`m1xg?=`DoYIAlph* zDe)QNyjBbu4P%E5AV4~)O63ufcOs^pX%p7j%gTyRPZn-+%W@`OkJdC_uf&gW{@H+a z;m-#>wmX=!>nPJrg)UJ!MnZ&vN#sGy|3+9D#U9Dn4EQQea_6vq?t|5s) zsF_(P66*?Qzb5&NDbSfe5ycf2%^#0izMuf2gA(E_x2$(M4$TX(@@Q1@amAtAo79A` zd0d@0M1I?6Y(=c(gDGPl=e##K(#33_ORZ?RtJZFTJc>b$48u}TE0hJn&#CBBgdCqu z?+Q6OirgrB+!D(=5joGR81~jRc!ZK18f_S7_dx&|BbG-)$jGMI|Sr2WXFtulVp0@B{h}RTYO+1 zn4Tei35NokW0iq1Ono}t&myS>e~n+_2(y6f%+P8ozS4?)Fw&9r7f zCsoxu-1X5E`-V%{YsbU89r}(Qm&&pr)HJRe{jrXC8l0an{=_g3)NzGy9ClsTZRZmg zC9o`-jQ=2ZM`F6@n_>uTD>pMm1K=A=giyEV&GfEQsL+V$2GBwp1e#6-nb{m=q!Phf zosr)Z{4?NTit zlk&cdAG7B_D&%}DX(b05$#=Rg+8+>n3XDx`*vKb+9{ zz2A@F*@-bX?{v8}#W3?#w~_m8zhYtRB~$rj2wT0(}!4<4XW(|K-A|vQRF@FL?WJFwoAd zZr)?b>LM*3$WxX(85@F2o0rNC_ZV#gXPWU!I%5c1B3b*rPnh!(mtPYwho2EW3f{>6G$|-J^&GmWvUHN^aE)z%!F&?4P=h)653+O;m+4 zP0@6nGK`l^q6ql%j7HQSpUXiHO_L3r#UCoO4m?{V={S0;BW&fX1Xi0l>$FzMIQGIeSUt|jWXnDxKrmMUm3BH#r5f$)t6t7b79Eky8#23yzD>< z-;ul*9?6g2fBo{ts=@Hw_XCF8N>8NpJ`xH1gETc_nnL^j(>1l#gZfutsBKc&Ty3f#&^*On2 zHq7!|w4&!=II|%OY(jHI`2We)F=a=vv!PzrO>jJMWhw9W-=9xJ$=_0!NN;Q4s9w^# zmPj~PR=^d1i7tK7&%vcbG8LeWKF=4Ehq|@jrEEF@&@@4C48LQs?WOiZlj6#TqMAo0 zX)fC&7Joa>;4@7bN-}KRj~hF`u6Je~e-Pv=fbH-9B6D*x>^Gb48}YvO!FQZ4O43FZ zYoLk2+x@7j3O;M>)P`xCoy1PfcpovwC>JmPj`6-UT-EjEs^DB%)L1cL#5i9&$Msvz zh+hps)I0QuYNjo=Sb^+WFJ6wetm$6q*z)>*y$u9`m!OUVMwtF14(^Fbih7s|!pyXA zguVx=KE+R-Ew8cdjnaIez8f36E1j4Gs_H0lH$irDWvQ-VMPZ#x|55%681!B0d)a!K==;|y(Zz3m`K8Q);KwJfwNH<({P7pnY%1eBWDO}?g zL@9_h926xsc_h&9jch~HE3?7DjgD*=t;`EiDJp!mi{hdp$Y60L(D(H|psRfI?=8)v7FJ8LRd9bJ%?mi`F32u$F zo_B3!imq?_INtL2?~0tyZH)YqutVS;8e()S*#_RHxxB=@%*eq|*vd09Amt0kz}J7r z?rp{4C8Lb8u5}5Fj<(r$t!u%kOB|b`W5xfBr8XuJOv9emPMOfB;vcYO4Xi1`H?;!Z zHy@sj%#RoXCI?uhhRM&lf*{>u3V|*_u~Qzq8s3&IxW^L*fjV(;sejZ56K-q)~;MxY60APD5Yr+!F4`E?S) z+Umjv@nL^%1hU3<7*OT@6DZQpAwlEr!TRO%(A~)4D!bG2W?l8&YY@?r_)O_!Nfg_Qj}l!^EPo+l90! z+RkyF-?PiIUY^HkCgaCjIpV*p3r71GWJ;zPh+GGpWY8I&O+K^UA(jZ#v6U|Zq6*@R7V#*5!Rm$`Q`bbk-lskE&$q z%cp)sXv5a1$-k)do3JAG`(rWMIxjc)RDhNZ#%F_`?qi6keMrIN6|&TcmxFw~f>EV= zhH6l`Y?{D( zh8!*`poSx>l&*N2jS-YjvhyeKtk=DfZjR)XopZzjD_=ZW<>cMN--|g18;Z>gHLkOy z!0Hc<5?7@$VL#SmbAjX2(bH^+mNXc)TaNbM7GdmFCpmE3&-%b{4!EHMH zj$S9mfvn|IbyR`C)kUc&xnkQL6)7{nc3_TGZ#_HbQgxp@7$@x|WE>-TyiQ zWC#}=_bunIcUVm->|{pH2c7Naw{n&4!nFVA0lwjUwbu@CeS#?6-f@}lWm)=oz58f+ zOuhRyk~Jec#S|7A5UTD0wRM!khK80EWfQ~Z!)xUfz)(0(O)Ma08?&J=8Fiy6nGK+k z&-*6l`78yeJ#FQCUB@9`;_pAc6fKIYH;&~@&K!>O`#RztIWlK%<4>`ls%0DT%7VK1k^-4N53wFAf>^ZuXBsyxlscrK5ZdLdZT;oi^9o`1%!=T%Kl6)-?9=lP1Rj@@UM z#Zu|V1O?wA*54V(7GKPi47{V2?3^HY{KRUn;&>MC43^nSK<*sru#+QXg?fgRSQuv4 zP_=@ez65G8Gj+)vCooA}XX^SqHh$QaVQ^exTvryQ|2A;%^$$%X(;Z*~=#Jt@oBERv zmB0{V$P;#ieeo;G@w$TNMBR$ACk;y48#dt2;ZzdBoVl}%RQaGwmIq2oj_3q-^pt>a zIa6X18m=ZH`b$3AWXO$k7^ke`Fj}tRTf`O7sdjWQHI|ID6 z6_g)#qUd@d9vbpuad|jX5a&~bfNVLffa9!=H&ZaZx3+d!#VecxO(wutsfY^08r?kXZpr+@g@5nUbl&+E9SKDTm3TOgh5w zp!#*s;jcgsF)pKCq3*;6WlOW3sJ?J*;Nm(@js%(Xa5}2Pc!S-1RdTl~P$CG=ZM7)c z$)nBFmB(2Qo5pxj)OOmbQ@rhJPqS@MoPzcP&FgYTvf^2E;y{nf)#_6(xhOfz0$pt; z_h8Y-Ka>!w0ASHvc*;q~)c$4`W33Pu{=(N2RvyO&n^;!(8-M*}Y-*Bru^+>NjF5ed zT=S5p#s^bmDd(6To!~%`ntLL}o?%fr0y957n%nosNWtA@M+@XtKhOaIC`Tz~pmqpV z5b8)L8eaSNrfqL`=4QHhgxu`$bmp)77?)-mD{Dy$gDcF|mZ4@5GregGcAnyN$436x zKK`VD(SWeKr-YiW`vE9F8KPOY^fl#nl6eu`SS*eeb&ncuO;M*Gax>^wr0d6--;Y_P zXUv!Vvl7?zlbzq^mX(<5%J1H82eX<>j)gnhMeS__)rxF(;3xxHP&*`$=qPG=v30i8 z2&9Ndyg=mxG~*1l$~$Pr8N6|>K&2oO7n(J=ID#M$L43Uc1WJ}G*K9wAYuRVTN1m4xity;^J&j zgGo29lgWIRYPGChAwzelx=IZ-nC|api4xZmL;vOq$5ER!As|9(JA7-k0{@2!v%?PCSW#mbl%ZANh#SohUDGszFPyj_NR_TML@=e`g zL5S{pXQ}~hU&Nc#IgOa6S07Xh&$~d0tv!(U9OLbQiGuTRxm=~qlz%M2m)cS*mvz#R z-iJ1hW+x^jK<0{LCRJJ-BcG`835r}4xV@Z_JaNHy6tH?sdaIvmm88J!=z!L%T}pDJ z59ySmLUt{EkUH(XMT@V37@aCC130k?$sHs6dVIe*$-FO?Q!AB+nb%ZpirL$QmFvHR zvyvekMIEY`C}|f~Z7eN*ooZz>&o?t;`$jv{3t6UfUXSIWpMwIKzqiZ8Do%rT-{Y z<-&5{dCqj@JOj(Ij-(F7vYKVoW37;vHL735ZiNsCE$R$O=YRJ;0Zj+w8bk7`l9youy!9i)S*c2EGbf&3$j1dV0oR_jA;-m&0Daw|<=%K>W zl(8ikxy4p&{?Z+^`JthKfjsy*P+^e2054|5(JVG@zTF`rP9?M-$ENJ#|5UjLXnXpXxMCPu zlo6z!)CHOpnE5R;3gkw%*xeevI*CAtr0@88bl6gMqyN0CM}WCFUC(0;*+sGKsD6Fpr)m_W-CguFBJWmGG9J}_0x=O6@4TD>u`98v1lK14&#Hb}ir zyQ6UGYP-WJ;J92zE5juDnbWoJrPTAK4?Um@8 zi`cpM@oerfkn$OdhoNsiNWq+x8SG4)%c6eWMzZ~DIDd%L!cs-uvbC+(-CR5lA+eM^ zsTSe$65j7EXocF;%&dUvMVgKn%k8vjjyI~=kUu&Vrza_t{7OC$b2~<2?p1@AP2*j( zJI$dKs}t08oiIlQ>!wBt){oHWaY2+MEhT|E;<}G=^JhgZcdHrTqYiL{wT-8GpqkQ$ zLY4Bk_4D=b-@QIQ2={Fzsrltuquj(75a7`2;$TI1jX{bxDKb)%$vpz)_QVomSnZWn zV+#HzM^FM0LFSF8yMrO;+U(bm+(oy8|YAfRoRJ*fgT`xDBi zKkx4zRUGMLqp0rXvh>F?RA8Bh5u~$o1q{GsBs92CQ%Z7yYw7jI)_MNYPot>hm-7We z-uoK$Y6Md4gDxGF7=_x+_SAhW;B%EUN=i!IBegnR%&LruKA$fP+{U85Q#pJ;_i+bn zciOf`h}e$^-GPsOZCNKR9td>Mb!@sNt<{8VkFL{9Nu%zxdcD-DeH~5Hl)sr*Bkt4~ zQ&Aw&oz1NoB-}haH+D1tw$|0)1qy7=BMT#`W||+wO*KbNniNv7pU60|Dgl-1km62f zvpJS{ALMv^zSNr`+-eNM&=#%i2uYt-rwzxp+f-%QT$5gjTD3FTg9oB3@I|f ze8S2CZf2dKEbH$ld9Ddrkw~Pv_&~LGaA(>kyJrdvpNw;8q8?_mU#YH=^je@Cedt{a z^0-#nw7@W(8PqIzzfEWsMb>q=6?ay_p?2F$O0f+K0l7&TyHB+W>TznWA}s2Us| zx7vkj4;u&Pd4I*5l(bo)93jRiwYa@re(W6(jh8%mxe?Eu-_aX3s>{Kiw$V^tu^1Y*RHFMROww&o$pv_*|_HmjO zecaJ&XlTeAIwqoyQi+~s#oHs!KuV3Ggff#B=DU z<4n%)BMFEddn~=O67{deAktbK9$)OON^Smue|$Mm7X&)9u5hC1`tOFNX(p7K`9s0Q z`}PrF=vVn)XhijyesQplZm~M61ENk*gvoPQqQ~M2%@eqbup;rCA@e585}SL7$^r79 zM!r-i!YCT}!}1eh(plL;v?cMp#10ITym3q;BO`b`UKH>oFA#@)Sy0$}o(@}PMr9Hq z2F?{HEq)UcA_EQ9m`OZli6cT(X<-7zHj_E`Ax3&%?IP4r*f%F7T$j*#&e*V!sJcF9 zYsv)3i~dyA#qB1uIE2F0-jkkvF@&Iy-S!LT2yjwCX< z%(5~xs&QY%(#Tj>6$44^AnfDa9+V9S zd+v5aGS1NH@s*`{m^La@l&mQ-Y|T!6Ytv&BO1(L?1PqEfTVyA%VMxN`HJkK!@ADdK zP1c-PD~c+tI!YZ`11X8vd}680{>wM8(C!x{Y59?Cd)|-Wd~X7&y5nk(=`nzf<2kti znuT=F5EG&MxoEW7Z<}kqq=*lw+&bQnDi%?t5QB%X%m+)=cz}RIp-(kV3TT37j_8-l zEk-}l4K3@*rckwApik5htETfnznZYpQnr z&{~;;@-{9p{U(#v@26B^cH?^Iq!`?pQve=_dM;z`uf6Z~1JV!eB_QJL-U;G@zxO)&gUPsLsRO{=g4Thb^Re*Vr&X1|wW}6e9bCd#Io@CTQRf&oX`L0%)ua2HLo z|J~8@~3&y7row;@YeqIX; z4MlH#0sP0Jg|<`-h3v%BX>kS8akXksa1KpX)3^r5-K9}hB7}QsHN#;&Uat>Mub{x= zxF{mw1(BKN=wNuhk6C;D(j@6SU2a(q`92wHOP!r|d%TdN=WGsx&2yJ4=Echbtl{v6 zk3RI&U<2ni{-PYd&n%$)nvlzt%Mk~!K>%*t;l9Ov+xYnM zEgFM~K6I!&dSe{RN(H^CiA;<*MMPc|rj-yIFjAD`yL09lu1?q+@q@xHl|2ddFrW%q zsg(OT|Js9ehxL2@iB64(>+v|Ei*dkPI_y%Wk~&*p{Y4Gjga&2R$Qyxq_a^whB)4Wh zGOB%0T}=ihe?z;m*-@=8l&dW$x+bBlHd2>u}&-oF|M9A!*8gK00N*+1#H{Y088wL)uT zn}GqkTZ3J}sZX&&%4PJPD%3l8Q+dpI-b?`bG^Bdch`H2lnI~*nkzZvExzkk5E(r zbzd&4>IOj&@2fQoNol#9>WM)jrE(gT!|~9I`PrU3g5gE}kh9qlOD`><0b*47BYOz;q%LtnamjId+OiPsMmCYiaRpj=x{PS%GGpf zv!mH#u4GdTHAl;iVYA)V?}5jESEKEqpvBzWhTO(qZ9K(li5nKdl1T|(hA9Or zYzoMX_&UL^w7|T{x6_gSY_E^z3p!MyAeRX4`D4Wbflp$_*|Mn#RZ0;@Tar$CuUFxL z4py8YE+pR_E6AIstcK}<6lgeY3N?|Qm`_{RO}&v^B9_i?6CMVS_oB5z-GL+weWvJ8 zI^^|w>wx2;3nLK%5ZhC!^E`&@oHJ6bF&kkL%3`o`O2#w+Yn`W@tt`wUqiSRY@SSYA zBit~8@uT!Lz2}Fh?-B1xJQEV0=l!C-Xsy{oSfNr}>557Xi@BPz z=rZb_ZodoUxFRgo7P^gzajIp7QoAYA$0Uwn!r}Oc3FFUjq07=|NwN^`TSGUXKAMq{ zfu`#&pd%wk$9E9+VIjlnB7U+_YLMnJ&U-SF=lAn2``HCuAUtP{-?yX%IkHi|ykdz^ z(ovR_lvJ)mi;LEB=#}k=6FMFNZh~I+aK1p~)2PB_<^O$}Fn(zXIB!RV@m;f$Kc$Qw z2P~}2T%bs;)`7St!KYg>+Bv{%EwKj%lWK;_m3m_!9~*dicXzi5QC4y!1vX#~3r)kJ ziCJg#t?ZPUa)B_MA3PR^*R&lj+ued0y_kWu?ncK&Js>ItxKWY@;pL&)qp?@vPL4oq zn0eR)SmUh^LoXj0R%24lKOUU8iG&IoTaP(xw1s5O7cWS*ebYAqB&GIJdW>r|e zrj~}~Y2d}w17Be3(!xd2Y|X*>AAKz>C%jPFF&c|)M`nqLgv=^0 zT`Z;elnu=28ZD-{YEQqmBBr)pzV_l(2R@xlp$dG_8d{MrTbEWx1Z$;#=+vm8h~2O% zBqb$nk2%1wpz;@j6sU)T*0TlEs`TS%1KnF`pJi;q%`O*^53>Y)3ouL+wLq7w6r6P8 z5w8Kj9)+zTLKG+8#S2M;*JWF@uSXQL+K~J>1>L|Zk>%mzqjOH8yY1T)N=7i^p)P@h zWvntTm7iydj7n%9s?it}OdgUS{u4F?voEw0&w%Gsllm7^e2SsO|=MvX8ee#6og zO73DaVWPMbp!$y}vl0_9!HH|ma=mC(z0!Y5q|)4>h|Jb;?_jWeF|~@DYQoiMB`sDW z>?>8ooKj6`mF#9HVP^d=xqlm{M#shs?(dX@kPKWUkwQx^p7hp*tf2fa)Su;cD;eK=ge1=;(o=)q@6T47UDJT{+Y-ZNZUCHgawv76 zhY^2E2oo#FK>1@N}idM&Xoi?=}?xUVYvGCMU1TvW=Ry%S`m!DEg zTqqvo_+b35@2AaqWEsCW!`RdziyYR2#R%CCVfkR|Y%UMgkc_g$4F3T&NinmLE3v)V z_uuj8(kk=~!xDfS+<$k@ta-NV+rDWU?j`eD_yy>1;iO=m_enhUi|!U;$$J;t4aW9m z;k=oemz9x}4m3g335uUCs!9@v89P<60wj>(&>b3M$jyHVn~UJVSFqY_D56S;Zj|Hv za-7KLe23A7A@5`*>GhC;2{aiSQ53?;xT1%IpbH zJ6-QEWGcCL9i$hOg_z1^WJ$b4g_HSQ4Rq%-N%^DSF(hoo#3PK+^OR|e6MK!=t}8Yb zEv67yR(2KT!{uNb!BN)StOw)c;}c0_A=*Y91@u>F9e%BLd$Vc^oFHAoEzBR0@S3;6 z@PXbn!M5#Q%ww>AxUT9IKV>`XsG)KYoM`+Qr1R6tMxo6*P=ISKC}Mf)HnttP;3<$fm|dh z9|bY7{Cn5y@6A>le6K}f)Tqg$L(I-<>ZCFeGd>MrzP-X6dXt#lcoQe;8}$j+=v3C~ zr3M4dWKBf&gnvg~Lj46hFt>H}n~a)iYFM78jBCu*gRKIEi=!h_B%+KM@=6dmygE#? zT2VDI5K$pM4uQS%!$ZbaJMAvi&GVX`S%BScHzZ;o;#7CbMXUN=<)b1!6N( z(@NECUT&b%!EqWrQ^llAQgEBZE<+T!Q`O;>5D{X%zN;z>bp0>ymDoN~3BjV7^fpjy3ld%zF2SD4ic3?kqa0WmUc z)?5bC4$CopvGzZ-%?^ww;?DP7mfdjFU8YJ<)X>2Dh@?D3(~Y_uid^e%erii znrLbJq{}UezI1Q(9v_ULoEzlZyc|_gv6ak-Dphy#=}~eNa!`rWM6xQJ#lrl*;t3tm z`~9*NU4yo*OlOx4J+{O|A4}-23MW|WRH)tOn*;eZ%GPU86Fkj_Y-I|btxQJGX3iu* zYxrmLoo!wVXHT&X5FtG`_m+9-v9Y`KgvE#Od(Cy}G-zIXO|h~|^B9y3COYvb4F~jj zyU$OLo2|W=Aei%fMoM7Ef6^e+-T-1vyMNi2eo6TONe#U>8mYLTpYWgNTpuR1TzNrM z+gTiK1Yyb5^5LYjs+ewusfhz^>K=~`p=re02($Hi@2Xc+q54`PIgMO*V2G0>hO+un z!GhJzdl~EK;F!93qL3F0Jnd-FZghNMexl;=q!(L&Q+hegkIz4i8WiLEr!rTGI*DoP z8W=6~@me!U>To*vX{L+cji8H{(-R#FEWV5M|+MnC?s?O(ON4^*q6LHkIk*3_NBkC?bPEa{|UgrKjZ z+hboY1D%4+g9C&1Gq>JHoYcG}EQVDbsEvYBj20zBGi%1>WnD1U3|Uv4rK-oVOCkn4 z4y=cEl8)GhXy2FBrPg7S8Iy*>xdTZ*o?nA<~di0(53Y1wgyzE8%XhVSQlPw`($NT3-f3bDSN(Ah*CTL#ycr%AhEo8v#K3y39|;W&&2(JKvAN;? z2`6B)+cmy-E)b*=#TQKOa=i)kXV-a8>miOMjTNv zcVtB&uv$r}YHiE0pwQWZa+WW;7P^qDwE=)UPWE+PVnR=cQxa3tK8Q9u4uiwlY_r4D zd(J2pJ3G|XBuiOnB!|oYLwt9}_p8BgK)y$~RoC;9$-;mE!zJhxNgDXY>ar#iW1?T% zTLLbuDWJ3eMXuD{9>Xxtgq74`n1PPEXQ?QK6HvOM=0qd(O{dd2?5bFx^eIJylb%mL zLU98yQvV2A%B&#CFDe4*JMQxPeE{!A9+9RTADJS`G> zD-D>vdjtT0AphSiz?;YGj+@78?fZ38QxoPP8Vj29ra_SD_uYX|S?um|e#T-FncOPO zj8APEm|;hyKd=g-Q562y=iYB_U#g_Zy4h^@e`EMsyQ%wtDJjCj=64L(?bLZgsS&|c z%`1Zx1h7~-dbbo7Xtr-}_eNJL#tG}FcQSu=2jdmaI%>raR~fH3zLN9AXTaL+eKGGV z^Og;>9h{!?qT4N^0Y0>pHEW{IWRHa8Bdg%-J4$ZzkW}mzO2@X#<&Kh;2Q(-^MRo89 zCDd+?#r>`d%x{nrvV!Orqx>A=6wS`mOdQI1_jF|T&MdcLh{ye7R2Dy!Mc?YYad?0{ zoDnp+oQ5W&wHi>?88JP+v58f!;5rMYLjpjxkWCDj%!lOGaHLN5SNiDQYSB4xr>N^Y zRJQkTkr4Q?eTR@bVy-Ur1eKb;+u@`{%og~x{>UiO$RsohAKS1@@^{c$RdAwmo})qJ zT~60)y3f6CoFmjyQl;fJ8e5{4G-#!8oCR19n)?~ycE#3!QCcF-^WF?b^>zj3t}DRs zgK$GA{wCfjvt<2MRGV~bV>VD}2lN1$!@}b*{%H-?UWtUL*aUrZ#`&*J`QYKm+xHD0LLw3) zJ|3Je6zP3ms+ECg*7w8mwG5%8mV{%VpgusG-aPzrRxagTrRfJd*u8Mh)|#AI zW7XeUF{1)aFQEgU$Vi9^Bm&Dof+1?rqHK%s#6&BFL-c1Npu0M?053I*1%_g0To`2@ zK_?9@w8OM%Sp(eQgjBY~CFR_44HVFGhBVkkjXmUVKbpScSCv6>U+B^hBlp*#M;${u zV^p{I4B%`oBq*<;qvJzc-*=5e%l`#R4Yl%+7Q-&bqofbpdhNB>R`Nt$|4N$Iu3by; z#>B+L=;$b-E2PeJSwb}^78Wjr=8-TAOzaes<@E_dh!h`Tyy2%ClBMSCby4^4-!E#J z(I-_am=uEG3d8|dwTy}mN>c+6VIXVMtGQXfiZ(UK5zc$*(j_+GQ%^m`&J4{VqH2LP z-K9@o#FJ{s=6m++8Hgp$2q$L79hIA=gR~35!F+%V_=9AZCNL^)l10KrjWiCd;1UIVU55ME& zS2y9DFUxRB_ZrU3=J?%d%9nXHhJ`lb|GWftsSE5FBRc;?KR zb*1dohQTc9L0*a2C>(V>Cq{8Hjqq#Fp?H(mzOn6DIN1lE4Mq6(R?};r*dxZ=ixrj< zRU1>pZ)A-qGCS({PWt%SZ?m;!z-`6B83MbyJ?$NN@W{! z_8RO~`zqiOrJf?lqaIrgOk9Cbpf)}zSRFZX#EcHnd10x@5yABR+u#1CrZ1{_)e6;G zJ?r5Lc{G}%WiQ`+^G$ZnN-96|zL`U>7|&~mPc|&~!Gi~S5&0W*+|XEP~+gt9Q<>9d8ZErem1 zm)J4Jw;&=w$@&#^Kfvn`9XjOxg811EJQ2ZVNr(qc+mI~vKUxjOVOnIT4j#F~AQiqX zAvxdDvLUs0)Eb17H$>Y;PAUB_?G7@b=|Z($3G zXf(DYp|^4nXM~Z(}5?o-7r2zPgeiZ`%;FT}G{PMy&)a2yk^z=0R zms4HnDahJ0yj1*GXvC0$mrbK3fIQp;{t|$9PKAK352UGhu4l+KnAFV7jCEfW*5DM? z3Sf*=)F9vnz24ozh$W5Nrq+8VcQPvoip+HMQqQ?;Y?Ij9XX?WZ2Y(9n>Er|sLfS8ZH7 zdss=1P}xw=Vcd7`-tFIsRLh``3&kpj@>A>0$pQvG=bd890!Q5%H`WxD7RglP)1dd) zCar%I0_QXQv{r{Iy}^JuY2Zh)C(g)T(y_HkL}OC*sjX%y;c_DCV_e2mh$S>XKM#$W zwtMQ-DahWCm2h^gwCoNDJNP)v9zt%zPq&KV-#_co>Mxb(o92p@cKQ)pASsin^cu&G zCb-`~2Dx|dUWgt=6)n}ydDc>`pQ#kB+f7tMPm#guXc7Gu%^!P$yOBp9IB?*jk3JHn zB)LE}MY~o9rB}TyuRrt5GXj8iH7r`A)RU%^Ogm#X|N7UzN{xw=r^5vz&sH*x{8lU* z{))ZeLnUdAwgEqcaxAB20*k3-4O82qihqr2P4ifuuGF<@RJwZgsz@BCPoEyF&YR>v z{j)w|Njg_FIM}hppQ~1-6FQ-*n(t*LgWo_#v>@GSrbw=OT#kXAu0EZ$ReQnoz_F)0 zG(irXmhO^w8-bu5|KY=jcA_Jwbfd-lw@PUv(d&)RFUQ;~Cbwzpq^))BsaUMaf8irS z{&3`47G>p+Fk{uS7CU$D3>f>M8l7v4xOVN@+}s?DTY)<8tTm>F9#^6|!SRvEbO)#Y zumesSz8Fz3+=~}4?%TI-*=2k-Q2ifymPs5+?oU7cv}u ziVXkOY8!548^wQ(M3N*&#xiBrN;QZIiwGufyzvIqnS|8ADyn`kPa$sQPWP@+qE$Pq z(g<8pph`ZXyI;gPcMDRs<@TypI?b{jYipcxI*?`^7j^UaWP~Tz`5se7FFJ|SX3-Z< z*r~{;$ui>SAAkI@$)=_?o6Y4~>o{FWjhRKMi-$s8M~)l`3U$M<8pXdS5nCQ7Y*cUu zr8!~MEr+{8#!82mFJGpgfjIiRHW3TYo;@qzz&r1}!{CK+soJ@f${kg^?zwd(&9AN6 zxWY;tzJ4lY<%~fnGnJM-ynp}xB}H*>TrY5`*sjp}(REr{OT>tBDEYZ$kQ@9@J;OisjKgQqsuC z2;wjd@vXPsnwXeC8Qi&Zr`xPVZ1IguZ}T~m6XYI)>M^)a_S1zq6I4<0;F;4)7U z$z@5~c;iOZ)RLnXPM2D$*e;p`3xU!QJE_OKS@S0BSw%)uEXsJ0XN@ITr6LhR`58Oa zw1%ZFS$BqCKtmsV@PU%rwr$&>?3IhCYcPIaf$@)xjm^x=z`S6cCVeS>>QD~rlvfJ8 zR<+^;_{_V*h53pdtEkKN)?Q)EwuPJ6{KLb;h)!(sC5ddCLt!l6ciC^0PysvHv113v zITNc=FSewn7?DXQCnxp1TlUJ;QMXvtw2{xXTBGSVEOkY^6=Fr7LJ`w+)tR8OZYhk7 zXRN-ZiTSKzm{3q9AgdE6tjFq=d`5YUzMlk5cQ}7F3m~Z8zI_`J_@j?L+G>F^16=pg zY9H-EAkhRx8M-8jsO6P7^KRa}$;$E}LTf-(R)aDdX1a0PlL_^&kv0FCH)-)G6H;qb zrKNRtcD58W)=5L6fqNc4e3+eS>7@TZvf4gbn5jyl>ZHDG_cQ;d;2OVWW9!(}P+&$p z&B6potcddi!9CiVEqTCE;aQhJ4p!~RP?MKTyW-442WOS7E+HDPKws;S8k@EirPQu? z>1tu{7sv*`E(9fRRewW=usoZtepH(f!u>Fu~1To_I(4GSQ^;q`|rOGSB3m6 zi6|O$nOdFVRL$ya<@RJe{xu#~%j$`CR))x|^GKi%Km73W<;zto%O&9oDM=y{3ZL1Q z!G%ffHg+$6wRrGzSSnvBx*`@a36>J6+TJY$-qj0W*gCBQR24%uz&#RMlBQxDkYk2i zwc!MWFq9k}Vm*VMH*M3h)aujmF`bg;dbXmL=qKzze0N%-8SZxflE5ZG1T37O`ihQI_b5m-LCcZ8FG)SycLW-Rvb9DWCS(~;Q6#rUV z*qOTyfucK`+|b>nW9s3Jr;kbNDqM6sgP%TGBMw-pJN=Byu2rTvdE+Kwm4M=azdNopN)0W@#N134*aVA*q{bQhT=Vw0i zna_Xz^Dxsmy9S6R)3mO0UbVB-`kdz!nl67^_SURd!%~~J{08r{FuGpg6`IfBr8DShKHW_C>a{RX`01lZk81GJEFWuETW+glm#bfc zb8T9~Qk%B4)`{iJrV=IIP)9!SuZ6aZ{ExYfzlK|NZI4=e<7p$P4t$a=l{&zQwKoHbO&880_EQG2 zX-#Vs|5a=Q$DeQ7+6ce38h=&GZPmb_G_7H&O>0`yn)YN`z>%07*qoM6N<$g55Eh AssI20 literal 0 Hc$@X0Bk~MA&$X#IXal5PQJzGFU2 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 literal 0 Hc$@hs1O-dju5C8yBYHMj2-ErT4AS1auv1S!H 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 OJ(6N_qSeCsf&T;QRD#6- literal 0 Hc$@z10XsV9 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 data( - new FreehandStrokeStrategy::Data(resources->currentNode(), - 0, pi1, pi2)); + new FreehandStrokeStrategy::Data(0, pi1, pi2)); image->addJob(strokeId(), data.take()); + image->addJob(strokeId(), new FreehandStrokeStrategy::UpdateData(true)); } private: diff --git a/libs/ui/tool/KisStrokeSpeedMonitor.h b/libs/ui/tool/KisStrokeSpeedMonitor.h new file mode 100644 --- /dev/null +++ b/libs/ui/tool/KisStrokeSpeedMonitor.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017 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 KISSTROKESPEEDMONITOR_H +#define KISSTROKESPEEDMONITOR_H + +#include + +#include "kis_types.h" +#include "kritaui_export.h" + +class KRITAUI_EXPORT KisStrokeSpeedMonitor : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString lastPresetName READ lastPresetName NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal lastPresetSize READ lastPresetSize NOTIFY sigStatsUpdated) + + Q_PROPERTY(qreal lastCursorSpeed READ lastCursorSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal lastRenderingSpeed READ lastRenderingSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal lastFps READ lastFps NOTIFY sigStatsUpdated) + + Q_PROPERTY(bool lastStrokeSaturated READ lastCursorSpeed NOTIFY sigStatsUpdated) + + Q_PROPERTY(qreal avgCursorSpeed READ avgCursorSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal avgRenderingSpeed READ avgRenderingSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal avgFps READ avgFps NOTIFY sigStatsUpdated) + +public: + KisStrokeSpeedMonitor(); + ~KisStrokeSpeedMonitor(); + + static KisStrokeSpeedMonitor* instance(); + + bool haveStrokeSpeedMeasurement() const; + + void notifyStrokeFinished(qreal cursorSpeed, qreal renderingSpeed, qreal fps, KisPaintOpPresetSP preset); + + + QString lastPresetName() const; + qreal lastPresetSize() const; + + qreal lastCursorSpeed() const; + qreal lastRenderingSpeed() const; + qreal lastFps() const; + bool lastStrokeSaturated() const; + + qreal avgCursorSpeed() const; + qreal avgRenderingSpeed() const; + qreal avgFps() const; + + +Q_SIGNALS: + void sigStatsUpdated(); + +public Q_SLOTS: + void setHaveStrokeSpeedMeasurement(bool value); + +private Q_SLOTS: + void resetAccumulatedValues(); + void slotConfigChanged(); + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISSTROKESPEEDMONITOR_H diff --git a/libs/ui/tool/KisStrokeSpeedMonitor.cpp b/libs/ui/tool/KisStrokeSpeedMonitor.cpp new file mode 100644 --- /dev/null +++ b/libs/ui/tool/KisStrokeSpeedMonitor.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2017 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 "KisStrokeSpeedMonitor.h" + +#include +#include +#include + +#include +#include "kis_paintop_preset.h" +#include "kis_paintop_settings.h" + +#include "kis_config.h" +#include "kis_config_notifier.h" +#include "KisUpdateSchedulerConfigNotifier.h" + + +Q_GLOBAL_STATIC(KisStrokeSpeedMonitor, s_instance) + + +struct KisStrokeSpeedMonitor::Private +{ + static const int averageWindow = 10; + + Private() + : avgCursorSpeed(averageWindow), + avgRenderingSpeed(averageWindow), + avgFps(averageWindow) + { + } + + KisRollingMeanAccumulatorWrapper avgCursorSpeed; + KisRollingMeanAccumulatorWrapper avgRenderingSpeed; + KisRollingMeanAccumulatorWrapper avgFps; + + qreal cachedAvgCursorSpeed = 0; + qreal cachedAvgRenderingSpeed = 0; + qreal cachedAvgFps = 0; + + qreal lastCursorSpeed = 0; + qreal lastRenderingSpeed = 0; + qreal lastFps = 0; + bool lastStrokeSaturated = false; + + QByteArray lastPresetMd5; + QString lastPresetName; + qreal lastPresetSize = 0; + + bool haveStrokeSpeedMeasurement = true; + + QMutex mutex; +}; + +KisStrokeSpeedMonitor::KisStrokeSpeedMonitor() + : m_d(new Private()) +{ + connect(KisUpdateSchedulerConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(resetAccumulatedValues())); + connect(KisUpdateSchedulerConfigNotifier::instance(), SIGNAL(configChanged()), SIGNAL(sigStatsUpdated())); + connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); + + slotConfigChanged(); +} + +KisStrokeSpeedMonitor::~KisStrokeSpeedMonitor() +{ +} + +KisStrokeSpeedMonitor *KisStrokeSpeedMonitor::instance() +{ + return s_instance; +} + +bool KisStrokeSpeedMonitor::haveStrokeSpeedMeasurement() const +{ + return m_d->haveStrokeSpeedMeasurement; +} + +void KisStrokeSpeedMonitor::setHaveStrokeSpeedMeasurement(bool value) +{ + m_d->haveStrokeSpeedMeasurement = value; +} + +void KisStrokeSpeedMonitor::resetAccumulatedValues() +{ + m_d->avgCursorSpeed.reset(m_d->averageWindow); + m_d->avgRenderingSpeed.reset(m_d->averageWindow); + m_d->avgFps.reset(m_d->averageWindow); +} + +void KisStrokeSpeedMonitor::slotConfigChanged() +{ + KisConfig cfg; + m_d->haveStrokeSpeedMeasurement = cfg.enableBrushSpeedLogging(); + resetAccumulatedValues(); + emit sigStatsUpdated(); +} + +void KisStrokeSpeedMonitor::notifyStrokeFinished(qreal cursorSpeed, qreal renderingSpeed, qreal fps, KisPaintOpPresetSP preset) +{ + if (qFuzzyCompare(cursorSpeed, 0.0) || qFuzzyCompare(renderingSpeed, 0.0)) return; + + QMutexLocker locker(&m_d->mutex); + + const bool isSamePreset = + m_d->lastPresetName == preset->name() && + qFuzzyCompare(m_d->lastPresetSize, preset->settings()->paintOpSize()); + + ENTER_FUNCTION() << ppVar(isSamePreset); + + if (!isSamePreset) { + resetAccumulatedValues(); + m_d->lastPresetName = preset->name(); + m_d->lastPresetSize = preset->settings()->paintOpSize(); + } + + m_d->lastCursorSpeed = cursorSpeed; + m_d->lastRenderingSpeed = renderingSpeed; + m_d->lastFps = fps; + + + static const qreal saturationSpeedThreshold = 0.30; // cursor speed should be at least 30% higher + m_d->lastStrokeSaturated = cursorSpeed / renderingSpeed > (1.0 + saturationSpeedThreshold); + + + if (m_d->lastStrokeSaturated) { + m_d->avgCursorSpeed(cursorSpeed); + m_d->avgRenderingSpeed(renderingSpeed); + m_d->avgFps(fps); + + m_d->cachedAvgCursorSpeed = m_d->avgCursorSpeed.rollingMean(); + m_d->cachedAvgRenderingSpeed = m_d->avgRenderingSpeed.rollingMean(); + m_d->cachedAvgFps = m_d->avgFps.rollingMean(); + } + + emit sigStatsUpdated(); + + + ENTER_FUNCTION() << + QString(" CS: %1 RS: %2 FPS: %3 %4") + .arg(m_d->lastCursorSpeed, 5) + .arg(m_d->lastRenderingSpeed, 5) + .arg(m_d->lastFps, 5) + .arg(m_d->lastStrokeSaturated ? "(saturated)" : ""); + ENTER_FUNCTION() << + QString("ACS: %1 ARS: %2 AFPS: %3") + .arg(m_d->cachedAvgCursorSpeed, 5) + .arg(m_d->cachedAvgRenderingSpeed, 5) + .arg(m_d->cachedAvgFps, 5); +} + +QString KisStrokeSpeedMonitor::lastPresetName() const +{ + return m_d->lastPresetName; +} + +qreal KisStrokeSpeedMonitor::lastPresetSize() const +{ + return m_d->lastPresetSize; +} + +qreal KisStrokeSpeedMonitor::lastCursorSpeed() const +{ + return m_d->lastCursorSpeed; +} + +qreal KisStrokeSpeedMonitor::lastRenderingSpeed() const +{ + return m_d->lastRenderingSpeed; +} + +qreal KisStrokeSpeedMonitor::lastFps() const +{ + return m_d->lastFps; +} + +bool KisStrokeSpeedMonitor::lastStrokeSaturated() const +{ + return m_d->lastStrokeSaturated; +} + +qreal KisStrokeSpeedMonitor::avgCursorSpeed() const +{ + return m_d->cachedAvgCursorSpeed; +} + +qreal KisStrokeSpeedMonitor::avgRenderingSpeed() const +{ + return m_d->cachedAvgRenderingSpeed; +} + +qreal KisStrokeSpeedMonitor::avgFps() const +{ + return m_d->cachedAvgFps; +} diff --git a/libs/ui/tool/kis_figure_painting_tool_helper.cpp b/libs/ui/tool/kis_figure_painting_tool_helper.cpp --- a/libs/ui/tool/kis_figure_painting_tool_helper.cpp +++ b/libs/ui/tool/kis_figure_painting_tool_helper.cpp @@ -55,59 +55,55 @@ KisFigurePaintingToolHelper::~KisFigurePaintingToolHelper() { + m_strokesFacade->addJob(m_strokeId, + new FreehandStrokeStrategy::UpdateData(true)); m_strokesFacade->endStroke(m_strokeId); } void KisFigurePaintingToolHelper::paintLine(const KisPaintInformation &pi0, const KisPaintInformation &pi1) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, pi0, pi1)); } void KisFigurePaintingToolHelper::paintPolyline(const vQPointF &points) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::POLYLINE, points)); } void KisFigurePaintingToolHelper::paintPolygon(const vQPointF &points) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::POLYGON, points)); } void KisFigurePaintingToolHelper::paintRect(const QRectF &rect) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::RECT, rect)); } void KisFigurePaintingToolHelper::paintEllipse(const QRectF &rect) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::ELLIPSE, rect)); } void KisFigurePaintingToolHelper::paintPainterPath(const QPainterPath &path) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::PAINTER_PATH, path)); } @@ -135,17 +131,15 @@ void KisFigurePaintingToolHelper::paintPainterPathQPen(const QPainterPath path, const QPen &pen, const KoColor &color) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::QPAINTER_PATH, path, pen, color)); } void KisFigurePaintingToolHelper::paintPainterPathQPenFill(const QPainterPath path, const QPen &pen, const KoColor &color) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::QPAINTER_PATH_FILL, path, pen, color)); } diff --git a/libs/ui/tool/kis_resources_snapshot.h b/libs/ui/tool/kis_resources_snapshot.h --- a/libs/ui/tool/kis_resources_snapshot.h +++ b/libs/ui/tool/kis_resources_snapshot.h @@ -89,6 +89,7 @@ qreal effectiveZoom() const; bool presetAllowsLod() const; + bool presetNeedsAsynchronousUpdates() const; void setFGColorOverride(const KoColor &color); void setBGColorOverride(const KoColor &color); diff --git a/libs/ui/tool/kis_resources_snapshot.cpp b/libs/ui/tool/kis_resources_snapshot.cpp --- a/libs/ui/tool/kis_resources_snapshot.cpp +++ b/libs/ui/tool/kis_resources_snapshot.cpp @@ -50,27 +50,27 @@ KisDefaultBoundsBaseSP bounds; KoColor currentFgColor; KoColor currentBgColor; - KoPattern *currentPattern; + KoPattern *currentPattern = 0; KoAbstractGradient *currentGradient; KisPaintOpPresetSP currentPaintOpPreset; KisNodeSP currentNode; qreal currentExposure; KisFilterConfigurationSP currentGenerator; QPointF axesCenter; - bool mirrorMaskHorizontal; - bool mirrorMaskVertical; + bool mirrorMaskHorizontal = false; + bool mirrorMaskVertical = false; - quint8 opacity; - QString compositeOpId; + quint8 opacity = OPACITY_OPAQUE_U8; + QString compositeOpId = COMPOSITE_OVER; const KoCompositeOp *compositeOp; - KisPainter::StrokeStyle strokeStyle; - KisPainter::FillStyle fillStyle; + KisPainter::StrokeStyle strokeStyle = KisPainter::StrokeStyleBrush; + KisPainter::FillStyle fillStyle = KisPainter::FillStyleForegroundColor; - bool globalAlphaLock; - qreal effectiveZoom; - bool presetAllowsLod; + bool globalAlphaLock = false; + qreal effectiveZoom = 1.0; + bool presetAllowsLod = false; KisSelectionSP selectionOverride; }; @@ -373,6 +373,11 @@ return m_d->presetAllowsLod; } +bool KisResourcesSnapshot::presetNeedsAsynchronousUpdates() const +{ + return m_d->currentPaintOpPreset && m_d->currentPaintOpPreset->settings()->needsAsynchronousUpdates(); +} + void KisResourcesSnapshot::setFGColorOverride(const KoColor &color) { m_d->currentFgColor = color; diff --git a/libs/ui/tool/kis_tool_freehand_helper.h b/libs/ui/tool/kis_tool_freehand_helper.h --- a/libs/ui/tool/kis_tool_freehand_helper.h +++ b/libs/ui/tool/kis_tool_freehand_helper.h @@ -75,7 +75,6 @@ void paintEvent(KoPointerEvent *event); void endPaint(); - const KisPaintOp* currentPaintOp() const; QPainterPath paintOpOutline(const QPointF &savedCursorPos, const KoPointerEvent *event, const KisPaintOpSettingsSP globalSettings, @@ -152,6 +151,7 @@ void finishStroke(); void doAirbrushing(); + void doAsynchronousUpdate(bool forceUpdate = false); void stabilizerPollAndPaint(); private: diff --git a/libs/ui/tool/kis_tool_freehand_helper.cpp b/libs/ui/tool/kis_tool_freehand_helper.cpp --- a/libs/ui/tool/kis_tool_freehand_helper.cpp +++ b/libs/ui/tool/kis_tool_freehand_helper.cpp @@ -101,6 +101,8 @@ KisStabilizedEventsSampler stabilizedSampler; KisStabilizerDelayedPaintHelper stabilizerDelayedPaintHelper; + QTimer asynchronousUpdatesThresholdTimer; + int canvasRotation; bool canvasMirroredH; @@ -124,6 +126,7 @@ m_d->strokeTimeoutTimer.setSingleShot(true); connect(&m_d->strokeTimeoutTimer, SIGNAL(timeout()), SLOT(finishStroke())); connect(&m_d->airbrushingTimer, SIGNAL(timeout()), SLOT(doAirbrushing())); + connect(&m_d->asynchronousUpdatesThresholdTimer, SIGNAL(timeout()), SLOT(doAsynchronousUpdate())); connect(&m_d->stabilizerPollTimer, SIGNAL(timeout()), SLOT(stabilizerPollAndPaint())); m_d->stabilizerDelayedPaintHelper.setPaintLineCallback( @@ -162,7 +165,7 @@ qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, savedCursorPos, 0); info.setCanvasRotation(m_d->canvasRotation); info.setCanvasHorizontalMirrorState( m_d->canvasMirroredH ); - KisDistanceInformation distanceInfo(prevPoint, 0, startAngle); + KisDistanceInformation distanceInfo(prevPoint, startAngle); if (!m_d->painterInfos.isEmpty()) { settings = m_d->resources->currentPaintOpPreset()->settings(); @@ -193,7 +196,7 @@ * local copy of the coordinates. */ distanceInfo = *buddyDistance; - distanceInfo.overrideLastValues(prevPoint, 0, startAngle); + distanceInfo.overrideLastValues(prevPoint, startAngle); } else if (m_d->painterInfos.first()->dragDistance->isStarted()) { distanceInfo = *m_d->painterInfos.first()->dragDistance; @@ -284,7 +287,6 @@ const bool useSpacingUpdates = m_d->resources->needsSpacingUpdates(); KisDistanceInitInfo startDistInfo(m_d->previousPaintInformation.pos(), - m_d->previousPaintInformation.currentTime(), startAngle, useSpacingUpdates ? SPACING_UPDATE_INTERVAL : LONG_TIME, airbrushing ? TIMING_UPDATE_INTERVAL : LONG_TIME); @@ -307,9 +309,12 @@ m_d->history.clear(); m_d->distanceHistory.clear(); - if(airbrushing) { + if (airbrushing) { m_d->airbrushingTimer.setInterval(computeAirbrushTimerInterval()); m_d->airbrushingTimer.start(); + } else if (m_d->resources->presetNeedsAsynchronousUpdates()) { + m_d->asynchronousUpdatesThresholdTimer.setInterval(80 /* msec */); + m_d->asynchronousUpdatesThresholdTimer.start(); } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { @@ -613,6 +618,10 @@ m_d->airbrushingTimer.stop(); } + if (m_d->asynchronousUpdatesThresholdTimer.isActive()) { + m_d->asynchronousUpdatesThresholdTimer.stop(); + } + if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { stabilizerEnd(); } @@ -625,6 +634,9 @@ */ m_d->painterInfos.clear(); + // last update to complete rendering if there is still something pending + doAsynchronousUpdate(true); + m_d->strokesFacade->endStroke(m_d->strokeId); m_d->strokeId.clear(); @@ -643,6 +655,10 @@ m_d->airbrushingTimer.stop(); } + if (m_d->asynchronousUpdatesThresholdTimer.isActive()) { + m_d->asynchronousUpdatesThresholdTimer.stop(); + } + if (m_d->stabilizerPollTimer.isActive()) { m_d->stabilizerPollTimer.stop(); } @@ -822,11 +838,6 @@ } } -const KisPaintOp* KisToolFreehandHelper::currentPaintOp() const -{ - return !m_d->painterInfos.isEmpty() ? m_d->painterInfos.first()->painter->paintOp() : 0; -} - void KisToolFreehandHelper::finishStroke() { if (m_d->haveTangent) { @@ -863,6 +874,12 @@ } } +void KisToolFreehandHelper::doAsynchronousUpdate(bool forceUpdate) +{ + m_d->strokesFacade->addJob(m_d->strokeId, + new FreehandStrokeStrategy::UpdateData(forceUpdate)); +} + int KisToolFreehandHelper::computeAirbrushTimerInterval() const { qreal realInterval = m_d->resources->airbrushingInterval() * AIRBRUSH_INTERVAL_FACTOR; @@ -874,8 +891,7 @@ { m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, - new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), - painterInfoId, pi)); + new FreehandStrokeStrategy::Data(painterInfoId, pi)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addPoint(pi); @@ -888,8 +904,7 @@ { m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, - new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), - painterInfoId, pi1, pi2)); + new FreehandStrokeStrategy::Data(painterInfoId, pi1, pi2)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addLine(pi1, pi2); @@ -928,8 +943,7 @@ m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, - new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), - painterInfoId, + new FreehandStrokeStrategy::Data(painterInfoId, pi1, control1, control2, pi2)); if(m_d->recordingAdapter) { diff --git a/libs/ui/tool/strokes/FreehandStrokeRunnableJobDataWithUpdate.h b/libs/ui/tool/strokes/FreehandStrokeRunnableJobDataWithUpdate.h new file mode 100644 --- /dev/null +++ b/libs/ui/tool/strokes/FreehandStrokeRunnableJobDataWithUpdate.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 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 FREEHANDSTROKERUNNABLEJOBDATAWITHUPDATE_H +#define FREEHANDSTROKERUNNABLEJOBDATAWITHUPDATE_H + +#include "KisRunnableStrokeJobData.h" + + +class FreehandStrokeRunnableJobDataWithUpdate : public KisRunnableStrokeJobData +{ +public: + FreehandStrokeRunnableJobDataWithUpdate(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL) + : KisRunnableStrokeJobData(runnable, sequentiality, exclusivity) + { + } + + FreehandStrokeRunnableJobDataWithUpdate(std::function func, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL) + : KisRunnableStrokeJobData(func, sequentiality, exclusivity) + { + } +}; + +#endif // FREEHANDSTROKERUNNABLEJOBDATAWITHUPDATE_H diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.h copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.h --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,30 +16,45 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISSTROKEEFFICIENCYMEASURER_H +#define KISSTROKEEFFICIENCYMEASURER_H +#include "kritaui_export.h" #include +#include +class QPointF; -class KisOpenglCanvasDebugger +class KRITAUI_EXPORT KisStrokeEfficiencyMeasurer { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); + KisStrokeEfficiencyMeasurer(); + ~KisStrokeEfficiencyMeasurer(); - static KisOpenglCanvasDebugger* instance(); + void setEnabled(bool value); + bool isEnabled() const; - bool showFpsOnCanvas() const; + void addSample(const QPointF &pt); + void addSamples(const QVector &points); - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); + qreal averageCursorSpeed() const; + qreal averageRenderingSpeed() const; + qreal averageFps() const; + + void notifyRenderingStarted(); + void notifyRenderingFinished(); + + void notifyCursorMoveStarted(); + void notifyCursorMoveFinished(); + + void notifyFrameRenderingStarted(); + + void reset(); private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISSTROKEEFFICIENCYMEASURER_H diff --git a/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.cpp b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.cpp new file mode 100644 --- /dev/null +++ b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2017 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 "KisStrokeEfficiencyMeasurer.h" + +#include +#include +#include + +#include + +#include "kis_global.h" + +struct KisStrokeEfficiencyMeasurer::Private +{ + boost::optional lastSamplePos; + qreal distance = 0; + + QElapsedTimer strokeTimeSource; + bool isEnabled = true; + + int renderingStartTime = 0; + int renderingTime = 0; + + int cursorMoveStartTime = 0; + int cursorMoveTime = 0; + + int framesCount = 0; + +}; + +KisStrokeEfficiencyMeasurer::KisStrokeEfficiencyMeasurer() + : m_d(new Private()) +{ + m_d->strokeTimeSource.start(); +} + +KisStrokeEfficiencyMeasurer::~KisStrokeEfficiencyMeasurer() +{ +} + +void KisStrokeEfficiencyMeasurer::setEnabled(bool value) +{ + m_d->isEnabled = value; +} + +bool KisStrokeEfficiencyMeasurer::isEnabled() const +{ + return m_d->isEnabled; +} + +void KisStrokeEfficiencyMeasurer::addSample(const QPointF &pt) +{ + if (!m_d->isEnabled) return; + + if (!m_d->lastSamplePos) { + m_d->lastSamplePos = pt; + } else { + m_d->distance += kisDistance(pt, *m_d->lastSamplePos); + *m_d->lastSamplePos = pt; + } +} + +void KisStrokeEfficiencyMeasurer::addSamples(const QVector &points) +{ + if (!m_d->isEnabled) return; + + Q_FOREACH (const QPointF &pt, points) { + addSample(pt); + } +} + +void KisStrokeEfficiencyMeasurer::notifyRenderingStarted() +{ + m_d->renderingStartTime = m_d->strokeTimeSource.elapsed(); +} + +void KisStrokeEfficiencyMeasurer::notifyRenderingFinished() +{ + m_d->renderingTime = m_d->strokeTimeSource.elapsed() - m_d->renderingStartTime; +} + +void KisStrokeEfficiencyMeasurer::notifyCursorMoveStarted() +{ + m_d->cursorMoveStartTime = m_d->strokeTimeSource.elapsed(); +} + +void KisStrokeEfficiencyMeasurer::notifyCursorMoveFinished() +{ + m_d->cursorMoveTime = m_d->strokeTimeSource.elapsed() - m_d->cursorMoveStartTime; +} + +void KisStrokeEfficiencyMeasurer::notifyFrameRenderingStarted() +{ + m_d->framesCount++; +} + +qreal KisStrokeEfficiencyMeasurer::averageCursorSpeed() const +{ + return m_d->cursorMoveTime ? m_d->distance / m_d->cursorMoveTime : 0.0; +} + +qreal KisStrokeEfficiencyMeasurer::averageRenderingSpeed() const +{ + return m_d->renderingTime ? m_d->distance / m_d->renderingTime : 0.0; +} + +qreal KisStrokeEfficiencyMeasurer::averageFps() const +{ + return m_d->renderingTime ? m_d->framesCount * 1000.0 / m_d->renderingTime : 0.0; +} + + diff --git a/libs/ui/tool/strokes/freehand_stroke.h b/libs/ui/tool/strokes/freehand_stroke.h --- a/libs/ui/tool/strokes/freehand_stroke.h +++ b/libs/ui/tool/strokes/freehand_stroke.h @@ -48,55 +48,62 @@ QPAINTER_PATH_FILL }; - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, const KisPaintInformation &_pi) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(POINT), pi1(_pi) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, const KisPaintInformation &_pi1, const KisPaintInformation &_pi2) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(LINE), pi1(_pi1), pi2(_pi2) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, const KisPaintInformation &_pi1, const QPointF &_control1, const QPointF &_control2, const KisPaintInformation &_pi2) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(CURVE), pi1(_pi1), pi2(_pi2), control1(_control1), control2(_control2) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const vQPointF &_points) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), points(_points) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const QRectF &_rect) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), rect(_rect) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const QPainterPath &_path) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), path(_path) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const QPainterPath &_path, const QPen &_pen, const KoColor &_customColor) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), path(_path), pen(_pen), customColor(_customColor) {} @@ -108,7 +115,6 @@ private: Data(const Data &rhs, int levelOfDetail) : KisStrokeJobData(rhs), - node(rhs.node), painterInfoId(rhs.painterInfoId), type(rhs.type) { @@ -155,7 +161,6 @@ }; } public: - KisNodeSP node; int painterInfoId; DabType type; @@ -171,6 +176,29 @@ KoColor customColor; }; + class UpdateData : public KisStrokeJobData { + public: + UpdateData(bool _forceUpdate) + : KisStrokeJobData(KisStrokeJobData::SEQUENTIAL), + forceUpdate(_forceUpdate) + {} + + + KisStrokeJobData* createLodClone(int levelOfDetail) override { + return new UpdateData(*this, levelOfDetail); + } + + private: + UpdateData(const UpdateData &rhs, int levelOfDetail) + : KisStrokeJobData(rhs), + forceUpdate(rhs.forceUpdate) + { + Q_UNUSED(levelOfDetail); + } + public: + bool forceUpdate = false; + }; + public: FreehandStrokeStrategy(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp, @@ -186,16 +214,25 @@ ~FreehandStrokeStrategy() override; + void initStrokeCallback() override; + void finishStrokeCallback() override; + void doStrokeCallback(KisStrokeJobData *data) override; KisStrokeStrategy* createLodClone(int levelOfDetail) override; + void notifyUserStartedStroke() override; + void notifyUserEndedStroke() override; + protected: FreehandStrokeStrategy(const FreehandStrokeStrategy &rhs, int levelOfDetail); private: void init(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp); + void tryDoUpdate(bool forceEnd = false); + void issueSetDirtySignals(); + private: struct Private; const QScopedPointer m_d; diff --git a/libs/ui/tool/strokes/freehand_stroke.cpp b/libs/ui/tool/strokes/freehand_stroke.cpp --- a/libs/ui/tool/strokes/freehand_stroke.cpp +++ b/libs/ui/tool/strokes/freehand_stroke.cpp @@ -18,22 +18,55 @@ #include "freehand_stroke.h" +#include + #include "kis_canvas_resource_provider.h" #include #include #include "kis_painter.h" +#include "kis_paintop.h" #include "kis_update_time_monitor.h" #include +#include +#include "FreehandStrokeRunnableJobDataWithUpdate.h" +#include +#include "KisStrokeEfficiencyMeasurer.h" +#include struct FreehandStrokeStrategy::Private { - Private(KisResourcesSnapshotSP _resources) : resources(_resources) {} + Private(KisResourcesSnapshotSP _resources) + : resources(_resources), + needsAsynchronousUpdates(_resources->presetNeedsAsynchronousUpdates()) + { + if (needsAsynchronousUpdates) { + timeSinceLastUpdate.start(); + } + } + + Private(const Private &rhs) + : randomSource(rhs.randomSource), + resources(rhs.resources), + needsAsynchronousUpdates(rhs.needsAsynchronousUpdates) + { + if (needsAsynchronousUpdates) { + timeSinceLastUpdate.start(); + } + } KisStrokeRandomSource randomSource; KisResourcesSnapshotSP resources; + + KisStrokeEfficiencyMeasurer efficiencyMeasurer; + + QElapsedTimer timeSinceLastUpdate; + int currentUpdatePeriod = 40; + + const bool needsAsynchronousUpdates = false; + std::mutex updateEntryMutex; }; FreehandStrokeStrategy::FreehandStrokeStrategy(bool needsIndirectPainting, @@ -69,6 +102,11 @@ FreehandStrokeStrategy::~FreehandStrokeStrategy() { + KisStrokeSpeedMonitor::instance()->notifyStrokeFinished(m_d->efficiencyMeasurer.averageCursorSpeed(), + m_d->efficiencyMeasurer.averageRenderingSpeed(), + m_d->efficiencyMeasurer.averageFps(), + m_d->resources->currentPaintOpPreset()); + KisUpdateTimeMonitor::instance()->endStrokeMeasure(); } @@ -80,64 +118,161 @@ setSupportsWrapAroundMode(true); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); + if (m_d->needsAsynchronousUpdates) { + /** + * In case the paintop uses asynchronous updates, we should set priority to it, + * because FPS is controlled separately, not by the queue's merging algorithm. + */ + setBalancingRatioOverride(0.01); // set priority to updates + } + KisUpdateTimeMonitor::instance()->startStrokeMeasure(); + m_d->efficiencyMeasurer.setEnabled(KisStrokeSpeedMonitor::instance()->haveStrokeSpeedMeasurement()); +} + +void FreehandStrokeStrategy::initStrokeCallback() +{ + KisPainterBasedStrokeStrategy::initStrokeCallback(); + m_d->efficiencyMeasurer.notifyRenderingStarted(); +} + +void FreehandStrokeStrategy::finishStrokeCallback() +{ + m_d->efficiencyMeasurer.notifyRenderingFinished(); + KisPainterBasedStrokeStrategy::finishStrokeCallback(); } + void FreehandStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { - Data *d = dynamic_cast(data); - PainterInfo *info = painterInfos()[d->painterInfoId]; - - KisUpdateTimeMonitor::instance()->reportPaintOpPreset(info->painter->preset()); - KisRandomSourceSP rnd = m_d->randomSource.source(); - - switch(d->type) { - case Data::POINT: - d->pi1.setRandomSource(rnd); - info->painter->paintAt(d->pi1, info->dragDistance); - break; - case Data::LINE: - d->pi1.setRandomSource(rnd); - d->pi2.setRandomSource(rnd); - info->painter->paintLine(d->pi1, d->pi2, info->dragDistance); - break; - case Data::CURVE: - d->pi1.setRandomSource(rnd); - d->pi2.setRandomSource(rnd); - info->painter->paintBezierCurve(d->pi1, - d->control1, - d->control2, - d->pi2, - info->dragDistance); - break; - case Data::POLYLINE: - info->painter->paintPolyline(d->points, 0, d->points.size()); - break; - case Data::POLYGON: - info->painter->paintPolygon(d->points); - break; - case Data::RECT: - info->painter->paintRect(d->rect); - break; - case Data::ELLIPSE: - info->painter->paintEllipse(d->rect); - break; - case Data::PAINTER_PATH: - info->painter->paintPainterPath(d->path); - break; - case Data::QPAINTER_PATH: - info->painter->drawPainterPath(d->path, d->pen); - break; - case Data::QPAINTER_PATH_FILL: { - info->painter->setBackgroundColor(d->customColor); - info->painter->fillPainterPath(d->path);} - info->painter->drawPainterPath(d->path, d->pen); - break; - }; - - QVector dirtyRects = info->painter->takeDirtyRegion(); - KisUpdateTimeMonitor::instance()->reportJobFinished(data, dirtyRects); - d->node->setDirty(dirtyRects); + PainterInfo *info = 0; + + if (UpdateData *d = dynamic_cast(data)) { + // this job is lod-clonable in contrast to FreehandStrokeRunnableJobDataWithUpdate! + tryDoUpdate(d->forceUpdate); + + } else if (Data *d = dynamic_cast(data)) { + info = painterInfos()[d->painterInfoId]; + + KisUpdateTimeMonitor::instance()->reportPaintOpPreset(info->painter->preset()); + KisRandomSourceSP rnd = m_d->randomSource.source(); + + switch(d->type) { + case Data::POINT: + d->pi1.setRandomSource(rnd); + info->painter->paintAt(d->pi1, info->dragDistance); + m_d->efficiencyMeasurer.addSample(d->pi1.pos()); + break; + case Data::LINE: + d->pi1.setRandomSource(rnd); + d->pi2.setRandomSource(rnd); + info->painter->paintLine(d->pi1, d->pi2, info->dragDistance); + m_d->efficiencyMeasurer.addSample(d->pi2.pos()); + break; + case Data::CURVE: + d->pi1.setRandomSource(rnd); + d->pi2.setRandomSource(rnd); + info->painter->paintBezierCurve(d->pi1, + d->control1, + d->control2, + d->pi2, + info->dragDistance); + m_d->efficiencyMeasurer.addSample(d->pi2.pos()); + break; + case Data::POLYLINE: + info->painter->paintPolyline(d->points, 0, d->points.size()); + m_d->efficiencyMeasurer.addSamples(d->points); + break; + case Data::POLYGON: + info->painter->paintPolygon(d->points); + m_d->efficiencyMeasurer.addSamples(d->points); + break; + case Data::RECT: + info->painter->paintRect(d->rect); + m_d->efficiencyMeasurer.addSample(d->rect.topLeft()); + m_d->efficiencyMeasurer.addSample(d->rect.topRight()); + m_d->efficiencyMeasurer.addSample(d->rect.bottomRight()); + m_d->efficiencyMeasurer.addSample(d->rect.bottomLeft()); + break; + case Data::ELLIPSE: + info->painter->paintEllipse(d->rect); + // TODO: add speed measures + break; + case Data::PAINTER_PATH: + info->painter->paintPainterPath(d->path); + // TODO: add speed measures + break; + case Data::QPAINTER_PATH: + info->painter->drawPainterPath(d->path, d->pen); + break; + case Data::QPAINTER_PATH_FILL: { + info->painter->setBackgroundColor(d->customColor); + info->painter->fillPainterPath(d->path);} + info->painter->drawPainterPath(d->path, d->pen); + break; + }; + + tryDoUpdate(); + } else { + KisPainterBasedStrokeStrategy::doStrokeCallback(data); + + FreehandStrokeRunnableJobDataWithUpdate *dataWithUpdate = + dynamic_cast(data); + + if (dataWithUpdate) { + tryDoUpdate(); + } + } +} + +void FreehandStrokeStrategy::tryDoUpdate(bool forceEnd) +{ + // we should enter this function only once! + std::unique_lock entryLock(m_d->updateEntryMutex, std::try_to_lock); + if (!entryLock.owns_lock()) return; + + if (m_d->needsAsynchronousUpdates) { + if (forceEnd || m_d->timeSinceLastUpdate.elapsed() > m_d->currentUpdatePeriod) { + m_d->timeSinceLastUpdate.restart(); + + Q_FOREACH (PainterInfo *info, painterInfos()) { + KisPaintOp *paintop = info->painter->paintOp(); + KIS_SAFE_ASSERT_RECOVER_RETURN(paintop); + + // TODO: well, we should count all N simultaneous painters for FPS rate! + QVector jobs; + m_d->currentUpdatePeriod = paintop->doAsyncronousUpdate(jobs); + + if (!jobs.isEmpty()) { + jobs.append(new KisRunnableStrokeJobData( + [this] () { + this->issueSetDirtySignals(); + }, + KisStrokeJobData::SEQUENTIAL)); + + runnableJobsInterface()->addRunnableJobs(jobs); + m_d->efficiencyMeasurer.notifyFrameRenderingStarted(); + } + + } + } + } else { + issueSetDirtySignals(); + } + + +} + +void FreehandStrokeStrategy::issueSetDirtySignals() +{ + QVector dirtyRects; + + Q_FOREACH (PainterInfo *info, painterInfos()) { + dirtyRects.append(info->painter->takeDirtyRegion()); + } + + //KisUpdateTimeMonitor::instance()->reportJobFinished(data, dirtyRects); + targetNode()->setDirty(dirtyRects); } KisStrokeStrategy* FreehandStrokeStrategy::createLodClone(int levelOfDetail) @@ -147,3 +282,13 @@ FreehandStrokeStrategy *clone = new FreehandStrokeStrategy(*this, levelOfDetail); return clone; } + +void FreehandStrokeStrategy::notifyUserStartedStroke() +{ + m_d->efficiencyMeasurer.notifyCursorMoveStarted(); +} + +void FreehandStrokeStrategy::notifyUserEndedStroke() +{ + m_d->efficiencyMeasurer.notifyCursorMoveFinished(); +} diff --git a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h --- a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h +++ b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h @@ -19,16 +19,16 @@ #ifndef __KIS_PAINTER_BASED_STROKE_STRATEGY_H #define __KIS_PAINTER_BASED_STROKE_STRATEGY_H -#include "kis_simple_stroke_strategy.h" +#include "KisRunnableBasedStrokeStrategy.h" #include "kis_resources_snapshot.h" #include "kis_selection.h" class KisPainter; class KisDistanceInformation; class KisTransaction; -class KRITAUI_EXPORT KisPainterBasedStrokeStrategy : public KisSimpleStrokeStrategy +class KRITAUI_EXPORT KisPainterBasedStrokeStrategy : public KisRunnableBasedStrokeStrategy { public: /** @@ -77,6 +77,7 @@ void resumeStrokeCallback() override; protected: + KisNodeSP targetNode() const; KisPaintDeviceSP targetDevice() const; KisSelectionSP activeSelection() const; const QVector painterInfos() const; diff --git a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp --- a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp +++ b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp @@ -73,7 +73,7 @@ const KUndo2MagicString &name, KisResourcesSnapshotSP resources, QVector painterInfos,bool useMergeID) - : KisSimpleStrokeStrategy(id, name), + : KisRunnableBasedStrokeStrategy(id, name), m_resources(resources), m_painterInfos(painterInfos), m_transaction(0), @@ -86,7 +86,7 @@ const KUndo2MagicString &name, KisResourcesSnapshotSP resources, PainterInfo *painterInfo,bool useMergeID) - : KisSimpleStrokeStrategy(id, name), + : KisRunnableBasedStrokeStrategy(id, name), m_resources(resources), m_painterInfos(QVector() << painterInfo), m_transaction(0), @@ -106,7 +106,7 @@ } KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const KisPainterBasedStrokeStrategy &rhs, int levelOfDetail) - : KisSimpleStrokeStrategy(rhs), + : KisRunnableBasedStrokeStrategy(rhs), m_resources(rhs.m_resources), m_transaction(rhs.m_transaction), m_useMergeID(rhs.m_useMergeID) @@ -147,6 +147,7 @@ KisPainter *painter = info->painter; painter->begin(targetDevice, !hasIndirectPainting ? selection : 0); + painter->setRunnableStrokeJobsInterface(runnableJobsInterface()); m_resources->setupPainter(painter); if(hasIndirectPainting) { @@ -314,3 +315,8 @@ } } } + +KisNodeSP KisPainterBasedStrokeStrategy::targetNode() const +{ + return m_resources->currentNode(); +} diff --git a/libs/ui/widgets/kis_preset_live_preview_view.h b/libs/ui/widgets/kis_preset_live_preview_view.h --- a/libs/ui/widgets/kis_preset_live_preview_view.h +++ b/libs/ui/widgets/kis_preset_live_preview_view.h @@ -33,6 +33,7 @@ #include "kis_painting_information_builder.h" #include #include +#include /** * Widget for displaying a live brush preview of your @@ -91,8 +92,8 @@ /// internally sets the color space for brush preview const KoColorSpace *m_colorSpace; - /// painter that actually paints the stroke - KisPainter *m_brushPreviewPainter; + /// the color which is used for rendering the stroke + KoColor m_paintColor; /// the scene that can add items like text and the brush stroke image QGraphicsScene *m_brushPreviewScene; diff --git a/libs/ui/widgets/kis_preset_live_preview_view.cpp b/libs/ui/widgets/kis_preset_live_preview_view.cpp --- a/libs/ui/widgets/kis_preset_live_preview_view.cpp +++ b/libs/ui/widgets/kis_preset_live_preview_view.cpp @@ -21,15 +21,15 @@ #include #include #include "kis_paintop_settings.h" +#include KisPresetLivePreviewView::KisPresetLivePreviewView(QWidget *parent): QGraphicsView(parent) { } KisPresetLivePreviewView::~KisPresetLivePreviewView() { - delete m_brushPreviewPainter; delete m_noPreviewText; delete m_brushPreviewScene; } @@ -58,7 +58,6 @@ m_layer = new KisPaintLayer(m_image, "livePreviewStrokeSample", OPACITY_OPAQUE_U8, m_colorSpace); - m_brushPreviewPainter = new KisPainter(m_layer->paintDevice()); // set scene for the view m_brushPreviewScene = new QGraphicsScene(); @@ -150,15 +149,14 @@ } - m_brushPreviewPainter->fill(m_layer->image()->width()*sectionPercent*i, - 0, - m_layer->image()->width()*(sectionPercent*i +sectionPercent), - m_layer->image()->height(), - fillColor); - + const QRect fillRect(m_layer->image()->width()*sectionPercent*i, + 0, + m_layer->image()->width()*(sectionPercent*i +sectionPercent), + m_layer->image()->height()); + m_layer->paintDevice()->fill(fillRect, fillColor); } - m_brushPreviewPainter->setPaintColor(KoColor(Qt::white, m_colorSpace)); + m_paintColor = KoColor(Qt::white, m_colorSpace); } else if (m_currentPreset->paintOp().id() == "roundmarker" || @@ -190,12 +188,8 @@ else { // fill with gray first to clear out what existed from previous preview - m_brushPreviewPainter->fill(0,0, - m_layer->image()->width(), - m_layer->image()->height(), - KoColor(palette().color(QPalette::Background) , m_colorSpace)); - - m_brushPreviewPainter->setPaintColor(KoColor(palette().color(QPalette::Text), m_colorSpace)); + m_layer->paintDevice()->fill(m_image->bounds(), KoColor(palette().color(QPalette::Background) , m_colorSpace)); + m_paintColor = KoColor(palette().color(QPalette::Text), m_colorSpace); } } @@ -208,7 +202,26 @@ qreal previewSize = qBound(1.0, m_currentPreset->settings()->paintOpSize(), 150.0 ); // constrain live preview brush size KisPaintOpPresetSP proxy_preset = m_currentPreset->clone(); proxy_preset->settings()->setPaintOpSize(previewSize); - m_brushPreviewPainter->setPaintOpPreset(proxy_preset, m_layer, m_image); + + + KisResourcesSnapshotSP resources = + new KisResourcesSnapshot(m_image, + m_layer); + + resources->setBrush(proxy_preset); + resources->setFGColorOverride(m_paintColor); + FreehandStrokeStrategy::PainterInfo *painterInfo = new FreehandStrokeStrategy::PainterInfo(); + + KisStrokeStrategy *stroke = + new FreehandStrokeStrategy(resources->needsIndirectPainting(), + resources->indirectPaintingCompositeOp(), + resources, painterInfo, kundo2_noi18n("temp_stroke")); + + KisStrokeId strokeId = m_image->startStroke(stroke); + + + + //m_brushPreviewPainter->setPaintOpPreset(proxy_preset, m_layer, m_image); // slope-intercept is good for mapping two values. @@ -251,13 +264,14 @@ m_canvasCenterPoint.y() + (this->height()*0.2) )); - m_brushPreviewPainter->paintBezierCurve(pointOne, - QPointF(m_canvasCenterPoint.x() + this->width(), - m_canvasCenterPoint.y() - (this->height()*0.2) ), - QPointF(m_canvasCenterPoint.x() - this->width(), - m_canvasCenterPoint.y() + (this->height()*0.2) ), - pointTwo, &m_currentDistance); - + m_image->addJob(strokeId, + new FreehandStrokeStrategy::Data(0, + pointOne, + QPointF(m_canvasCenterPoint.x() + this->width(), + m_canvasCenterPoint.y() - (this->height()*0.2) ), + QPointF(m_canvasCenterPoint.x() - this->width(), + m_canvasCenterPoint.y() + (this->height()*0.2) ), + pointTwo)); } else { @@ -271,14 +285,19 @@ m_curvePointPI2.setPressure(1.0); - m_brushPreviewPainter->paintBezierCurve(m_curvePointPI1, - QPointF(m_canvasCenterPoint.x(), - m_canvasCenterPoint.y()-this->height()), - QPointF(m_canvasCenterPoint.x(), - m_canvasCenterPoint.y()+this->height()), - m_curvePointPI2, &m_currentDistance); + m_image->addJob(strokeId, + new FreehandStrokeStrategy::Data(0, + m_curvePointPI1, + QPointF(m_canvasCenterPoint.x(), + m_canvasCenterPoint.y()-this->height()), + QPointF(m_canvasCenterPoint.x(), + m_canvasCenterPoint.y()+this->height()), + m_curvePointPI2)); } + m_image->addJob(strokeId, new FreehandStrokeStrategy::UpdateData(true)); + m_image->endStroke(strokeId); + m_image->waitForDone(); // even though the brush is cloned, the proxy_preset still has some connection to the original preset which will mess brush sizing diff --git a/libs/ui/widgets/kis_scratch_pad.cpp b/libs/ui/widgets/kis_scratch_pad.cpp --- a/libs/ui/widgets/kis_scratch_pad.cpp +++ b/libs/ui/widgets/kis_scratch_pad.cpp @@ -59,11 +59,14 @@ { } - void requestProjectionUpdate(KisNode *node, const QRect& rect, bool resetAnimationCache) override { - KisNodeGraphListener::requestProjectionUpdate(node, rect, resetAnimationCache); + void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override { + KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); QMutexLocker locker(&m_lock); - m_scratchPad->imageUpdated(rect); + + Q_FOREACH (const QRect &rc, rects) { + m_scratchPad->imageUpdated(rc); + } } private: diff --git a/libs/widgetutils/KoResourcePaths.h b/libs/widgetutils/KoResourcePaths.h --- a/libs/widgetutils/KoResourcePaths.h +++ b/libs/widgetutils/KoResourcePaths.h @@ -226,9 +226,10 @@ static void setReady(); /** - * Assert that all resource paths have been initialized. + * Return if resource paths have been initialized and users + * of this class may expect to load resources from the proper paths. */ - static void assertReady(); + static bool isReady(); private: diff --git a/libs/widgetutils/KoResourcePaths.cpp b/libs/widgetutils/KoResourcePaths.cpp --- a/libs/widgetutils/KoResourcePaths.cpp +++ b/libs/widgetutils/KoResourcePaths.cpp @@ -558,7 +558,7 @@ s_instance->d->ready = true; } -void KoResourcePaths::assertReady() +bool KoResourcePaths::isReady() { - KIS_ASSERT_X(s_instance->d->ready, "KoResourcePaths::assertReady", "Resource paths are not ready yet."); + return s_instance->d->ready; } diff --git a/plugins/color/lcms2engine/LcmsEnginePlugin.cpp b/plugins/color/lcms2engine/LcmsEnginePlugin.cpp --- a/plugins/color/lcms2engine/LcmsEnginePlugin.cpp +++ b/plugins/color/lcms2engine/LcmsEnginePlugin.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "kis_assert.h" @@ -88,7 +89,12 @@ { // We need all resource paths to be properly initialized via KisApplication, otherwise we will // initialize this instance with lacking color profiles which will cause lookup errors later on. - KoResourcePaths::assertReady(); + + KIS_ASSERT_X(KoResourcePaths::isReady() || + (QApplication::instance()->applicationName() != "krita" && + QApplication::instance()->applicationName() != "krita.exe"), + "LcmsEnginePlugin::LcmsEnginePlugin", "Resource paths are not ready yet."); + // Set the lmcs error reporting function cmsSetLogErrorHandler(&lcms2LogErrorHandlerFunction); diff --git a/plugins/dockers/throttle/Throttle.h b/plugins/dockers/throttle/Throttle.h --- a/plugins/dockers/throttle/Throttle.h +++ b/plugins/dockers/throttle/Throttle.h @@ -20,7 +20,9 @@ #include + class KisCanvas2; +class KisSignalCompressor; class ThreadManager : public QObject { Q_OBJECT @@ -34,10 +36,14 @@ int threadCount() const; int maxThreadCount() const; +private Q_SLOTS: + void slotDoUpdateConfig(); + Q_SIGNALS: void threadCountChanged(); private: - int m_threadCount {0}; + int m_threadCount = 0; + KisSignalCompressor *m_configUpdateCompressor; }; class Throttle : public QQuickWidget { diff --git a/plugins/dockers/throttle/Throttle.cpp b/plugins/dockers/throttle/Throttle.cpp --- a/plugins/dockers/throttle/Throttle.cpp +++ b/plugins/dockers/throttle/Throttle.cpp @@ -32,25 +32,30 @@ #include #include #include +#include "kis_signal_compressor.h" + +#include "KisUpdateSchedulerConfigNotifier.h" + ThreadManager::ThreadManager(QObject *parent) - : QObject(parent) -{} + : QObject(parent), + m_configUpdateCompressor(new KisSignalCompressor(500, KisSignalCompressor::POSTPONE, this)) +{ + connect(m_configUpdateCompressor, SIGNAL(timeout()), SLOT(slotDoUpdateConfig())); +} ThreadManager::~ThreadManager() { } void ThreadManager::setThreadCount(int threadCount) { - threadCount = qMin(maxThreadCount(), int(qreal(threadCount) * (maxThreadCount() / 100.0))); + threadCount = 1 + qreal(threadCount) * (maxThreadCount() - 1) / 100.0; if (m_threadCount != threadCount) { m_threadCount = threadCount; + m_configUpdateCompressor->start(); emit threadCountChanged(); - KisImageConfig().setMaxNumberOfThreads(m_threadCount); - KisImageConfig().setFrameRenderingClones(qCeil(m_threadCount * 0.5)); - // XXX: also set for the brush threads } } @@ -64,6 +69,14 @@ return QThread::idealThreadCount(); } +void ThreadManager::slotDoUpdateConfig() +{ + KisImageConfig cfg; + cfg.setMaxNumberOfThreads(m_threadCount); + cfg.setFrameRenderingClones(qCeil(m_threadCount * 0.5)); + KisUpdateSchedulerConfigNotifier::instance()->notifyConfigChanged(); +} + Throttle::Throttle(QWidget *parent) : QQuickWidget(parent) diff --git a/plugins/paintops/defaultpaintops/CMakeLists.txt b/plugins/paintops/defaultpaintops/CMakeLists.txt --- a/plugins/paintops/defaultpaintops/CMakeLists.txt +++ b/plugins/paintops/defaultpaintops/CMakeLists.txt @@ -1,22 +1,29 @@ add_subdirectory(brush/tests) -include_directories(brush) -include_directories(duplicate) - +include_directories(brush + duplicate + ${CMAKE_CURRENT_BINARY_DIR}) set(kritadefaultpaintops_SOURCES defaultpaintops_plugin.cc brush/kis_brushop.cpp + brush/KisBrushOpResources.cpp + brush/KisBrushOpSettings.cpp brush/kis_brushop_settings_widget.cpp - duplicate/kis_duplicateop.cpp + brush/KisDabRenderingQueue.cpp + brush/KisDabRenderingQueueCache.cpp + brush/KisDabRenderingJob.cpp + brush/KisDabRenderingExecutor.cpp + duplicate/kis_duplicateop.cpp duplicate/kis_duplicateop_settings.cpp duplicate/kis_duplicateop_settings_widget.cpp duplicate/kis_duplicateop_option.cpp ) ki18n_wrap_ui(kritadefaultpaintops_SOURCES duplicate/wdgduplicateop.ui ) add_library(kritadefaultpaintops MODULE ${kritadefaultpaintops_SOURCES}) +generate_export_header(kritadefaultpaintops BASE_NAME kritadefaultpaintops EXPORT_MACRO_NAME KRITADEFAULTPAINTOPS_EXPORT) target_link_libraries(kritadefaultpaintops kritalibpaintop) diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.h copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to plugins/paintops/defaultpaintops/brush/KisBrushOpResources.h --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,30 +16,28 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISBRUSHOPRESOURCES_H +#define KISBRUSHOPRESOURCES_H + +#include "KisDabCacheUtils.h" #include +class KisPainter; +class KisPaintInformation; -class KisOpenglCanvasDebugger +class KisBrushOpResources : public KisDabCacheUtils::DabRenderingResources { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); - - static KisOpenglCanvasDebugger* instance(); - - bool showFpsOnCanvas() const; + KisBrushOpResources(const KisPaintOpSettingsSP settings, KisPainter *painter); + ~KisBrushOpResources() override; - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); + void syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info) override; private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISBRUSHOPRESOURCES_H diff --git a/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.cpp b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2017 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 "KisBrushOpResources.h" + +#include +#include + +#include "kis_color_source.h" +#include "kis_pressure_mix_option.h" +#include "kis_pressure_darken_option.h" +#include "kis_pressure_hsv_option.h" +#include "kis_color_source_option.h" +#include "kis_pressure_sharpness_option.h" +#include "kis_texture_option.h" +#include "kis_painter.h" +#include "kis_paintop_settings.h" + +struct KisBrushOpResources::Private +{ + QList hsvOptions; + KoColorTransformation *hsvTransformation = 0; + KisPressureMixOption mixOption; + KisPressureDarkenOption darkenOption; +}; + + +KisBrushOpResources::KisBrushOpResources(const KisPaintOpSettingsSP settings, KisPainter *painter) + : m_d(new Private) +{ + KisColorSourceOption colorSourceOption; + colorSourceOption.readOptionSetting(settings); + colorSource.reset(colorSourceOption.createColorSource(painter)); + + sharpnessOption.reset(new KisPressureSharpnessOption()); + sharpnessOption->readOptionSetting(settings); + sharpnessOption->resetAllSensors(); + + textureOption.reset(new KisTextureProperties(painter->device()->defaultBounds()->currentLevelOfDetail())); + textureOption->fillProperties(settings); + + m_d->hsvOptions.append(KisPressureHSVOption::createHueOption()); + m_d->hsvOptions.append(KisPressureHSVOption::createSaturationOption()); + m_d->hsvOptions.append(KisPressureHSVOption::createValueOption()); + + Q_FOREACH (KisPressureHSVOption * option, m_d->hsvOptions) { + option->readOptionSetting(settings); + option->resetAllSensors(); + if (option->isChecked() && !m_d->hsvTransformation) { + m_d->hsvTransformation = painter->backgroundColor().colorSpace()->createColorTransformation("hsv_adjustment", QHash()); + } + } + + m_d->darkenOption.readOptionSetting(settings); + m_d->mixOption.readOptionSetting(settings); + + m_d->darkenOption.resetAllSensors(); + m_d->mixOption.resetAllSensors(); +} + +KisBrushOpResources::~KisBrushOpResources() +{ + qDeleteAll(m_d->hsvOptions); + delete m_d->hsvTransformation; +} + +void KisBrushOpResources::syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info) +{ + colorSource->selectColor(m_d->mixOption.apply(info), info); + m_d->darkenOption.apply(colorSource.data(), info); + + if (m_d->hsvTransformation) { + Q_FOREACH (KisPressureHSVOption * option, m_d->hsvOptions) { + option->apply(m_d->hsvTransformation, info); + } + colorSource->applyColorTransformation(m_d->hsvTransformation); + } + + KisDabCacheUtils::DabRenderingResources::syncResourcesToSeqNo(seqNo, info); +} diff --git a/libs/image/kis_projection_updates_filter.cpp b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.h copy from libs/image/kis_projection_updates_filter.cpp copy to plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,16 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISBRUSHOPSETTINGS_H +#define KISBRUSHOPSETTINGS_H +#include "kis_brush_based_paintop_settings.h" -#include -#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisBrushOpSettings : public KisBrushBasedPaintOpSettings { -} +public: + bool needsAsynchronousUpdates() const; +}; -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISBRUSHOPSETTINGS_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.cpp copy from libs/image/kis_projection_updates_filter.cpp copy to plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.cpp --- a/libs/image/kis_projection_updates_filter.cpp +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,10 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisBrushOpSettings.h" -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() -{ -} - -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +bool KisBrushOpSettings::needsAsynchronousUpdates() const { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); return true; } diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.h new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2017 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 KISDABRENDERINGEXECUTOR_H +#define KISDABRENDERINGEXECUTOR_H + +#include "kritadefaultpaintops_export.h" + +#include + +#include +class KisRenderedDab; + +#include "KisDabCacheUtils.h" + +class KisPressureMirrorOption; +class KisPrecisionOption; +class KisRunnableStrokeJobsInterface; + + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingExecutor +{ +public: + KisDabRenderingExecutor(const KoColorSpace *cs, + KisDabCacheUtils::ResourcesFactory resourcesFactory, + KisRunnableStrokeJobsInterface *runnableJobsInterface, + KisPressureMirrorOption *mirrorOption = 0, + KisPrecisionOption *precisionOption = 0); + ~KisDabRenderingExecutor(); + + void addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow); + + QList takeReadyDabs(bool returnMutableDabs = false); + + bool hasPreparedDabs() const; + + int averageDabRenderingTime() const; // usecs + int averageDabSize() const; + + void recyclePaintDevicesForCache(const QVector devices); + +private: + KisDabRenderingExecutor(const KisDabRenderingExecutor &rhs) = delete; + + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISDABRENDERINGEXECUTOR_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017 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 "KisDabRenderingExecutor.h" + +#include "KisDabRenderingQueue.h" +#include "KisDabRenderingQueueCache.h" +#include "KisDabRenderingJob.h" +#include "KisRenderedDab.h" +#include "KisRunnableStrokeJobsInterface.h" +#include "KisRunnableStrokeJobData.h" +#include + +struct KisDabRenderingExecutor::Private +{ + QScopedPointer renderingQueue; + KisRunnableStrokeJobsInterface *runnableJobsInterface; +}; + +KisDabRenderingExecutor::KisDabRenderingExecutor(const KoColorSpace *cs, + KisDabCacheUtils::ResourcesFactory resourcesFactory, + KisRunnableStrokeJobsInterface *runnableJobsInterface, + KisPressureMirrorOption *mirrorOption, + KisPrecisionOption *precisionOption) + : m_d(new Private) +{ + m_d->runnableJobsInterface = runnableJobsInterface; + + m_d->renderingQueue.reset( + new KisDabRenderingQueue(cs, resourcesFactory)); + + KisDabRenderingQueueCache *cache = new KisDabRenderingQueueCache(); + cache->setMirrorPostprocessing(mirrorOption); + cache->setPrecisionOption(precisionOption); + + m_d->renderingQueue->setCacheInterface(cache); +} + +KisDabRenderingExecutor::~KisDabRenderingExecutor() +{ +} + +void KisDabRenderingExecutor::addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow) +{ + KisDabRenderingJobSP job = m_d->renderingQueue->addDab(request, opacity, flow); + if (job) { + m_d->runnableJobsInterface->addRunnableJob( + new FreehandStrokeRunnableJobDataWithUpdate( + new KisDabRenderingJobRunner(job, m_d->renderingQueue.data(), m_d->runnableJobsInterface), + KisStrokeJobData::CONCURRENT)); + } +} + +QList KisDabRenderingExecutor::takeReadyDabs(bool returnMutableDabs) +{ + return m_d->renderingQueue->takeReadyDabs(returnMutableDabs); +} + +bool KisDabRenderingExecutor::hasPreparedDabs() const +{ + return m_d->renderingQueue->hasPreparedDabs(); +} + +int KisDabRenderingExecutor::averageDabRenderingTime() const +{ + return m_d->renderingQueue->averageExecutionTime(); +} + +int KisDabRenderingExecutor::averageDabSize() const +{ + return m_d->renderingQueue->averageDabSize(); +} + +void KisDabRenderingExecutor::recyclePaintDevicesForCache(const QVector devices) +{ + m_d->renderingQueue->recyclePaintDevicesForCache(devices); +} + diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.h new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2017 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 KISDABRENDERINGJOB_H +#define KISDABRENDERINGJOB_H + +#include +#include +#include +#include +#include "kritadefaultpaintops_export.h" + +class KisDabRenderingQueue; +class KisRunnableStrokeJobsInterface; + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingJob +{ +public: + enum JobType { + Dab, + Postprocess, + Copy + }; + + enum Status { + New, + Running, + Completed + }; + +public: + KisDabRenderingJob(); + KisDabRenderingJob(int _seqNo, + KisDabCacheUtils::DabGenerationInfo _generationInfo, + JobType _type); + KisDabRenderingJob(const KisDabRenderingJob &rhs); + KisDabRenderingJob& operator=(const KisDabRenderingJob &rhs); + + QPoint dstDabOffset() const; + + int seqNo = -1; + KisDabCacheUtils::DabGenerationInfo generationInfo; + JobType type = Dab; + KisFixedPaintDeviceSP originalDevice; + KisFixedPaintDeviceSP postprocessedDevice; + + // high-level members, not directly related to job execution itself + Status status = New; + + qreal opacity = OPACITY_OPAQUE_F; + qreal flow = OPACITY_OPAQUE_F; +}; + +#include +typedef QSharedPointer KisDabRenderingJobSP; + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingJobRunner : public QRunnable +{ +public: + KisDabRenderingJobRunner(KisDabRenderingJobSP job, + KisDabRenderingQueue *parentQueue, + KisRunnableStrokeJobsInterface *runnableJobsInterface); + ~KisDabRenderingJobRunner(); + + void run() override; + + static int executeOneJob(KisDabRenderingJob *job, KisDabCacheUtils::DabRenderingResources *resources, KisDabRenderingQueue *parentQueue); + +private: + KisDabRenderingJobSP m_job; + KisDabRenderingQueue *m_parentQueue = 0; + KisRunnableStrokeJobsInterface *m_runnableJobsInterface = 0; +}; + + +#endif // KISDABRENDERINGJOB_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2017 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 "KisDabRenderingJob.h" + +#include + +#include +#include + +#include "KisDabCacheUtils.h" +#include "KisDabRenderingQueue.h" + +#include + + +KisDabRenderingJob::KisDabRenderingJob() +{ +} + +KisDabRenderingJob::KisDabRenderingJob(int _seqNo, KisDabCacheUtils::DabGenerationInfo _generationInfo, KisDabRenderingJob::JobType _type) + : seqNo(_seqNo), + generationInfo(_generationInfo), + type(_type) +{ +} + +KisDabRenderingJob::KisDabRenderingJob(const KisDabRenderingJob &rhs) + : seqNo(rhs.seqNo), + generationInfo(rhs.generationInfo), + type(rhs.type), + originalDevice(rhs.originalDevice), + postprocessedDevice(rhs.postprocessedDevice), + status(rhs.status), + opacity(rhs.opacity), + flow(rhs.flow) +{ +} + +KisDabRenderingJob &KisDabRenderingJob::operator=(const KisDabRenderingJob &rhs) +{ + seqNo = rhs.seqNo; + generationInfo = rhs.generationInfo; + type = rhs.type; + originalDevice = rhs.originalDevice; + postprocessedDevice = rhs.postprocessedDevice; + status = rhs.status; + opacity = rhs.opacity; + flow = rhs.flow; + + return *this; +} + +QPoint KisDabRenderingJob::dstDabOffset() const +{ + return generationInfo.dstDabRect.topLeft(); +} + + + +KisDabRenderingJobRunner::KisDabRenderingJobRunner(KisDabRenderingJobSP job, + KisDabRenderingQueue *parentQueue, + KisRunnableStrokeJobsInterface *runnableJobsInterface) + : m_job(job), + m_parentQueue(parentQueue), + m_runnableJobsInterface(runnableJobsInterface) +{ +} + +KisDabRenderingJobRunner::~KisDabRenderingJobRunner() +{ +} + +int KisDabRenderingJobRunner::executeOneJob(KisDabRenderingJob *job, + KisDabCacheUtils::DabRenderingResources *resources, + KisDabRenderingQueue *parentQueue) +{ + using namespace KisDabCacheUtils; + + KIS_SAFE_ASSERT_RECOVER_NOOP(job->type == KisDabRenderingJob::Dab || + job->type == KisDabRenderingJob::Postprocess); + + QElapsedTimer executionTime; + executionTime.start(); + + resources->syncResourcesToSeqNo(job->seqNo, job->generationInfo.info); + + if (job->type == KisDabRenderingJob::Dab) { + // TODO: thing about better interface for the reverse queue link + job->originalDevice = parentQueue->fetchCachedPaintDevce(); + + generateDab(job->generationInfo, resources, &job->originalDevice); + } + + // by now the original device should be already prepared + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(job->originalDevice, 0); + + if (job->type == KisDabRenderingJob::Dab || + job->type == KisDabRenderingJob::Postprocess) { + + if (job->generationInfo.needsPostprocessing) { + // TODO: cache postprocessed device + + if (!job->postprocessedDevice || + *job->originalDevice->colorSpace() != *job->postprocessedDevice->colorSpace()) { + + job->postprocessedDevice = parentQueue->fetchCachedPaintDevce(); + *job->postprocessedDevice = *job->originalDevice; + } else { + *job->postprocessedDevice = *job->originalDevice; + } + + postProcessDab(job->postprocessedDevice, + job->generationInfo.dstDabRect.topLeft(), + job->generationInfo.info, + resources); + } else { + job->postprocessedDevice = job->originalDevice; + } + } + + return executionTime.nsecsElapsed() / 1000; +} + +void KisDabRenderingJobRunner::run() +{ + int executionTime = 0; + + KisDabCacheUtils::DabRenderingResources *resources = m_parentQueue->fetchResourcesFromCache(); + + executionTime = executeOneJob(m_job.data(), resources, m_parentQueue); + QList jobs = m_parentQueue->notifyJobFinished(m_job->seqNo, executionTime); + + while (!jobs.isEmpty()) { + QVector dataList; + + // start all-but-the-first jobs asynchronously + for (int i = 1; i < jobs.size(); i++) { + dataList.append(new FreehandStrokeRunnableJobDataWithUpdate( + new KisDabRenderingJobRunner(jobs[i], m_parentQueue, m_runnableJobsInterface), + KisStrokeJobData::CONCURRENT)); + } + + m_runnableJobsInterface->addRunnableJobs(dataList); + + + // execute the first job in the current thread + KisDabRenderingJobSP job = jobs.first(); + executionTime = executeOneJob(job.data(), resources, m_parentQueue); + jobs = m_parentQueue->notifyJobFinished(job->seqNo, executionTime); + } + + m_parentQueue->putResourcesToCache(resources); +} diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.h new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 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 KISDABRENDERINGQUEUE_H +#define KISDABRENDERINGQUEUE_H + +#include + +#include "kritadefaultpaintops_export.h" + +#include +class KisDabRenderingJob; +class KisRenderedDab; + +#include "KisDabCacheUtils.h" + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingQueue +{ +public: + struct CacheInterface { + virtual ~CacheInterface() {} + virtual void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) = 0; + + virtual bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const = 0; + }; + + +public: + KisDabRenderingQueue(const KoColorSpace *cs, KisDabCacheUtils::ResourcesFactory resourcesFactory); + ~KisDabRenderingQueue(); + + KisDabRenderingJobSP addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow); + + QList notifyJobFinished(int seqNo, int usecsTime = -1); + + QList takeReadyDabs(bool returnMutableDabs = false); + + bool hasPreparedDabs() const; + + void setCacheInterface(CacheInterface *interface); + + KisFixedPaintDeviceSP fetchCachedPaintDevce(); + void recyclePaintDevicesForCache(const QVector devices); + + void putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources); + KisDabCacheUtils::DabRenderingResources* fetchResourcesFromCache(); + + int averageExecutionTime() const; + int averageDabSize() const; + + int testingGetQueueSize() const; + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISDABRENDERINGQUEUE_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.cpp @@ -0,0 +1,459 @@ +/* + * Copyright (c) 2017 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 "KisDabRenderingQueue.h" + +#include "KisDabRenderingJob.h" +#include "KisRenderedDab.h" +#include "kis_painter.h" + +#include +#include +#include +#include + +#include "kis_algebra_2d.h" + +struct KisDabRenderingQueue::Private +{ + struct DumbCacheInterface : public CacheInterface { + void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) override + { + Q_UNUSED(hasDabInCache); + Q_UNUSED(resources); + Q_UNUSED(request); + + di->needsPostprocessing = false; + *shouldUseCache = false; + } + + bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const override + { + Q_UNUSED(resources); + return false; + } + + }; + + Private(const KoColorSpace *_colorSpace, + KisDabCacheUtils::ResourcesFactory _resourcesFactory) + : cacheInterface(new DumbCacheInterface), + colorSpace(_colorSpace), + resourcesFactory(_resourcesFactory), + avgExecutionTime(50), + avgDabSize(50) + { + KIS_SAFE_ASSERT_RECOVER_NOOP(resourcesFactory); + } + + ~Private() { + qDeleteAll(cachedResources); + cachedResources.clear(); + } + + QList jobs; + int nextSeqNoToUse = 0; + int lastPaintedJob = -1; + int lastDabJobInQueue = -1; + QScopedPointer cacheInterface; + const KoColorSpace *colorSpace; + qreal averageOpacity = 0.0; + + KisDabCacheUtils::ResourcesFactory resourcesFactory; + + QList cachedResources; + QSet cachedPaintDevices; + + QMutex mutex; + + KisRollingMeanAccumulatorWrapper avgExecutionTime; + KisRollingMeanAccumulatorWrapper avgDabSize; + + int calculateLastDabJobIndex(int startSearchIndex); + void cleanPaintedDabs(); + bool dabsHaveSeparateOriginal(); + + KisDabCacheUtils::DabRenderingResources* fetchResourcesFromCache(); + void putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources); +}; + + +KisDabRenderingQueue::KisDabRenderingQueue(const KoColorSpace *cs, + KisDabCacheUtils::ResourcesFactory resourcesFactory) + : m_d(new Private(cs, resourcesFactory)) +{ +} + +KisDabRenderingQueue::~KisDabRenderingQueue() +{ +} + +int KisDabRenderingQueue::Private::calculateLastDabJobIndex(int startSearchIndex) +{ + if (startSearchIndex < 0) { + startSearchIndex = jobs.size() - 1; + } + + // try to use cached value + if (startSearchIndex >= lastDabJobInQueue) { + return lastDabJobInQueue; + } + + // if we are below the cached value, just iterate through the dabs + // (which is extremely(!) slow) + for (int i = startSearchIndex; i >= 0; i--) { + if (jobs[i]->type == KisDabRenderingJob::Dab) { + return i; + } + } + + return -1; +} + +KisDabRenderingJobSP KisDabRenderingQueue::addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow) +{ + QMutexLocker l(&m_d->mutex); + + const int seqNo = m_d->nextSeqNoToUse++; + + KisDabCacheUtils::DabRenderingResources *resources = m_d->fetchResourcesFromCache(); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(resources, KisDabRenderingJobSP()); + + // We should sync the cached brush into the current seqNo + resources->syncResourcesToSeqNo(seqNo, request.info); + + const int lastDabJobIndex = m_d->lastDabJobInQueue; + + KisDabRenderingJobSP job(new KisDabRenderingJob()); + + bool shouldUseCache = false; + m_d->cacheInterface->getDabType(lastDabJobIndex >= 0, + resources, + request, + &job->generationInfo, + &shouldUseCache); + + m_d->putResourcesToCache(resources); + resources = 0; + + // TODO: initialize via c-tor + job->seqNo = seqNo; + job->type = + !shouldUseCache ? KisDabRenderingJob::Dab : + job->generationInfo.needsPostprocessing ? KisDabRenderingJob::Postprocess : + KisDabRenderingJob::Copy; + job->opacity = opacity; + job->flow = flow; + + + if (job->type == KisDabRenderingJob::Dab) { + job->status = KisDabRenderingJob::Running; + } else if (job->type == KisDabRenderingJob::Postprocess || + job->type == KisDabRenderingJob::Copy) { + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastDabJobIndex >= 0, KisDabRenderingJobSP()); + + if (m_d->jobs[lastDabJobIndex]->status == KisDabRenderingJob::Completed) { + if (job->type == KisDabRenderingJob::Postprocess) { + job->status = KisDabRenderingJob::Running; + job->originalDevice = m_d->jobs[lastDabJobIndex]->originalDevice; + } else if (job->type == KisDabRenderingJob::Copy) { + job->status = KisDabRenderingJob::Completed; + job->originalDevice = m_d->jobs[lastDabJobIndex]->originalDevice; + job->postprocessedDevice = m_d->jobs[lastDabJobIndex]->postprocessedDevice; + } + } + } + + m_d->jobs.append(job); + + KisDabRenderingJobSP jobToRun; + if (job->status == KisDabRenderingJob::Running) { + jobToRun = job; + } + + if (job->type == KisDabRenderingJob::Dab) { + m_d->lastDabJobInQueue = m_d->jobs.size() - 1; + m_d->cleanPaintedDabs(); + } + + // collect some statistics about the dab + m_d->avgDabSize(KisAlgebra2D::maxDimension(job->generationInfo.dstDabRect)); + + return jobToRun; +} + +QList KisDabRenderingQueue::notifyJobFinished(int seqNo, int usecsTime) +{ + QMutexLocker l(&m_d->mutex); + + QList dependentJobs; + + /** + * Here we use binary search for locating the necessary original dab + */ + auto finishedJobIt = + std::lower_bound(m_d->jobs.begin(), m_d->jobs.end(), seqNo, + [] (KisDabRenderingJobSP job, int seqNo) { + return job->seqNo < seqNo; + }); + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(finishedJobIt != m_d->jobs.end(), dependentJobs); + KisDabRenderingJobSP finishedJob = *finishedJobIt; + + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->status == KisDabRenderingJob::Running); + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->seqNo == seqNo); + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->originalDevice); + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->postprocessedDevice); + + finishedJob->status = KisDabRenderingJob::Completed; + + if (finishedJob->type == KisDabRenderingJob::Dab) { + for (auto it = finishedJobIt + 1; it != m_d->jobs.end(); ++it) { + KisDabRenderingJobSP j = *it; + + // next dab job closes the chain + if (j->type == KisDabRenderingJob::Dab) break; + + // the non 'dab'-type job couldn't have + // been started before the source ob was completed + KIS_SAFE_ASSERT_RECOVER_BREAK(j->status == KisDabRenderingJob::New); + + if (j->type == KisDabRenderingJob::Copy) { + + j->originalDevice = finishedJob->originalDevice; + j->postprocessedDevice = finishedJob->postprocessedDevice; + j->status = KisDabRenderingJob::Completed; + + } else if (j->type == KisDabRenderingJob::Postprocess) { + + j->originalDevice = finishedJob->originalDevice; + j->status = KisDabRenderingJob::Running; + dependentJobs << j; + } + } + } + + if (usecsTime >= 0) { + m_d->avgExecutionTime(usecsTime); + } + + return dependentJobs; +} + +void KisDabRenderingQueue::Private::cleanPaintedDabs() +{ + const int nextToBePainted = lastPaintedJob + 1; + const int lastSourceJob = calculateLastDabJobIndex(qMin(nextToBePainted, jobs.size() - 1)); + + if (lastPaintedJob >= 0) { + int numRemovedJobs = 0; + int numRemovedJobsBeforeLastSource = 0; + + auto it = jobs.begin(); + for (int i = 0; i <= lastPaintedJob; i++) { + KisDabRenderingJobSP job = *it; + + if (i < lastSourceJob || job->type != KisDabRenderingJob::Dab){ + + // cache unique 'original' devices + if (job->type == KisDabRenderingJob::Dab && + job->postprocessedDevice != job->originalDevice) { + cachedPaintDevices << job->originalDevice; + job->originalDevice = 0; + } + + it = jobs.erase(it); + numRemovedJobs++; + if (i < lastSourceJob) { + numRemovedJobsBeforeLastSource++; + } + + } else { + ++it; + } + } + + KIS_SAFE_ASSERT_RECOVER_RETURN(jobs.size() > 0); + + lastPaintedJob -= numRemovedJobs; + lastDabJobInQueue -= numRemovedJobsBeforeLastSource; + } +} + +QList KisDabRenderingQueue::takeReadyDabs(bool returnMutableDabs) +{ + QMutexLocker l(&m_d->mutex); + + QList renderedDabs; + if (m_d->jobs.isEmpty()) return renderedDabs; + + KIS_SAFE_ASSERT_RECOVER_NOOP( + m_d->jobs.isEmpty() || + m_d->jobs.first()->type == KisDabRenderingJob::Dab); + + const int copyJobAfterInclusive = + returnMutableDabs && !m_d->dabsHaveSeparateOriginal() ? + m_d->lastDabJobInQueue : + std::numeric_limits::max(); + + for (int i = 0; i < m_d->jobs.size(); i++) { + KisDabRenderingJobSP j = m_d->jobs[i]; + + if (j->status != KisDabRenderingJob::Completed) break; + + if (i <= m_d->lastPaintedJob) continue; + + KisRenderedDab dab; + KisFixedPaintDeviceSP resultDevice = j->postprocessedDevice; + + if (i >= copyJobAfterInclusive) { + resultDevice = new KisFixedPaintDevice(*resultDevice); + } + + dab.device = resultDevice; + dab.offset = j->dstDabOffset(); + dab.opacity = j->opacity; + dab.flow = j->flow; + + m_d->averageOpacity = KisPainter::blendAverageOpacity(j->opacity, m_d->averageOpacity); + dab.averageOpacity = m_d->averageOpacity; + + + renderedDabs.append(dab); + + m_d->lastPaintedJob = i; + } + + m_d->cleanPaintedDabs(); + return renderedDabs; +} + +bool KisDabRenderingQueue::hasPreparedDabs() const +{ + QMutexLocker l(&m_d->mutex); + + const int nextToBePainted = m_d->lastPaintedJob + 1; + + return + nextToBePainted >= 0 && + nextToBePainted < m_d->jobs.size() && + m_d->jobs[nextToBePainted]->status == KisDabRenderingJob::Completed; +} + +void KisDabRenderingQueue::setCacheInterface(KisDabRenderingQueue::CacheInterface *interface) +{ + m_d->cacheInterface.reset(interface); +} + +KisFixedPaintDeviceSP KisDabRenderingQueue::fetchCachedPaintDevce() +{ + QMutexLocker l(&m_d->mutex); + + KisFixedPaintDeviceSP result; + + if (m_d->cachedPaintDevices.isEmpty()) { + result = new KisFixedPaintDevice(m_d->colorSpace); + } else { + // there is no difference from which side to take elements from QSet + auto it = m_d->cachedPaintDevices.begin(); + result = *it; + m_d->cachedPaintDevices.erase(it); + } + + return result; +} + +void KisDabRenderingQueue::recyclePaintDevicesForCache(const QVector devices) +{ + QMutexLocker l(&m_d->mutex); + + Q_FOREACH (KisFixedPaintDeviceSP device, devices) { + // the set automatically checks if the device is unique in the set + m_d->cachedPaintDevices << device; + } +} + +int KisDabRenderingQueue::averageExecutionTime() const +{ + QMutexLocker l(&m_d->mutex); + return qRound(m_d->avgExecutionTime.rollingMean()); +} + +int KisDabRenderingQueue::averageDabSize() const +{ + QMutexLocker l(&m_d->mutex); + return qRound(m_d->avgDabSize.rollingMean()); +} + +bool KisDabRenderingQueue::Private::dabsHaveSeparateOriginal() +{ + KisDabCacheUtils::DabRenderingResources *resources = fetchResourcesFromCache(); + + const bool result = cacheInterface->hasSeparateOriginal(resources); + + putResourcesToCache(resources); + + return result; +} + +KisDabCacheUtils::DabRenderingResources *KisDabRenderingQueue::Private::fetchResourcesFromCache() +{ + KisDabCacheUtils::DabRenderingResources *resources = 0; + + // fetch/create a temporary resources object + if (!cachedResources.isEmpty()) { + resources = cachedResources.takeLast(); + } else { + resources = resourcesFactory(); + } + + return resources; +} + +void KisDabRenderingQueue::Private::putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources) +{ + cachedResources << resources; +} + +KisDabCacheUtils::DabRenderingResources *KisDabRenderingQueue::fetchResourcesFromCache() +{ + // TODO: make a separate lock for that + QMutexLocker l(&m_d->mutex); + return m_d->fetchResourcesFromCache(); +} + +void KisDabRenderingQueue::putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources) +{ + QMutexLocker l(&m_d->mutex); + m_d->putResourcesToCache(resources); +} + +int KisDabRenderingQueue::testingGetQueueSize() const +{ + QMutexLocker l(&m_d->mutex); + + return m_d->jobs.size(); +} + diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.h new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 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 KISDABRENDERINGQUEUECACHE_H +#define KISDABRENDERINGQUEUECACHE_H + +#include "KisDabRenderingQueue.h" +#include "kis_dab_cache_base.h" + +#include "kritadefaultpaintops_export.h" + +class KisPressureMirrorOption; +class KisPrecisionOption; +class KisPressureSharpnessOption; + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingQueueCache : public KisDabRenderingQueue::CacheInterface, public KisDabCacheBase +{ +public: + +public: + KisDabRenderingQueueCache(); + ~KisDabRenderingQueueCache(); + + void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) override; + + bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const override; + +private: + struct Private; + QScopedPointer m_d; +}; + +#endif // KISDABRENDERINGQUEUECACHE_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017 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 "KisDabRenderingQueueCache.h" + +struct KisDabRenderingQueueCache::Private +{ + Private() + { + } +}; + +KisDabRenderingQueueCache::KisDabRenderingQueueCache() + : m_d(new Private()) +{ +} + +KisDabRenderingQueueCache::~KisDabRenderingQueueCache() +{ +} + +void KisDabRenderingQueueCache::getDabType(bool hasDabInCache, KisDabCacheUtils::DabRenderingResources *resources, const KisDabCacheUtils::DabRequestInfo &request, KisDabCacheUtils::DabGenerationInfo *di, bool *shouldUseCache) +{ + fetchDabGenerationInfo(hasDabInCache, resources, request, di, shouldUseCache); +} + +bool KisDabRenderingQueueCache::hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const +{ + return needSeparateOriginal(resources->textureOption.data(), resources->sharpnessOption.data()); +} diff --git a/plugins/paintops/defaultpaintops/brush/kis_brushop.h b/plugins/paintops/defaultpaintops/brush/kis_brushop.h --- a/plugins/paintops/defaultpaintops/brush/kis_brushop.h +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop.h @@ -25,25 +25,27 @@ #include "kis_brush_based_paintop.h" #include -#include #include #include #include #include #include -#include -#include #include #include #include -#include #include #include #include +#include + +#include + class KisPainter; class KisColorSource; - +class KisDabRenderingExecutor; +class KisRenderedDab; +class KisRunnableStrokeJobData; class KisBrushOp : public KisBrushBasedPaintOp { @@ -55,15 +57,27 @@ void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) override; + int doAsyncronousUpdate(QVector &jobs) override; + protected: KisSpacingInformation paintAt(const KisPaintInformation& info) override; KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const override; KisTimingInformation updateTimingImpl(const KisPaintInformation &info) const override; + struct UpdateSharedState; + typedef QSharedPointer UpdateSharedStateSP; + + void addMirroringJobs(Qt::Orientation direction, + QVector &rects, + UpdateSharedStateSP state, + QVector &jobs); + + UpdateSharedStateSP m_updateSharedState; + + private: - KisColorSource *m_colorSource; KisAirbrushOption m_airbrushOption; KisPressureSizeOption m_sizeOption; KisPressureRatioOption m_ratioOption; @@ -73,15 +87,17 @@ KisFlowOpacityOption m_opacityOption; KisPressureSoftnessOption m_softnessOption; KisPressureSharpnessOption m_sharpnessOption; - KisPressureDarkenOption m_darkenOption; KisPressureRotationOption m_rotationOption; - KisPressureMixOption m_mixOption; KisPressureScatterOption m_scatterOption; - QList m_hsvOptions; - KoColorTransformation *m_hsvTransformation; KisPaintDeviceSP m_lineCacheDevice; - KisPaintDeviceSP m_colorSourceDevice; + + QScopedPointer m_dabExecutor; + qreal m_currentUpdatePeriod = 20.0; + KisRollingMeanAccumulatorWrapper m_avgSpacing; + KisRollingMeanAccumulatorWrapper m_avgNumDabs; + + const int m_idealNumRects; }; #endif // KIS_BRUSHOP_H_ diff --git a/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp b/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp --- a/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp @@ -37,36 +37,38 @@ #include #include #include -#include -#include -#include #include #include +#include "krita_utils.h" +#include +#include "kis_algebra_2d.h" +#include +#include +#include +#include "KisBrushOpResources.h" +#include +#include + +#include +#include +#include "kis_image_config.h" KisBrushOp::KisBrushOp(const KisPaintOpSettingsSP settings, KisPainter *painter, KisNodeSP node, KisImageSP image) : KisBrushBasedPaintOp(settings, painter) , m_opacityOption(node) - , m_hsvTransformation(0) + , m_avgSpacing(50) + , m_avgNumDabs(50) + , m_idealNumRects(KisImageConfig().maxNumberOfThreads()) { Q_UNUSED(image); Q_ASSERT(settings); - KisColorSourceOption colorSourceOption; - colorSourceOption.readOptionSetting(settings); - m_colorSource = colorSourceOption.createColorSource(painter); - - m_hsvOptions.append(KisPressureHSVOption::createHueOption()); - m_hsvOptions.append(KisPressureHSVOption::createSaturationOption()); - m_hsvOptions.append(KisPressureHSVOption::createValueOption()); - - Q_FOREACH (KisPressureHSVOption * option, m_hsvOptions) { - option->readOptionSetting(settings); - option->resetAllSensors(); - if (option->isChecked() && !m_hsvTransformation) { - m_hsvTransformation = painter->backgroundColor().colorSpace()->createColorTransformation("hsv_adjustment", QHash()); - } - } + /** + * We do our own threading here, so we need to forbid the brushes + * to do threading internally + */ + m_brush->setThreadingAllowed(false); m_airbrushOption.readOptionSetting(settings); @@ -77,32 +79,45 @@ m_spacingOption.readOptionSetting(settings); m_rateOption.readOptionSetting(settings); m_softnessOption.readOptionSetting(settings); - m_sharpnessOption.readOptionSetting(settings); - m_darkenOption.readOptionSetting(settings); m_rotationOption.readOptionSetting(settings); - m_mixOption.readOptionSetting(settings); m_scatterOption.readOptionSetting(settings); + m_sharpnessOption.readOptionSetting(settings); m_opacityOption.resetAllSensors(); m_flowOption.resetAllSensors(); m_sizeOption.resetAllSensors(); m_ratioOption.resetAllSensors(); m_rateOption.resetAllSensors(); m_softnessOption.resetAllSensors(); m_sharpnessOption.resetAllSensors(); - m_darkenOption.resetAllSensors(); m_rotationOption.resetAllSensors(); m_scatterOption.resetAllSensors(); + m_sharpnessOption.resetAllSensors(); - m_dabCache->setSharpnessPostprocessing(&m_sharpnessOption); m_rotationOption.applyFanCornersInfo(this); + + KisBrushSP baseBrush = m_brush; + auto resourcesFactory = + [baseBrush, settings, painter] () { + KisDabCacheUtils::DabRenderingResources *resources = + new KisBrushOpResources(settings, painter); + resources->brush = baseBrush->clone(); + + return resources; + }; + + + m_dabExecutor.reset( + new KisDabRenderingExecutor( + painter->device()->compositionSourceColorSpace(), + resourcesFactory, + painter->runnableStrokeJobsInterface(), + &m_mirrorOption, + &m_precisionOption)); } KisBrushOp::~KisBrushOp() { - qDeleteAll(m_hsvOptions); - delete m_colorSource; - delete m_hsvTransformation; } KisSpacingInformation KisBrushOp::paintAt(const KisPaintInformation& info) @@ -124,51 +139,174 @@ qreal rotation = m_rotationOption.apply(info); qreal ratio = m_ratioOption.apply(info); - KisPaintDeviceSP device = painter()->device(); - - KisDabShape shape(scale, ratio, rotation); QPointF cursorPos = m_scatterOption.apply(info, brush->maskWidth(shape, 0, 0, info), brush->maskHeight(shape, 0, 0, info)); - quint8 origOpacity = painter()->opacity(); - m_opacityOption.setFlow(m_flowOption.apply(info)); - m_opacityOption.apply(painter(), info); - m_colorSource->selectColor(m_mixOption.apply(info), info); - m_darkenOption.apply(m_colorSource, info); - if (m_hsvTransformation) { - Q_FOREACH (KisPressureHSVOption * option, m_hsvOptions) { - option->apply(m_hsvTransformation, info); - } - m_colorSource->applyColorTransformation(m_hsvTransformation); + quint8 dabOpacity = OPACITY_OPAQUE_U8; + quint8 dabFlow = OPACITY_OPAQUE_U8; + + m_opacityOption.apply(info, &dabOpacity, &dabFlow); + + KisDabCacheUtils::DabRequestInfo request(painter()->paintColor(), + cursorPos, + shape, + info, + m_softnessOption.apply(info)); + + m_dabExecutor->addDab(request, qreal(dabOpacity) / 255.0, qreal(dabFlow) / 255.0); + + + KisSpacingInformation spacingInfo = + effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); + + // gather statistics about dabs + m_avgSpacing(spacingInfo.scalarApprox()); + + return spacingInfo; +} + +struct KisBrushOp::UpdateSharedState +{ + // rendering data + KisPainter *painter = 0; + QList dabsQueue; + + // speed metrics + QVector dabPoints; + QElapsedTimer dabRenderingTimer; + + // final report + QVector allDirtyRects; +}; + +void KisBrushOp::addMirroringJobs(Qt::Orientation direction, + QVector &rects, + UpdateSharedStateSP state, + QVector &jobs) +{ + jobs.append(new KisRunnableStrokeJobData(0, KisStrokeJobData::SEQUENTIAL)); + + for (KisRenderedDab &dab : state->dabsQueue) { + jobs.append( + new KisRunnableStrokeJobData( + [state, &dab, direction] () { + state->painter->mirrorDab(direction, &dab); + }, + KisStrokeJobData::CONCURRENT)); } - QRect dabRect; - KisFixedPaintDeviceSP dab = m_dabCache->fetchDab(device->compositionSourceColorSpace(), - m_colorSource, - cursorPos, - shape, - info, - m_softnessOption.apply(info), - &dabRect); - - // sanity check for the size calculation code - if (dab->bounds().size() != dabRect.size()) { - warnKrita << "KisBrushOp: dab bounds is not dab rect. See bug 327156" << dab->bounds().size() << dabRect.size(); + jobs.append(new KisRunnableStrokeJobData(0, KisStrokeJobData::SEQUENTIAL)); + + for (QRect &rc : rects) { + state->painter->mirrorRect(direction, &rc); + + jobs.append( + new KisRunnableStrokeJobData( + [rc, state] () { + state->painter->bltFixed(rc, state->dabsQueue); + }, + KisStrokeJobData::CONCURRENT)); } - painter()->bltFixed(dabRect.topLeft(), dab, dab->bounds()); + state->allDirtyRects.append(rects); +} - painter()->renderMirrorMaskSafe(dabRect, - dab, - !m_dabCache->needSeparateOriginal()); - painter()->setOpacity(origOpacity); +int KisBrushOp::doAsyncronousUpdate(QVector &jobs) +{ + if (!m_updateSharedState && m_dabExecutor->hasPreparedDabs()) { - return effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); + m_updateSharedState = toQShared(new UpdateSharedState()); + UpdateSharedStateSP state = m_updateSharedState; + + state->painter = painter(); + + state->dabsQueue = m_dabExecutor->takeReadyDabs(painter()->hasMirroring()); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!state->dabsQueue.isEmpty(), + m_currentUpdatePeriod); + + const int diameter = m_dabExecutor->averageDabSize(); + const qreal spacing = m_avgSpacing.rollingMean(); + + const int idealNumRects = m_idealNumRects; + QVector rects = + KisPaintOpUtils::splitDabsIntoRects(state->dabsQueue, + idealNumRects, diameter, spacing); + + state->allDirtyRects = rects; + + Q_FOREACH (const KisRenderedDab &dab, state->dabsQueue) { + state->dabPoints.append(dab.realBounds().center()); + } + + state->dabRenderingTimer.start(); + + Q_FOREACH (const QRect &rc, rects) { + jobs.append( + new KisRunnableStrokeJobData( + [rc, state] () { + state->painter->bltFixed(rc, state->dabsQueue); + }, + KisStrokeJobData::CONCURRENT)); + } + + if (state->painter->hasHorizontalMirroring()) { + addMirroringJobs(Qt::Horizontal, rects, state, jobs); + } + + if (state->painter->hasVerticalMirroring()) { + addMirroringJobs(Qt::Vertical, rects, state, jobs); + } + + if (state->painter->hasHorizontalMirroring() && state->painter->hasVerticalMirroring()) { + addMirroringJobs(Qt::Horizontal, rects, state, jobs); + } + + jobs.append( + new KisRunnableStrokeJobData( + [state, this] () { + Q_FOREACH(const QRect &rc, state->allDirtyRects) { + state->painter->addDirtyRect(rc); + } + + state->painter->setAverageOpacity(state->dabsQueue.last().averageOpacity); + + const int updateRenderingTime = state->dabRenderingTimer.elapsed(); + const int dabRenderingTime = m_dabExecutor->averageDabRenderingTime() / 1000; + m_avgNumDabs(state->dabsQueue.size()); + + QVector recycledDevices; + for (auto it = state->dabsQueue.begin(); it != state->dabsQueue.end(); ++it) { + // we don't need to check for uniqueness, it is done by the queue + recycledDevices << it->device; + it->device.clear(); + } + m_dabExecutor->recyclePaintDevicesForCache(recycledDevices); + + + + const int approxDabRenderingTime = qreal(dabRenderingTime) / m_idealNumRects * m_avgNumDabs.rollingMean(); + + m_currentUpdatePeriod = qBound(20, int(1.5 * (approxDabRenderingTime + updateRenderingTime)), 100); + + + { // debug chunk +// const int updateRenderingTime = state->dabRenderingTimer.nsecsElapsed() / 1000; +// const int dabRenderingTime = m_dabExecutor->averageDabRenderingTime(); +// ENTER_FUNCTION() << ppVar(state->allDirtyRects.size()) << ppVar(state->dabsQueue.size()) << ppVar(dabRenderingTime) << ppVar(updateRenderingTime); +// ENTER_FUNCTION() << ppVar(m_currentUpdatePeriod); + } + + m_updateSharedState.clear(); + }, + KisStrokeJobData::SEQUENTIAL)); + } + + return m_currentUpdatePeriod; } KisSpacingInformation KisBrushOp::updateSpacingImpl(const KisPaintInformation &info) const diff --git a/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp b/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp --- a/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp @@ -21,7 +21,7 @@ */ #include "kis_brushop_settings_widget.h" -#include +#include #include #include #include @@ -90,7 +90,7 @@ KisPropertiesConfigurationSP KisBrushOpSettingsWidget::configuration() const { - KisBrushBasedPaintOpSettingsSP config = new KisBrushBasedPaintOpSettings(); + KisBrushBasedPaintOpSettingsSP config = new KisBrushOpSettings(); config->setOptionsWidget(const_cast(this)); config->setProperty("paintop", "paintbrush"); // XXX: make this a const id string writeConfiguration(config); diff --git a/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt b/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt --- a/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt +++ b/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt @@ -8,6 +8,12 @@ include(ECMAddTests) +ecm_add_test(KisDabRenderingQueueTest.cpp + TEST_NAME KisDabRenderingQueueTest + LINK_LIBRARIES kritadefaultpaintops kritalibpaintop kritaimage Qt5::Test) + + + krita_add_broken_unit_test(kis_brushop_test.cpp ../../../../../sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-plugins-KisBrushOpTest LINK_LIBRARIES kritaimage kritaui kritalibpaintop Qt5::Test) diff --git a/libs/image/kis_projection_updates_filter.cpp b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.h copy from libs/image/kis_projection_updates_filter.cpp copy to plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.h --- a/libs/image/kis_projection_updates_filter.cpp +++ b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 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 @@ -16,21 +16,20 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISDABRENDERINGQUEUETEST_H +#define KISDABRENDERINGQUEUETEST_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisDabRenderingQueueTest : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void testCachedDabs(); + void testPostprocessedDabs(); + void testRunningJobs(); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} + void testExecutor(); +}; + +#endif // KISDABRENDERINGQUEUETEST_H diff --git a/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.cpp b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.cpp @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2017 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 "KisDabRenderingQueueTest.h" + +#include +#include +#include + +#include <../KisDabRenderingQueue.h> +#include <../KisRenderedDab.h> +#include <../KisDabRenderingJob.h> + +struct SurrogateCacheInterface : public KisDabRenderingQueue::CacheInterface +{ + void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) override + { + Q_UNUSED(resources); + Q_UNUSED(request); + + if (!hasDabInCache || typeOverride == KisDabRenderingJob::Dab) { + di->needsPostprocessing = false; + *shouldUseCache = false; + } else if (typeOverride == KisDabRenderingJob::Copy) { + di->needsPostprocessing = false; + *shouldUseCache = true; + } else if (typeOverride == KisDabRenderingJob::Postprocess) { + di->needsPostprocessing = true; + *shouldUseCache = true; + } + + di->info = request.info; + } + + bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const override { + Q_UNUSED(resources); + return typeOverride == KisDabRenderingJob::Postprocess; + } + + KisDabRenderingJob::JobType typeOverride = KisDabRenderingJob::Dab; +}; + +#include +#include "kis_auto_brush.h" + +KisDabCacheUtils::DabRenderingResources *testResourcesFactory() +{ + KisDabCacheUtils::DabRenderingResources *resources = + new KisDabCacheUtils::DabRenderingResources(); + + KisCircleMaskGenerator* circle = new KisCircleMaskGenerator(10, 1.0, 1.0, 1.0, 2, false); + KisBrushSP brush = new KisAutoBrush(circle, 0.0, 0.0); + resources->brush = brush; + + return resources; +} + +void KisDabRenderingQueueTest::testCachedDabs() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + SurrogateCacheInterface *cacheInterface = new SurrogateCacheInterface(); + + KisDabRenderingQueue queue(cs, testResourcesFactory); + queue.setCacheInterface(cacheInterface); + + KoColor color; + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job0 = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job0); + QCOMPARE(job0->seqNo, 0); + QCOMPARE(job0->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job0->type, KisDabRenderingJob::Dab); + QVERIFY(!job0->originalDevice); + QVERIFY(!job0->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job1 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job1); + QCOMPARE(job1->seqNo, 1); + QCOMPARE(job1->generationInfo.info.pos(), request2.info.pos()); + QCOMPARE(job1->type, KisDabRenderingJob::Dab); + QVERIFY(!job1->originalDevice); + QVERIFY(!job1->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Copy; + KisDabRenderingJobSP job2 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job2); + + cacheInterface->typeOverride = KisDabRenderingJob::Copy; + KisDabRenderingJobSP job3 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job3); + + // we only added the dabs, but we haven't completed them yet + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 4); + + QList jobs; + QList renderedDabs; + + + { + // we've completed job0 + job0->originalDevice = new KisFixedPaintDevice(cs); + job0->postprocessedDevice = job0->originalDevice; + + jobs = queue.notifyJobFinished(job0->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 3); + } + + { + // we've completed job1 + job1->originalDevice = new KisFixedPaintDevice(cs); + job1->postprocessedDevice = job1->originalDevice; + + jobs = queue.notifyJobFinished(job1->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 3); + + // since they are copies, they should be the same + QCOMPARE(renderedDabs[1].device, renderedDabs[0].device); + QCOMPARE(renderedDabs[2].device, renderedDabs[0].device); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add one more cached job and take it + cacheInterface->typeOverride = KisDabRenderingJob::Copy; + KisDabRenderingJobSP job = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add a 'dab' job and complete it + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job); + QCOMPARE(job->seqNo, 5); + QCOMPARE(job->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job->type, KisDabRenderingJob::Dab); + QVERIFY(!job->originalDevice); + QVERIFY(!job->postprocessedDevice); + + // now the queue can be cleared from the completed dabs! + QCOMPARE(queue.testingGetQueueSize(), 1); + + job->originalDevice = new KisFixedPaintDevice(cs); + job->postprocessedDevice = job->originalDevice; + + jobs = queue.notifyJobFinished(job->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we do not delete the queue of jobs until the next 'dab' + // job arrives + QCOMPARE(queue.testingGetQueueSize(), 1); + } + +} + +void KisDabRenderingQueueTest::testPostprocessedDabs() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + SurrogateCacheInterface *cacheInterface = new SurrogateCacheInterface(); + + KisDabRenderingQueue queue(cs, testResourcesFactory); + queue.setCacheInterface(cacheInterface); + + KoColor color; + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job0 = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job0); + QCOMPARE(job0->seqNo, 0); + QCOMPARE(job0->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job0->type, KisDabRenderingJob::Dab); + QVERIFY(!job0->originalDevice); + QVERIFY(!job0->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job1 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job1); + QCOMPARE(job1->seqNo, 1); + QCOMPARE(job1->generationInfo.info.pos(), request2.info.pos()); + QCOMPARE(job1->type, KisDabRenderingJob::Dab); + QVERIFY(!job1->originalDevice); + QVERIFY(!job1->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Postprocess; + KisDabRenderingJobSP job2 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job2); + + cacheInterface->typeOverride = KisDabRenderingJob::Postprocess; + KisDabRenderingJobSP job3 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job3); + + // we only added the dabs, but we haven't completed them yet + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 4); + + QList jobs; + QList renderedDabs; + + + { + // we've completed job0 + job0->originalDevice = new KisFixedPaintDevice(cs); + job0->postprocessedDevice = job0->originalDevice; + + jobs = queue.notifyJobFinished(job0->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 3); + } + + { + // we've completed job1 + job1->originalDevice = new KisFixedPaintDevice(cs); + job1->postprocessedDevice = job1->originalDevice; + + jobs = queue.notifyJobFinished(job1->seqNo); + QCOMPARE(jobs.size(), 2); + + QCOMPARE(jobs[0]->seqNo, 2); + QCOMPARE(jobs[1]->seqNo, 3); + + QVERIFY(jobs[0]->originalDevice); + QVERIFY(!jobs[0]->postprocessedDevice); + + QVERIFY(jobs[1]->originalDevice); + QVERIFY(!jobs[1]->postprocessedDevice); + + // pretend we have created a postprocessed device + jobs[0]->postprocessedDevice = new KisFixedPaintDevice(cs); + jobs[1]->postprocessedDevice = new KisFixedPaintDevice(cs); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + + // return back two postprocessed dabs + QList emptyJobs; + emptyJobs = queue.notifyJobFinished(jobs[0]->seqNo); + QVERIFY(emptyJobs.isEmpty()); + + emptyJobs = queue.notifyJobFinished(jobs[1]->seqNo); + QVERIFY(emptyJobs.isEmpty()); + + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 2); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add one more postprocessed job and take it + cacheInterface->typeOverride = KisDabRenderingJob::Postprocess; + KisDabRenderingJobSP job = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job); + QCOMPARE(job->seqNo, 4); + QCOMPARE(job->generationInfo.info.pos(), request2.info.pos()); + ENTER_FUNCTION() << ppVar(job->type); + + QCOMPARE(job->type, KisDabRenderingJob::Postprocess); + QVERIFY(job->originalDevice); + QVERIFY(!job->postprocessedDevice); + + // the list should still be empty + QVERIFY(!queue.hasPreparedDabs()); + + // pretend we have created a postprocessed device + job->postprocessedDevice = new KisFixedPaintDevice(cs); + + // return back the postprocessed dab + QList emptyJobs; + emptyJobs = queue.notifyJobFinished(job->seqNo); + QVERIFY(emptyJobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add a 'dab' job and complete it. That will clear the queue! + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job); + QCOMPARE(job->seqNo, 5); + QCOMPARE(job->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job->type, KisDabRenderingJob::Dab); + QVERIFY(!job->originalDevice); + QVERIFY(!job->postprocessedDevice); + + // now the queue can be cleared from the completed dabs! + QCOMPARE(queue.testingGetQueueSize(), 1); + + job->originalDevice = new KisFixedPaintDevice(cs); + job->postprocessedDevice = job->originalDevice; + + jobs = queue.notifyJobFinished(job->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we do not delete the queue of jobs until the next 'dab' + // job arrives + QCOMPARE(queue.testingGetQueueSize(), 1); + } + +} + +#include <../KisDabRenderingQueueCache.h> + +void KisDabRenderingQueueTest::testRunningJobs() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + KisDabRenderingQueueCache *cacheInterface = new KisDabRenderingQueueCache(); + // we do *not* initialize any options yet! + + KisDabRenderingQueue queue(cs, testResourcesFactory); + queue.setCacheInterface(cacheInterface); + + + KoColor color(Qt::red, cs); + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + KisDabRenderingJobSP job0 = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job0); + QCOMPARE(job0->seqNo, 0); + QCOMPARE(job0->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job0->type, KisDabRenderingJob::Dab); + + QVERIFY(!job0->originalDevice); + QVERIFY(!job0->postprocessedDevice); + + KisDabRenderingJobRunner runner(job0, &queue, 0); + runner.run(); + + QVERIFY(job0->originalDevice); + QVERIFY(job0->postprocessedDevice); + QCOMPARE(job0->originalDevice, job0->postprocessedDevice); + + QVERIFY(!job0->originalDevice->bounds().isEmpty()); + + KisDabRenderingJobSP job1 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job1); + + QList renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 2); + + // we did the caching + QVERIFY(renderedDabs[0].device == renderedDabs[1].device); + + QCOMPARE(renderedDabs[0].offset, QPoint(5,5)); + QCOMPARE(renderedDabs[1].offset, QPoint(15,15)); +} + +#include "../KisDabRenderingExecutor.h" +#include "KisFakeRunnableStrokeJobsExecutor.h" + +void KisDabRenderingQueueTest::testExecutor() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + QScopedPointer runner(new KisFakeRunnableStrokeJobsExecutor()); + + KisDabRenderingExecutor executor(cs, testResourcesFactory, runner.data()); + + KoColor color(Qt::red, cs); + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + executor.addDab(request1, 0.5, 0.25); + executor.addDab(request2, 0.125, 1.0); + + QList renderedDabs = executor.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 2); + + // we did the caching + QVERIFY(renderedDabs[0].device == renderedDabs[1].device); + + QCOMPARE(renderedDabs[0].offset, QPoint(5,5)); + QCOMPARE(renderedDabs[1].offset, QPoint(15,15)); + + QCOMPARE(renderedDabs[0].opacity, 0.5); + QCOMPARE(renderedDabs[0].flow, 0.25); + QCOMPARE(renderedDabs[1].opacity, 0.125); + QCOMPARE(renderedDabs[1].flow, 1.0); + +} + +QTEST_MAIN(KisDabRenderingQueueTest) diff --git a/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc b/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc --- a/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc +++ b/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc @@ -33,7 +33,7 @@ #include "kis_duplicateop_settings.h" #include "kis_global.h" #include -#include "kis_brush_based_paintop_settings.h" +#include "KisBrushOpSettings.h" #include "kis_brush_server.h" #include "kis_duplicateop_settings_widget.h" @@ -44,7 +44,7 @@ : QObject(parent) { KisPaintOpRegistry *r = KisPaintOpRegistry::instance(); - r->add(new KisSimplePaintOpFactory("paintbrush", i18nc("Pixel paintbrush", "Pixel"), KisPaintOpFactory::categoryStable(), "krita-paintbrush.png", QString(), QStringList(), 1)); + r->add(new KisSimplePaintOpFactory("paintbrush", i18nc("Pixel paintbrush", "Pixel"), KisPaintOpFactory::categoryStable(), "krita-paintbrush.png", QString(), QStringList(), 1)); r->add(new KisSimplePaintOpFactory("duplicate", i18nc("clone paintbrush (previously \"Duplicate\")", "Clone"), KisPaintOpFactory::categoryStable(), "krita-duplicate.png", QString(), QStringList(COMPOSITE_COPY), 15)); KisBrushServer::instance(); diff --git a/plugins/paintops/deform/deform_brush.cpp b/plugins/paintops/deform/deform_brush.cpp --- a/plugins/paintops/deform/deform_brush.cpp +++ b/plugins/paintops/deform/deform_brush.cpp @@ -174,10 +174,7 @@ // clear if (dab->bounds().width() != dstWidth || dab->bounds().height() != dstHeight) { dab->setRect(m_maskRect.toRect()); - dab->initialize(); - } - else { - dab->clear(m_maskRect.toRect()); + dab->lazyGrowBufferWithoutInitialization(); } qreal const centerX = dstWidth * 0.5 + subPixelX; @@ -201,7 +198,7 @@ } mask->setRect(dab->bounds()); - mask->initialize(); + mask->lazyGrowBufferWithoutInitialization(); quint8* maskPointer = mask->data(); qint8 maskPixelSize = mask->pixelSize(); diff --git a/plugins/paintops/libpaintop/CMakeLists.txt b/plugins/paintops/libpaintop/CMakeLists.txt --- a/plugins/paintops/libpaintop/CMakeLists.txt +++ b/plugins/paintops/libpaintop/CMakeLists.txt @@ -19,6 +19,8 @@ kis_custom_brush_widget.cpp kis_clipboard_brush_widget.cpp kis_dynamic_sensor.cc + KisDabCacheUtils.cpp + kis_dab_cache_base.cpp kis_dab_cache.cpp kis_filter_option.cpp kis_multi_sensors_model_p.cpp diff --git a/plugins/paintops/libpaintop/KisDabCacheUtils.h b/plugins/paintops/libpaintop/KisDabCacheUtils.h new file mode 100644 --- /dev/null +++ b/plugins/paintops/libpaintop/KisDabCacheUtils.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2017 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 KISDABCACHEUTILS_H +#define KISDABCACHEUTILS_H + +#include +#include + +#include "kis_types.h" + +#include +#include "kis_dab_shape.h" + +#include "kritapaintop_export.h" +#include + +class KisBrush; +typedef KisSharedPtr KisBrushSP; + +class KisColorSource; +class KisPressureSharpnessOption; +class KisTextureProperties; + + +namespace KisDabCacheUtils +{ + +struct PAINTOP_EXPORT DabRenderingResources +{ + DabRenderingResources(); + virtual ~DabRenderingResources(); + + virtual void syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info); + + KisBrushSP brush; + QScopedPointer colorSource; + + QScopedPointer sharpnessOption; + QScopedPointer textureOption; + + KisPaintDeviceSP colorSourceDevice; + +private: + DabRenderingResources(const DabRenderingResources &rhs) = delete; +}; + +typedef std::function ResourcesFactory; + +struct PAINTOP_EXPORT DabRequestInfo +{ + DabRequestInfo(const KoColor &_color, + const QPointF &_cursorPoint, + const KisDabShape &_shape, + const KisPaintInformation &_info, + qreal _softnessFactor) + : color(_color), + cursorPoint(_cursorPoint), + shape(_shape), + info(_info), + softnessFactor(_softnessFactor) + { + } + + const KoColor &color; + const QPointF &cursorPoint; + const KisDabShape &shape; + const KisPaintInformation &info; + const qreal softnessFactor; + +private: + DabRequestInfo(const DabRequestInfo &rhs); +}; + +struct PAINTOP_EXPORT DabGenerationInfo +{ + MirrorProperties mirrorProperties; + KisDabShape shape; + QRect dstDabRect; + QPointF subPixel; + bool solidColorFill = true; + KoColor paintColor; + KisPaintInformation info; + qreal softnessFactor = 1.0; + + bool needsPostprocessing = false; +}; + +PAINTOP_EXPORT QRect correctDabRectWhenFetchedFromCache(const QRect &dabRect, + const QSize &realDabSize); + +PAINTOP_EXPORT void generateDab(const DabGenerationInfo &di, + DabRenderingResources *resources, + KisFixedPaintDeviceSP *dab); + +PAINTOP_EXPORT void postProcessDab(KisFixedPaintDeviceSP dab, + const QPoint &dabTopLeft, + const KisPaintInformation& info, + DabRenderingResources *resources); + +} + +template class QSharedPointer; +class KisDabRenderingJob; +typedef QSharedPointer KisDabRenderingJobSP; + +#endif // KISDABCACHEUTILS_H diff --git a/plugins/paintops/libpaintop/KisDabCacheUtils.cpp b/plugins/paintops/libpaintop/KisDabCacheUtils.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/libpaintop/KisDabCacheUtils.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2017 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 "KisDabCacheUtils.h" + +#include "kis_brush.h" +#include "kis_paint_device.h" +#include "kis_fixed_paint_device.h" +#include "kis_color_source.h" + +#include +#include + +#include + +namespace KisDabCacheUtils +{ + +DabRenderingResources::DabRenderingResources() +{ +} + +DabRenderingResources::~DabRenderingResources() +{ +} + +void DabRenderingResources::syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info) +{ + brush->prepareForSeqNo(info, seqNo); +} + +QRect correctDabRectWhenFetchedFromCache(const QRect &dabRect, + const QSize &realDabSize) +{ + int diffX = (realDabSize.width() - dabRect.width()) / 2; + int diffY = (realDabSize.height() - dabRect.height()) / 2; + + return QRect(dabRect.x() - diffX, dabRect.y() - diffY, + realDabSize.width() , realDabSize.height()); +} + + +void generateDab(const DabGenerationInfo &di, DabRenderingResources *resources, KisFixedPaintDeviceSP *dab) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(*dab); + const KoColorSpace *cs = (*dab)->colorSpace(); + + + if (resources->brush->brushType() == IMAGE || resources->brush->brushType() == PIPE_IMAGE) { + *dab = resources->brush->paintDevice(cs, di.shape, di.info, + di.subPixel.x(), + di.subPixel.y()); + } else if (di.solidColorFill) { + resources->brush->mask(*dab, + di.paintColor, + di.shape, + di.info, + di.subPixel.x(), di.subPixel.y(), + di.softnessFactor); + } + else { + if (!resources->colorSourceDevice || + *cs != *resources->colorSourceDevice->colorSpace()) { + + resources->colorSourceDevice = new KisPaintDevice(cs); + } + else { + resources->colorSourceDevice->clear(); + } + + QRect maskRect(QPoint(), di.dstDabRect.size()); + resources->colorSource->colorize(resources->colorSourceDevice, maskRect, di.info.pos().toPoint()); + delete resources->colorSourceDevice->convertTo(cs); + + resources->brush->mask(*dab, resources->colorSourceDevice, + di.shape, + di.info, + di.subPixel.x(), di.subPixel.y(), + di.softnessFactor); + } + + if (!di.mirrorProperties.isEmpty()) { + (*dab)->mirror(di.mirrorProperties.horizontalMirror, + di.mirrorProperties.verticalMirror); + } +} + +void postProcessDab(KisFixedPaintDeviceSP dab, + const QPoint &dabTopLeft, + const KisPaintInformation& info, + DabRenderingResources *resources) +{ + if (resources->sharpnessOption) { + resources->sharpnessOption->applyThreshold(dab); + } + + if (resources->textureOption) { + resources->textureOption->apply(dab, dabTopLeft, info); + } +} + +} + diff --git a/plugins/paintops/libpaintop/kis_airbrush_option.cpp b/plugins/paintops/libpaintop/kis_airbrush_option.cpp --- a/plugins/paintops/libpaintop/kis_airbrush_option.cpp +++ b/plugins/paintops/libpaintop/kis_airbrush_option.cpp @@ -54,7 +54,7 @@ // We store the airbrush interval (in milliseconds) instead of the rate because the interval is // likely to be accessed more often. qreal airbrushInterval; - KisAirbrushWidget *configPage; + QScopedPointer configPage; }; KisAirbrushOption::KisAirbrushOption(bool enabled, bool canIgnoreSpacing) @@ -65,11 +65,11 @@ // Initialize GUI. m_checkable = true; - m_d->configPage = new KisAirbrushWidget(nullptr, canIgnoreSpacing); + m_d->configPage.reset(new KisAirbrushWidget(nullptr, canIgnoreSpacing)); connect(m_d->configPage->sliderRate, SIGNAL(valueChanged(qreal)), SLOT(slotIntervalChanged())); connect(m_d->configPage->checkBoxIgnoreSpacing, SIGNAL(toggled(bool)), SLOT(slotIgnoreSpacingChanged())); - setConfigurationPage(m_d->configPage); + setConfigurationPage(m_d->configPage.data()); // Read initial configuration from the GUI. updateIgnoreSpacing(); diff --git a/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp b/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp --- a/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp +++ b/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp @@ -88,7 +88,7 @@ KisFixedPaintDevice canvas(device->colorSpace()); canvas.setRect(QRect(dstRect.x(), dstRect.y(), sw, sh)); - canvas.initialize(); + canvas.lazyGrowBufferWithoutInitialization(); device->readBytes(canvas.data(), canvas.bounds()); const KoColorSpace* cs = dab->colorSpace(); diff --git a/plugins/paintops/libpaintop/kis_brush_based_paintop.h b/plugins/paintops/libpaintop/kis_brush_based_paintop.h --- a/plugins/paintops/libpaintop/kis_brush_based_paintop.h +++ b/plugins/paintops/libpaintop/kis_brush_based_paintop.h @@ -94,6 +94,8 @@ private: KisTextureProperties m_textureProperties; + +protected: KisPressureMirrorOption m_mirrorOption; KisPrecisionOption m_precisionOption; }; diff --git a/plugins/paintops/libpaintop/kis_dab_cache.h b/plugins/paintops/libpaintop/kis_dab_cache.h --- a/plugins/paintops/libpaintop/kis_dab_cache.h +++ b/plugins/paintops/libpaintop/kis_dab_cache.h @@ -20,6 +20,9 @@ #define __KIS_DAB_CACHE_H #include "kritapaintop_export.h" + +#include "kis_dab_cache_base.h" + #include "kis_brush.h" class KisColorSource; @@ -49,29 +52,14 @@ * * The texturing and mirroring problems are solved. */ -class PAINTOP_EXPORT KisDabCache +class PAINTOP_EXPORT KisDabCache : public KisDabCacheBase { public: KisDabCache(KisBrushSP brush); ~KisDabCache(); - void setMirrorPostprocessing(KisPressureMirrorOption *option); - void setSharpnessPostprocessing(KisPressureSharpnessOption *option); - void setTexturePostprocessing(KisTextureProperties *option); - void setPrecisionOption(KisPrecisionOption *option); - - /** - * Disables handling of the subPixelX and subPixelY values, this - * is needed at least for the Color Smudge paint op, which reads - * aligned areas from image, so additional offsets generated by - * the subpixel precision should be avoided - */ - void disableSubpixelPrecision(); - - bool needSeparateOriginal(); - KisFixedPaintDeviceSP fetchDab(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const QPointF &cursorPoint, KisDabShape const&, const KisPaintInformation& info, @@ -86,44 +74,25 @@ qreal softnessFactor, QRect *dstDabRect); + void setSharpnessPostprocessing(KisPressureSharpnessOption *option); + void setTexturePostprocessing(KisTextureProperties *option); + + bool needSeparateOriginal() const; private: - struct SavedDabParameters; - struct DabPosition; -private: - inline SavedDabParameters getDabParameters(const KoColor& color, - KisDabShape const&, - const KisPaintInformation& info, - double subPixelX, double subPixelY, - qreal softnessFactor, - MirrorProperties mirrorProperties); - inline KisDabCache::DabPosition - calculateDabRect(const QPointF &cursorPoint, - KisDabShape, - const KisPaintInformation& info, - const MirrorProperties &mirrorProperties); - - inline - QRect correctDabRectWhenFetchedFromCache(const QRect &dabRect, - const QSize &realDabSize); - - inline KisFixedPaintDeviceSP tryFetchFromCache(const SavedDabParameters ¶ms, - const KisPaintInformation& info, - QRect *dstDabRect); + + inline KisFixedPaintDeviceSP fetchFromCache(KisDabCacheUtils::DabRenderingResources *resources, const KisPaintInformation& info, + QRect *dstDabRect); inline KisFixedPaintDeviceSP fetchDabCommon(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const KoColor& color, const QPointF &cursorPoint, KisDabShape, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect); - void postProcessDab(KisFixedPaintDeviceSP dab, - const QPoint &dabTopLeft, - const KisPaintInformation& info); - private: struct Private; diff --git a/plugins/paintops/libpaintop/kis_dab_cache.cpp b/plugins/paintops/libpaintop/kis_dab_cache.cpp --- a/plugins/paintops/libpaintop/kis_dab_cache.cpp +++ b/plugins/paintops/libpaintop/kis_dab_cache.cpp @@ -19,85 +19,29 @@ #include "kis_dab_cache.h" #include -#include "kis_color_source.h" #include "kis_paint_device.h" #include "kis_brush.h" -#include -#include -#include -#include #include -#include +#include "kis_color_source.h" +#include "kis_pressure_sharpness_option.h" +#include "kis_texture_option.h" #include -struct PrecisionValues { - qreal angle; - qreal sizeFrac; - qreal subPixel; - qreal softnessFactor; -}; - -const qreal eps = 1e-6; -static const PrecisionValues precisionLevels[] = { - {M_PI / 180, 0.05, 1, 0.01}, - {M_PI / 180, 0.01, 1, 0.01}, - {M_PI / 180, 0, 1, 0.01}, - {M_PI / 180, 0, 0.5, 0.01}, - {eps, 0, eps, eps} -}; - -struct KisDabCache::SavedDabParameters { - KoColor color; - qreal angle; - int width; - int height; - qreal subPixelX; - qreal subPixelY; - qreal softnessFactor; - int index; - MirrorProperties mirrorProperties; - - bool compare(const SavedDabParameters &rhs, int precisionLevel) const { - const PrecisionValues &prec = precisionLevels[precisionLevel]; - - return color == rhs.color && - qAbs(angle - rhs.angle) <= prec.angle && - qAbs(width - rhs.width) <= (int)(prec.sizeFrac * width) && - qAbs(height - rhs.height) <= (int)(prec.sizeFrac * height) && - qAbs(subPixelX - rhs.subPixelX) <= prec.subPixel && - qAbs(subPixelY - rhs.subPixelY) <= prec.subPixel && - qAbs(softnessFactor - rhs.softnessFactor) <= prec.softnessFactor && - index == rhs.index && - mirrorProperties.horizontalMirror == rhs.mirrorProperties.horizontalMirror && - mirrorProperties.verticalMirror == rhs.mirrorProperties.verticalMirror; - } -}; - struct KisDabCache::Private { Private(KisBrushSP brush) - : brush(brush), - mirrorOption(0), - sharpnessOption(0), - textureOption(0), - precisionOption(0), - subPixelPrecisionDisabled(false), - cachedDabParameters(new SavedDabParameters) + : brush(brush) {} + KisFixedPaintDeviceSP dab; KisFixedPaintDeviceSP dabOriginal; KisBrushSP brush; KisPaintDeviceSP colorSourceDevice; - KisPressureMirrorOption *mirrorOption; - KisPressureSharpnessOption *sharpnessOption; - KisTextureProperties *textureOption; - KisPrecisionOption *precisionOption; - bool subPixelPrecisionDisabled; - - SavedDabParameters *cachedDabParameters; + KisPressureSharpnessOption *sharpnessOption = 0; + KisTextureProperties *textureOption = 0; }; @@ -109,15 +53,9 @@ KisDabCache::~KisDabCache() { - delete m_d->cachedDabParameters; delete m_d; } -void KisDabCache::setMirrorPostprocessing(KisPressureMirrorOption *option) -{ - m_d->mirrorOption = option; -} - void KisDabCache::setSharpnessPostprocessing(KisPressureSharpnessOption *option) { m_d->sharpnessOption = option; @@ -128,41 +66,14 @@ m_d->textureOption = option; } -void KisDabCache::setPrecisionOption(KisPrecisionOption *option) +bool KisDabCache::needSeparateOriginal() const { - m_d->precisionOption = option; + return KisDabCacheBase::needSeparateOriginal(m_d->textureOption, m_d->sharpnessOption); } -void KisDabCache::disableSubpixelPrecision() -{ - m_d->subPixelPrecisionDisabled = true; -} - -inline KisDabCache::SavedDabParameters -KisDabCache::getDabParameters(const KoColor& color, - KisDabShape const& shape, - const KisPaintInformation& info, - double subPixelX, double subPixelY, - qreal softnessFactor, - MirrorProperties mirrorProperties) -{ - SavedDabParameters params; - - params.color = color; - params.angle = shape.rotation(); - params.width = m_d->brush->maskWidth(shape, subPixelX, subPixelY, info); - params.height = m_d->brush->maskHeight(shape, subPixelX, subPixelY, info); - params.subPixelX = subPixelX; - params.subPixelY = subPixelY; - params.softnessFactor = softnessFactor; - params.index = m_d->brush->brushIndex(info); - params.mirrorProperties = mirrorProperties; - - return params; -} KisFixedPaintDeviceSP KisDabCache::fetchDab(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const QPointF &cursorPoint, KisDabShape const& shape, const KisPaintInformation& info, @@ -193,130 +104,42 @@ dstDabRect); } -bool KisDabCache::needSeparateOriginal() -{ - return (m_d->textureOption && m_d->textureOption->m_enabled) || - (m_d->sharpnessOption && m_d->sharpnessOption->isChecked()); -} - -struct KisDabCache::DabPosition { - DabPosition(const QRect &_rect, - const QPointF &_subPixel, - qreal _realAngle) - : rect(_rect), - subPixel(_subPixel), - realAngle(_realAngle) { - } - - QRect rect; - QPointF subPixel; - qreal realAngle; -}; - inline -QRect KisDabCache::correctDabRectWhenFetchedFromCache(const QRect &dabRect, - const QSize &realDabSize) -{ - int diffX = (realDabSize.width() - dabRect.width()) / 2; - int diffY = (realDabSize.height() - dabRect.height()) / 2; - - return QRect(dabRect.x() - diffX, dabRect.y() - diffY, - realDabSize.width() , realDabSize.height()); -} - -inline -KisFixedPaintDeviceSP KisDabCache::tryFetchFromCache(const SavedDabParameters ¶ms, - const KisPaintInformation& info, - QRect *dstDabRect) +KisFixedPaintDeviceSP KisDabCache::fetchFromCache(KisDabCacheUtils::DabRenderingResources *resources, + const KisPaintInformation& info, + QRect *dstDabRect) { - int precisionLevel = m_d->precisionOption ? m_d->precisionOption->precisionLevel() - 1 : 3; - - if (!params.compare(*m_d->cachedDabParameters, precisionLevel)) { - return 0; - } - if (needSeparateOriginal()) { *m_d->dab = *m_d->dabOriginal; - *dstDabRect = correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); - postProcessDab(m_d->dab, dstDabRect->topLeft(), info); + *dstDabRect = KisDabCacheUtils::correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); + KisDabCacheUtils::postProcessDab(m_d->dab, dstDabRect->topLeft(), info, resources); } else { - *dstDabRect = correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); + *dstDabRect = KisDabCacheUtils::correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); } - m_d->brush->notifyCachedDabPainted(info); + resources->brush->notifyCachedDabPainted(info); return m_d->dab; } -qreal positiveFraction(qreal x) { - qint32 unused = 0; - qreal fraction = 0.0; - KisPaintOp::splitCoordinate(x, &unused, &fraction); - - return fraction; -} - -inline -KisDabCache::DabPosition -KisDabCache::calculateDabRect(const QPointF &cursorPoint, - KisDabShape shape, - const KisPaintInformation& info, - const MirrorProperties &mirrorProperties) +/** + * A special hack class that allows creation of temporary object with resources + * without taking ownershop over the option classes + */ +struct TemporaryResourcesWithoutOwning : public KisDabCacheUtils::DabRenderingResources { - qint32 x = 0, y = 0; - qreal subPixelX = 0.0, subPixelY = 0.0; - - if (mirrorProperties.coordinateSystemFlipped) { - shape = KisDabShape(shape.scale(), shape.ratio(), 2 * M_PI - shape.rotation()); + ~TemporaryResourcesWithoutOwning() override { + // we do not own these resources, so just + // release them before destruction + colorSource.take(); + sharpnessOption.take(); + textureOption.take(); } - - QPointF hotSpot = m_d->brush->hotSpot(shape, info); - QPointF pt = cursorPoint - hotSpot; - - if (m_d->sharpnessOption) { - m_d->sharpnessOption->apply(info, pt, x, y, subPixelX, subPixelY); - } - else { - KisPaintOp::splitCoordinate(pt.x(), &x, &subPixelX); - KisPaintOp::splitCoordinate(pt.y(), &y, &subPixelY); - } - - if (m_d->subPixelPrecisionDisabled) { - subPixelX = 0; - subPixelY = 0; - } - - if (qIsNaN(subPixelX)) { - subPixelX = 0; - } - - if (qIsNaN(subPixelY)) { - subPixelY = 0; - } - - int width = m_d->brush->maskWidth(shape, subPixelX, subPixelY, info); - int height = m_d->brush->maskHeight(shape, subPixelX, subPixelY, info); - - if (mirrorProperties.horizontalMirror) { - subPixelX = positiveFraction(-(cursorPoint.x() + hotSpot.x())); - width = m_d->brush->maskWidth(shape, subPixelX, subPixelY, info); - x = qRound(cursorPoint.x() + subPixelX + hotSpot.x()) - width; - } - - if (mirrorProperties.verticalMirror) { - subPixelY = positiveFraction(-(cursorPoint.y() + hotSpot.y())); - height = m_d->brush->maskHeight(shape, subPixelX, subPixelY, info); - y = qRound(cursorPoint.y() + subPixelY + hotSpot.y()) - height; - } - - return DabPosition(QRect(x, y, width, height), - QPointF(subPixelX, subPixelY), - shape.rotation()); -} +}; inline KisFixedPaintDeviceSP KisDabCache::fetchDabCommon(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const KoColor& color, const QPointF &cursorPoint, KisDabShape shape, @@ -326,96 +149,65 @@ { Q_ASSERT(dstDabRect); - MirrorProperties mirrorProperties; - if (m_d->mirrorOption) { - mirrorProperties = m_d->mirrorOption->apply(info); + bool hasDabInCache = true; + + if (!m_d->dab || *m_d->dab->colorSpace() != *cs) { + m_d->dab = new KisFixedPaintDevice(cs); + hasDabInCache = false; } - DabPosition position = calculateDabRect(cursorPoint, - shape, - info, - mirrorProperties); - shape = KisDabShape(shape.scale(), shape.ratio(), position.realAngle); - *dstDabRect = position.rect; + using namespace KisDabCacheUtils; - bool cachingIsPossible = !colorSource || colorSource->isUniformColor(); - KoColor paintColor = colorSource && colorSource->isUniformColor() ? - colorSource->uniformColor() : color; + // 1. Calculate new dab parameters and whether we can reuse the cache - SavedDabParameters newParams = getDabParameters(paintColor, - shape, info, - position.subPixel.x(), - position.subPixel.y(), - softnessFactor, - mirrorProperties); + TemporaryResourcesWithoutOwning resources; + resources.brush = m_d->brush; + resources.colorSourceDevice = m_d->colorSourceDevice; - if (!m_d->dab || *m_d->dab->colorSpace() != *cs) { - m_d->dab = new KisFixedPaintDevice(cs); - } - else if (cachingIsPossible) { - KisFixedPaintDeviceSP cachedDab = - tryFetchFromCache(newParams, info, dstDabRect); + // NOTE: we use a special subclass of resources that will NOT + // delete options on destruction! + resources.colorSource.reset(colorSource); + resources.sharpnessOption.reset(m_d->sharpnessOption); + resources.textureOption.reset(m_d->textureOption); - if (cachedDab) return cachedDab; - } - if (m_d->brush->brushType() == IMAGE || m_d->brush->brushType() == PIPE_IMAGE) { - m_d->dab = m_d->brush->paintDevice(cs, shape, info, - position.subPixel.x(), - position.subPixel.y()); - } - else if (cachingIsPossible) { - *m_d->cachedDabParameters = newParams; - m_d->brush->mask(m_d->dab, paintColor, shape, - info, - position.subPixel.x(), position.subPixel.y(), - softnessFactor); - } - else { - if (!m_d->colorSourceDevice || *cs != *m_d->colorSourceDevice->colorSpace()) { - m_d->colorSourceDevice = new KisPaintDevice(cs); - } - else { - m_d->colorSourceDevice->clear(); - } + DabGenerationInfo di; + bool shouldUseCache = false; - QRect maskRect(QPoint(), position.rect.size()); - colorSource->colorize(m_d->colorSourceDevice, maskRect, info.pos().toPoint()); - delete m_d->colorSourceDevice->convertTo(cs); + fetchDabGenerationInfo(hasDabInCache, + &resources, + DabRequestInfo( + color, + cursorPoint, + shape, + info, + softnessFactor), + &di, + &shouldUseCache); + + *dstDabRect = di.dstDabRect; - m_d->brush->mask(m_d->dab, m_d->colorSourceDevice, shape, - info, - position.subPixel.x(), position.subPixel.y(), - softnessFactor); - } - if (!mirrorProperties.isEmpty()) { - m_d->dab->mirror(mirrorProperties.horizontalMirror, - mirrorProperties.verticalMirror); + // 2. Try return a saved dab from the cache + + if (shouldUseCache) { + return fetchFromCache(&resources, info, dstDabRect); } - if (needSeparateOriginal()) { + // 3. Generate new dab + + generateDab(di, &resources, &m_d->dab); + + // 4. Do postprocessing + if (di.needsPostprocessing) { if (!m_d->dabOriginal || *cs != *m_d->dabOriginal->colorSpace()) { m_d->dabOriginal = new KisFixedPaintDevice(cs); } *m_d->dabOriginal = *m_d->dab; - } - - postProcessDab(m_d->dab, position.rect.topLeft(), info); - - return m_d->dab; -} -void KisDabCache::postProcessDab(KisFixedPaintDeviceSP dab, - const QPoint &dabTopLeft, - const KisPaintInformation& info) -{ - if (m_d->sharpnessOption) { - m_d->sharpnessOption->applyThreshold(dab); + postProcessDab(m_d->dab, di.dstDabRect.topLeft(), info, &resources); } - if (m_d->textureOption) { - m_d->textureOption->apply(dab, dabTopLeft, info); - } + return m_d->dab; } diff --git a/plugins/paintops/libpaintop/kis_dab_cache_base.h b/plugins/paintops/libpaintop/kis_dab_cache_base.h new file mode 100644 --- /dev/null +++ b/plugins/paintops/libpaintop/kis_dab_cache_base.h @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012 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_DAB_CACHE_BASE_H +#define __KIS_DAB_CACHE_BASE_H + +#include "kritapaintop_export.h" +#include "kis_brush.h" + +#include "KisDabCacheUtils.h" + +class KisColorSource; +class KisPressureSharpnessOption; +class KisTextureProperties; +class KisPressureMirrorOption; +class KisPrecisionOption; +struct MirrorProperties; + + +/** + * @brief The KisDabCacheBase class provides caching for dabs into the brush paintop + * + * This class adds caching of the dabs to the paintop system of Krita. + * Such cache makes the execution of the benchmarks up to 2 times faster. + * Subjectively, the real painting becomes much faster, especially with + * huge brushes. Artists report up to 20% speed gain while painting. + * + * Of course, such caching makes the painting a bit less precise: we need + * to tolerate subpixel differences to allow the cache to work. Sometimes + * small difference in the size of a dab can also be acceptable. That is + * why I introduced levels of precision. They are graded from 1 to 5: from + * the fastest and less precise to the slowest, but with the best quality. + * You can see the slider in the paintop settings dialog. The ToolTip text + * explains which features of the brush are sacrificed on each precision + * level. + * + * The texturing and mirroring problems are solved. + */ +class PAINTOP_EXPORT KisDabCacheBase +{ +public: + KisDabCacheBase(); + ~KisDabCacheBase(); + + void setMirrorPostprocessing(KisPressureMirrorOption *option); + void setPrecisionOption(KisPrecisionOption *option); + + /** + * Disables handling of the subPixelX and subPixelY values, this + * is needed at least for the Color Smudge paint op, which reads + * aligned areas from image, so additional offsets generated by + * the subpixel precision should be avoided + */ + void disableSubpixelPrecision(); + + /** + * Return true if the dab needs postprocesing by special options + * like 'texture' or 'sharpness' + */ + bool needSeparateOriginal(KisTextureProperties *textureOption, + KisPressureSharpnessOption *sharpnessOption) const; + +protected: + /** + * Fetches all the necessary information for dab generation and + * tells if the caller must should reuse the preciously returned dab. * + * Please note that KisDabCacheBase has an internal state, that keeps the + * parameters of the previously generated (on a cache-miss) dab. This function + * automatically updates this state when 'shouldUseCache == false'. Therefore, the + * caller *must* generate the dab if and only if when 'shouldUseCache == false'. + * Othewise the internal state will become inconsistent. + * + * @param hasDabInCache shows if the caller has something in its cache + * @param resources rendering resources available for this dab + * @param color current painting color + * @param cursorPoint cursor point at which the dab should be painted + * @param shape dab shape requested by the caller. It will be modified before + * generation to accomodate the mirroring and rotation options. + * @param info painting info associated with the dab + * @param softnessFactor softness factor + * @param di (OUT) calculated dab generation information + * @param shouldUseCache (OUT) shows whether the caller *must* use cache or not + */ + void fetchDabGenerationInfo(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache); + +private: + struct SavedDabParameters; + struct DabPosition; +private: + inline SavedDabParameters getDabParameters(KisBrushSP brush, const KoColor& color, + KisDabShape const&, + const KisPaintInformation& info, + double subPixelX, double subPixelY, + qreal softnessFactor, + MirrorProperties mirrorProperties); + + inline KisDabCacheBase::DabPosition + calculateDabRect(KisBrushSP brush, const QPointF &cursorPoint, + KisDabShape, + const KisPaintInformation& info, + const MirrorProperties &mirrorProperties, KisPressureSharpnessOption *sharpnessOption); + +private: + struct Private; + Private * const m_d; +}; + +#endif /* __KIS_DAB_CACHE_BASE_H */ diff --git a/plugins/paintops/libpaintop/kis_dab_cache_base.cpp b/plugins/paintops/libpaintop/kis_dab_cache_base.cpp new file mode 100644 --- /dev/null +++ b/plugins/paintops/libpaintop/kis_dab_cache_base.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2012 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_dab_cache_base.h" + +#include +#include "kis_color_source.h" +#include "kis_paint_device.h" +#include "kis_brush.h" +#include +#include +#include +#include +#include +#include + +#include + +struct PrecisionValues { + qreal angle; + qreal sizeFrac; + qreal subPixel; + qreal softnessFactor; +}; + +const qreal eps = 1e-6; +static const PrecisionValues precisionLevels[] = { + {M_PI / 180, 0.05, 1, 0.01}, + {M_PI / 180, 0.01, 1, 0.01}, + {M_PI / 180, 0, 1, 0.01}, + {M_PI / 180, 0, 0.5, 0.01}, + {eps, 0, eps, eps} +}; + +struct KisDabCacheBase::SavedDabParameters { + KoColor color; + qreal angle; + int width; + int height; + qreal subPixelX; + qreal subPixelY; + qreal softnessFactor; + int index; + MirrorProperties mirrorProperties; + + bool compare(const SavedDabParameters &rhs, int precisionLevel) const { + const PrecisionValues &prec = precisionLevels[precisionLevel]; + + return color == rhs.color && + qAbs(angle - rhs.angle) <= prec.angle && + qAbs(width - rhs.width) <= (int)(prec.sizeFrac * width) && + qAbs(height - rhs.height) <= (int)(prec.sizeFrac * height) && + qAbs(subPixelX - rhs.subPixelX) <= prec.subPixel && + qAbs(subPixelY - rhs.subPixelY) <= prec.subPixel && + qAbs(softnessFactor - rhs.softnessFactor) <= prec.softnessFactor && + index == rhs.index && + mirrorProperties.horizontalMirror == rhs.mirrorProperties.horizontalMirror && + mirrorProperties.verticalMirror == rhs.mirrorProperties.verticalMirror; + } +}; + +struct KisDabCacheBase::Private { + + Private() + : mirrorOption(0), + precisionOption(0), + subPixelPrecisionDisabled(false) + {} + + KisPressureMirrorOption *mirrorOption; + KisPrecisionOption *precisionOption; + bool subPixelPrecisionDisabled; + + SavedDabParameters lastSavedDabParameters; + + static qreal positiveFraction(qreal x); +}; + + + +KisDabCacheBase::KisDabCacheBase() + : m_d(new Private()) +{ +} + +KisDabCacheBase::~KisDabCacheBase() +{ + delete m_d; +} + +void KisDabCacheBase::setMirrorPostprocessing(KisPressureMirrorOption *option) +{ + m_d->mirrorOption = option; +} + +void KisDabCacheBase::setPrecisionOption(KisPrecisionOption *option) +{ + m_d->precisionOption = option; +} + +void KisDabCacheBase::disableSubpixelPrecision() +{ + m_d->subPixelPrecisionDisabled = true; +} + +inline KisDabCacheBase::SavedDabParameters +KisDabCacheBase::getDabParameters(KisBrushSP brush, + const KoColor& color, + KisDabShape const& shape, + const KisPaintInformation& info, + double subPixelX, double subPixelY, + qreal softnessFactor, + MirrorProperties mirrorProperties) +{ + SavedDabParameters params; + + params.color = color; + params.angle = shape.rotation(); + params.width = brush->maskWidth(shape, subPixelX, subPixelY, info); + params.height = brush->maskHeight(shape, subPixelX, subPixelY, info); + params.subPixelX = subPixelX; + params.subPixelY = subPixelY; + params.softnessFactor = softnessFactor; + params.index = brush->brushIndex(info); + params.mirrorProperties = mirrorProperties; + + return params; +} + +bool KisDabCacheBase::needSeparateOriginal(KisTextureProperties *textureOption, + KisPressureSharpnessOption *sharpnessOption) const +{ + return (textureOption && textureOption->m_enabled) || + (sharpnessOption && sharpnessOption->isChecked()); +} + +struct KisDabCacheBase::DabPosition { + DabPosition(const QRect &_rect, + const QPointF &_subPixel, + qreal _realAngle) + : rect(_rect), + subPixel(_subPixel), + realAngle(_realAngle) { + } + + QRect rect; + QPointF subPixel; + qreal realAngle; +}; + +qreal KisDabCacheBase::Private::positiveFraction(qreal x) { + qint32 unused = 0; + qreal fraction = 0.0; + KisPaintOp::splitCoordinate(x, &unused, &fraction); + + return fraction; +} + +inline +KisDabCacheBase::DabPosition +KisDabCacheBase::calculateDabRect(KisBrushSP brush, + const QPointF &cursorPoint, + KisDabShape shape, + const KisPaintInformation& info, + const MirrorProperties &mirrorProperties, + KisPressureSharpnessOption *sharpnessOption) +{ + qint32 x = 0, y = 0; + qreal subPixelX = 0.0, subPixelY = 0.0; + + if (mirrorProperties.coordinateSystemFlipped) { + shape = KisDabShape(shape.scale(), shape.ratio(), 2 * M_PI - shape.rotation()); + } + + QPointF hotSpot = brush->hotSpot(shape, info); + QPointF pt = cursorPoint - hotSpot; + + if (sharpnessOption) { + sharpnessOption->apply(info, pt, x, y, subPixelX, subPixelY); + } + else { + KisPaintOp::splitCoordinate(pt.x(), &x, &subPixelX); + KisPaintOp::splitCoordinate(pt.y(), &y, &subPixelY); + } + + if (m_d->subPixelPrecisionDisabled) { + subPixelX = 0; + subPixelY = 0; + } + + if (qIsNaN(subPixelX)) { + subPixelX = 0; + } + + if (qIsNaN(subPixelY)) { + subPixelY = 0; + } + + int width = brush->maskWidth(shape, subPixelX, subPixelY, info); + int height = brush->maskHeight(shape, subPixelX, subPixelY, info); + + if (mirrorProperties.horizontalMirror) { + subPixelX = Private::positiveFraction(-(cursorPoint.x() + hotSpot.x())); + width = brush->maskWidth(shape, subPixelX, subPixelY, info); + x = qRound(cursorPoint.x() + subPixelX + hotSpot.x()) - width; + } + + if (mirrorProperties.verticalMirror) { + subPixelY = Private::positiveFraction(-(cursorPoint.y() + hotSpot.y())); + height = brush->maskHeight(shape, subPixelX, subPixelY, info); + y = qRound(cursorPoint.y() + subPixelY + hotSpot.y()) - height; + } + + return DabPosition(QRect(x, y, width, height), + QPointF(subPixelX, subPixelY), + shape.rotation()); +} + +void KisDabCacheBase::fetchDabGenerationInfo(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) +{ + di->info = request.info; + di->softnessFactor = request.softnessFactor; + + if (m_d->mirrorOption) { + di->mirrorProperties = m_d->mirrorOption->apply(request.info); + } + + DabPosition position = calculateDabRect(resources->brush, + request.cursorPoint, + request.shape, + request.info, + di->mirrorProperties, + resources->sharpnessOption.data()); + di->shape = KisDabShape(request.shape.scale(), request.shape.ratio(), position.realAngle); + di->dstDabRect = position.rect; + di->subPixel = position.subPixel; + + di->solidColorFill = !resources->colorSource || resources->colorSource->isUniformColor(); + di->paintColor = resources->colorSource && resources->colorSource->isUniformColor() ? + resources->colorSource->uniformColor() : request.color; + + SavedDabParameters newParams = getDabParameters(resources->brush, + di->paintColor, + di->shape, + di->info, + di->subPixel.x(), + di->subPixel.y(), + di->softnessFactor, + di->mirrorProperties); + + const int precisionLevel = m_d->precisionOption ? m_d->precisionOption->precisionLevel() - 1 : 3; + *shouldUseCache = hasDabInCache && di->solidColorFill && + newParams.compare(m_d->lastSavedDabParameters, precisionLevel); + + if (!*shouldUseCache) { + m_d->lastSavedDabParameters = newParams; + } + + di->needsPostprocessing = needSeparateOriginal(resources->textureOption.data(), resources->sharpnessOption.data()); +} + diff --git a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h --- a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h +++ b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h @@ -40,6 +40,7 @@ void setFlow(qreal flow); void setOpacity(qreal opacity); void apply(KisPainter* painter, const KisPaintInformation& info); + void apply(const KisPaintInformation& info, quint8 *opacity, quint8 *flow); qreal getFlow() const; qreal getStaticOpacity() const; diff --git a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp --- a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp +++ b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp @@ -82,10 +82,22 @@ void KisFlowOpacityOption::apply(KisPainter* painter, const KisPaintInformation& info) { - if (m_paintActionType == WASH && m_nodeHasIndirectPaintingSupport) - painter->setOpacityUpdateAverage(quint8(getDynamicOpacity(info) * 255.0)); - else - painter->setOpacityUpdateAverage(quint8(getStaticOpacity() * getDynamicOpacity(info) * 255.0)); + quint8 opacity = OPACITY_OPAQUE_U8; + quint8 flow = OPACITY_OPAQUE_U8; - painter->setFlow(quint8(getFlow() * 255.0)); + apply(info, &opacity, &flow); + + painter->setOpacityUpdateAverage(opacity); + painter->setFlow(flow); +} + +void KisFlowOpacityOption::apply(const KisPaintInformation &info, quint8 *opacity, quint8 *flow) +{ + if (m_paintActionType == WASH && m_nodeHasIndirectPaintingSupport) { + *opacity = quint8(getDynamicOpacity(info) * 255.0); + } else { + *opacity = quint8(getStaticOpacity() * getDynamicOpacity(info) * 255.0); + } + + *flow = quint8(getFlow() * 255.0); } diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h @@ -59,7 +59,6 @@ bool m_fanCornersEnabled; int m_fanCornersStep; int m_angleOffset; // in degrees - int m_dabIndex; qreal m_lockedAngle; bool m_lockedAngleMode; diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp @@ -32,35 +32,24 @@ m_fanCornersEnabled(false), m_fanCornersStep(30), m_angleOffset(0), - m_dabIndex(0), m_lockedAngle(0), m_lockedAngleMode(false) { } void KisDynamicSensorDrawingAngle::reset() { - m_dabIndex = 0; } qreal KisDynamicSensorDrawingAngle::value(const KisPaintInformation& info) { /* so that we are in 0.0..1.0 */ - qreal ret = 0.5 + info.drawingAngle() / (2.0 * M_PI) + m_angleOffset/360.0; + qreal ret = 0.5 + info.drawingAngle(m_lockedAngleMode) / (2.0 * M_PI) + m_angleOffset/360.0; // check if m_angleOffset pushed us out of bounds if (ret > 1.0) ret -= 1.0; - if (!info.isHoveringMode() && m_lockedAngleMode) { - if (!m_dabIndex) { - info.lockCurrentDrawingAngle(1.0); - } else { - info.lockCurrentDrawingAngle(0.5); - } - m_dabIndex++; - } - return ret; } diff --git a/plugins/tools/basictools/strokes/move_stroke_strategy.cpp b/plugins/tools/basictools/strokes/move_stroke_strategy.cpp --- a/plugins/tools/basictools/strokes/move_stroke_strategy.cpp +++ b/plugins/tools/basictools/strokes/move_stroke_strategy.cpp @@ -218,6 +218,8 @@ KisStrokeStrategy* MoveStrokeStrategy::createLodClone(int levelOfDetail) { + Q_UNUSED(levelOfDetail); + Q_FOREACH (KisNodeSP node, m_nodes) { if (!checkSupportsLodMoves(node)) return 0; } diff --git a/plugins/tools/selectiontools/kis_tool_select_elliptical.cc b/plugins/tools/selectiontools/kis_tool_select_elliptical.cc --- a/plugins/tools/selectiontools/kis_tool_select_elliptical.cc +++ b/plugins/tools/selectiontools/kis_tool_select_elliptical.cc @@ -57,7 +57,6 @@ KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); - painter.setPaintOpPreset(currentPaintOpPreset(), currentNode(), currentImage()); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); diff --git a/plugins/tools/selectiontools/kis_tool_select_outline.cc b/plugins/tools/selectiontools/kis_tool_select_outline.cc --- a/plugins/tools/selectiontools/kis_tool_select_outline.cc +++ b/plugins/tools/selectiontools/kis_tool_select_outline.cc @@ -171,7 +171,6 @@ KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); - painter.setPaintOpPreset(currentPaintOpPreset(), currentNode(), currentImage()); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); diff --git a/plugins/tools/selectiontools/kis_tool_select_polygonal.cc b/plugins/tools/selectiontools/kis_tool_select_polygonal.cc --- a/plugins/tools/selectiontools/kis_tool_select_polygonal.cc +++ b/plugins/tools/selectiontools/kis_tool_select_polygonal.cc @@ -59,7 +59,6 @@ KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); - painter.setPaintOpPreset(currentPaintOpPreset(), currentNode(), currentImage()); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); diff --git a/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp b/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp --- a/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp +++ b/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp @@ -69,7 +69,7 @@ qreal angle = KisAlgebra2D::directionBetweenPoints(prevPos, info.pos(), 0); previousDistanceInfo = - KisDistanceInformation(prevPos, 0, angle); + KisDistanceInformation(prevPos, angle); previousPaintInfo = info; } diff --git a/sdk/tests/KisRectsCollisionsTracker.h b/sdk/tests/KisRectsCollisionsTracker.h new file mode 100644 --- /dev/null +++ b/sdk/tests/KisRectsCollisionsTracker.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017 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 KISRECTSCOLLISIONSTRACKER_H +#define KISRECTSCOLLISIONSTRACKER_H + +#include +#include +#include +#include + +#include "kis_assert.h" + + +class KisRectsCollisionsTracker +{ +public: + + void startAccessingRect(const QRect &rc) { + QMutexLocker l(&m_mutex); + + checkUniqueAccessImpl(rc, "start"); + m_rectsInProgress.append(rc); + } + + void endAccessingRect(const QRect &rc) { + QMutexLocker l(&m_mutex); + const bool result = m_rectsInProgress.removeOne(rc); + KIS_SAFE_ASSERT_RECOVER_NOOP(result); + checkUniqueAccessImpl(rc, "end"); + } + +private: + + bool checkUniqueAccessImpl(const QRect &rect, const QString &tag) { + + Q_FOREACH (const QRect &rc, m_rectsInProgress) { + if (rc != rect && rect.intersects(rc)) { + ENTER_FUNCTION() << "FAIL: concurrect access from" << rect << "to" << rc << tag; + return false; + } + } + + return true; + } + +private: + QList m_rectsInProgress; + QMutex m_mutex; +}; + +#endif // KISRECTSCOLLISIONSTRACKER_H diff --git a/sdk/tests/stroke_testing_utils.h b/sdk/tests/stroke_testing_utils.h --- a/sdk/tests/stroke_testing_utils.h +++ b/sdk/tests/stroke_testing_utils.h @@ -50,6 +50,8 @@ void setNumIterations(int value); void setBaseFuzziness(int value); + int lastStrokeTime() const; + protected: KisStrokeId strokeId() { return m_strokeId; @@ -101,6 +103,7 @@ QString m_presetFilename; int m_numIterations; int m_baseFuzziness; + int m_strokeTime = 0; }; } diff --git a/sdk/tests/stroke_testing_utils.cpp b/sdk/tests/stroke_testing_utils.cpp --- a/sdk/tests/stroke_testing_utils.cpp +++ b/sdk/tests/stroke_testing_utils.cpp @@ -32,6 +32,7 @@ #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "kis_group_layer.h" +#include #include "testutil.h" @@ -63,6 +64,7 @@ const QString &presetFileName) { KoCanvasResourceManager *manager = new KoCanvasResourceManager(); + KisViewManager::initializeResourceManager(manager); QVariant i; @@ -148,6 +150,11 @@ testOneStroke(false, true, false, true); } +int utils::StrokeTester::lastStrokeTime() const +{ + return m_strokeTime; +} + void utils::StrokeTester::test() { testOneStroke(false, false, false); @@ -271,6 +278,9 @@ initImage(image, resources->currentNode(), i); + QElapsedTimer strokeTime; + strokeTime.start(); + KisStrokeStrategy *stroke = createStroke(indirectPainting, resources, image); m_strokeId = image->startStroke(stroke); addPaintingJobs(image, resources, i); @@ -283,6 +293,8 @@ } image->waitForDone(); + + m_strokeTime = strokeTime.elapsed(); currentNode = resources->currentNode(); } diff --git a/sdk/tests/testutil.h b/sdk/tests/testutil.h --- a/sdk/tests/testutil.h +++ b/sdk/tests/testutil.h @@ -419,6 +419,71 @@ qint64 m_cycles; }; +struct MeasureDistributionStats { + MeasureDistributionStats(int numBins, const QString &name = QString()) + : m_numBins(numBins), + m_name(name) + { + reset(); + } + + void reset() { + m_values.clear(); + m_values.resize(m_numBins); + } + + void addValue(int value) { + addValue(value, 1); + } + + void addValue(int value, int increment) { + KIS_SAFE_ASSERT_RECOVER_RETURN(value >= 0); + + if (value >= m_numBins) { + m_values[m_numBins - 1] += increment; + } else { + m_values[value] += increment; + } + } + + void print() { + qCritical() << "============= Stats =============="; + + if (!m_name.isEmpty()) { + qCritical() << "Name:" << m_name; + } + + int total = 0; + + for (int i = 0; i < m_numBins; i++) { + total += m_values[i]; + } + + for (int i = 0; i < m_numBins; i++) { + if (!m_values[i]) continue; + + const QString lastMarker = i == m_numBins - 1 ? "> " : " "; + + const QString line = + QString(" %1%2: %3 (%4%)") + .arg(lastMarker) + .arg(i, 3) + .arg(m_values[i], 5) + .arg(qreal(m_values[i]) / total * 100.0, 7, 'g', 2); + + qCritical() << qPrintable(line); + } + qCritical() << "---- ----"; + qCritical() << qPrintable(QString("Total: %1").arg(total)); + qCritical() << "=================================="; + } + +private: + QVector m_values; + int m_numBins = 0; + QString m_name; +}; + QStringList getHierarchy(KisNodeSP root, const QString &prefix = ""); bool checkHierarchy(KisNodeSP root, const QStringList &expected);