diff --git a/libs/image/KisRenderedDab.h b/libs/image/KisRenderedDab.h index 92f7f9a7f2..4b26a4e235 100644 --- a/libs/image/KisRenderedDab.h +++ b/libs/image/KisRenderedDab.h @@ -1,46 +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. */ #ifndef KISRENDEREDDAB_H #define KISRENDEREDDAB_H #include "kis_types.h" #include "kis_fixed_paint_device.h" struct KisRenderedDab { KisRenderedDab() {} KisRenderedDab(KisFixedPaintDeviceSP _device) : device(_device), offset(_device->bounds().topLeft()) { } + KisRenderedDab(const KisRenderedDab &rhs) = default; + 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 // KISRENDEREDDAB_H diff --git a/libs/image/brushengine/kis_paintop_utils.cpp b/libs/image/brushengine/kis_paintop_utils.cpp index b78833b9ab..5857b84e18 100644 --- a/libs/image/brushengine/kis_paintop_utils.cpp +++ b/libs/image/brushengine/kis_paintop_utils.cpp @@ -1,117 +1,115 @@ /* * 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 +#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 splitAndFilterDabRect(const QRect &totalRect, const QVector &dabRects, 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)) { + [dabRects] (const QRect &rc) { + Q_FOREACH (const QRect &dab, dabRects) { + if (dab.intersects(rc)) { return true; } } return false; }); return rects; } -QVector splitDabsIntoRects(const QList &dabs, int idealNumRects, int diameter, qreal spacing) +QVector splitDabsIntoRects(const QVector &dabRects, int idealNumRects, int diameter, qreal spacing) { - QRect totalRect; - - Q_FOREACH (const KisRenderedDab &dab, dabs) { - const QRect rc = dab.realBounds(); - totalRect |= rc; - } + QRect totalRect = + std::accumulate(dabRects.begin(), dabRects.end(), QRect(), std::bit_or()); 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); + QVector rects = splitAndFilterDabRect(totalRect, dabRects, idealPatchSize); while (rects.size() < idealNumRects && idealPatchSize >minPatchSize) { idealPatchSize = qMax(minPatchSize, idealPatchSize - patchStep); - rects = splitAndFilterDabRect(totalRect, dabs, idealPatchSize); + rects = splitAndFilterDabRect(totalRect, dabRects, idealPatchSize); } return rects; } } diff --git a/libs/image/brushengine/kis_paintop_utils.h b/libs/image/brushengine/kis_paintop_utils.h index 2e1acc84ea..1510bd5449 100644 --- a/libs/image/brushengine/kis_paintop_utils.h +++ b/libs/image/brushengine/kis_paintop_utils.h @@ -1,203 +1,203 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_PAINTOP_UTILS_H #define __KIS_PAINTOP_UTILS_H #include "kis_global.h" #include "kis_paint_information.h" #include "kis_distance_information.h" #include "kis_spacing_information.h" #include "kis_timing_information.h" #include "kritaimage_export.h" class KisRenderedDab; namespace KisPaintOpUtils { template bool paintFan(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, qreal fanCornersStep) { const qreal angleStep = fanCornersStep; const qreal initialAngle = currentDistance->lastDrawingAngle(); const qreal finalAngle = pi2.drawingAngleSafe(*currentDistance); const qreal fullDistance = shortestAngularDistance(initialAngle, finalAngle); qreal lastAngle = initialAngle; int i = 0; while (shortestAngularDistance(lastAngle, finalAngle) > angleStep) { lastAngle = incrementInDirection(lastAngle, angleStep, finalAngle); qreal t = angleStep * i++ / fullDistance; QPointF pt = pi1.pos() + t * (pi2.pos() - pi1.pos()); KisPaintInformation pi = KisPaintInformation::mix(pt, t, pi1, pi2); pi.overrideDrawingAngle(lastAngle); pi.paintAt(op, currentDistance); } return i; } template void paintLine(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, bool fanCornersEnabled, qreal fanCornersStep) { QPointF end = pi2.pos(); qreal endTime = pi2.currentTime(); KisPaintInformation pi = pi1; qreal t = 0.0; while ((t = currentDistance->getNextPointPosition(pi.pos(), end, pi.currentTime(), endTime)) >= 0.0) { pi = KisPaintInformation::mix(t, pi, pi2); if (fanCornersEnabled && currentDistance->hasLastPaintInformation()) { paintFan(op, currentDistance->lastPaintInformation(), pi, currentDistance, fanCornersStep); } /** * A bit complicated part to ensure the registration * of the distance information is done in right order */ pi.paintAt(op, currentDistance); } /* * Perform spacing and/or timing updates between dabs if appropriate. Typically, this will not * happen if the above loop actually painted anything. This is because the * getNextPointPosition() call before the paint operation will reset the accumulators in * currentDistance and therefore make needsSpacingUpdate() and needsTimingUpdate() false. The * temporal distance between pi1 and pi2 is typically too small for the accumulators to build * back up enough to require a spacing or timing update after that. (The accumulated time values * are updated not during the paint operation, but during the call to getNextPointPosition(), * that is, updated during every paintLine() call.) */ if (currentDistance->needsSpacingUpdate()) { op.updateSpacing(pi2, *currentDistance); } if (currentDistance->needsTimingUpdate()) { op.updateTiming(pi2, *currentDistance); } } /** * A special class containing the previous position of the cursor for * the sake of painting the outline of the paint op. The main purpose * of this class is to ensure that the saved point does not equal to * the current one, which would cause a outline flicker. To echieve * this the class stores two previosly requested points instead of the * last one. */ class KRITAIMAGE_EXPORT PositionHistory { public: /** * \return the previously used point, which is guaranteed not to * be equal to \p pt and updates the history if needed */ QPointF pushThroughHistory(const QPointF &pt) { QPointF result; const qreal pointSwapThreshold = 7.0; /** * We check x *and* y separately, because events generated by * a mouse device tend to come separately for x and y offsets. * Efficienty generating the 'stairs' pattern. */ if (qAbs(pt.x() - m_second.x()) > pointSwapThreshold && qAbs(pt.y() - m_second.y()) > pointSwapThreshold) { result = m_second; m_first = m_second; m_second = pt; } else { result = m_first; } return result; } private: QPointF m_first; QPointF m_second; }; inline bool checkSizeTooSmall(qreal scale, qreal width, qreal height) { return scale * width < 0.01 || scale * height < 0.01; } inline qreal calcAutoSpacing(qreal value, qreal coeff) { return coeff * (value < 1.0 ? value : sqrt(value)); } 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, bool distanceSpacingEnabled, bool isotropicSpacing, qreal rotation, bool axesFlipped, qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, qreal lodScale); KRITAIMAGE_EXPORT KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, qreal rateExtraScale); KRITAIMAGE_EXPORT -QVector splitAndFilterDabRect(const QRect &totalRect, const QList &dabs, int idealPatchSize); +QVector splitAndFilterDabRect(const QRect &totalRect, const QVector &dabRects, int idealPatchSize); KRITAIMAGE_EXPORT -QVector splitDabsIntoRects(const QList &dabs, int idealNumRects, int diameter, qreal spacing); +QVector splitDabsIntoRects(const QVector &dabRects, int idealNumRects, int diameter, qreal spacing); } #endif /* __KIS_PAINTOP_UTILS_H */ diff --git a/libs/image/kis_wrapped_rect.h b/libs/image/kis_wrapped_rect.h index 92b07b1bf5..9e80183128 100644 --- a/libs/image/kis_wrapped_rect.h +++ b/libs/image/kis_wrapped_rect.h @@ -1,116 +1,160 @@ /* * Copyright (c) 2013 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_WRAPPED_RECT_H #define __KIS_WRAPPED_RECT_H #include #include struct KisWrappedRect : public QVector { static inline int xToWrappedX(int x, const QRect &wrapRect) { x = (x - wrapRect.x()) % wrapRect.width(); if (x < 0) x += wrapRect.width(); return x; } static inline int yToWrappedY(int y, const QRect &wrapRect) { y = (y - wrapRect.y()) % wrapRect.height(); if (y < 0) y += wrapRect.height(); return y; } static inline QPoint ptToWrappedPt(QPoint pt, const QRect &wrapRect) { pt.rx() = xToWrappedX(pt.x(), wrapRect); pt.ry() = yToWrappedY(pt.y(), wrapRect); return pt; } + /** + * Return origins at which we should paint \p rc with crop rect set to \p wrapRect, + * so that the final image would look "wrapped". + */ + static inline QVector normalizationOriginsForRect(const QRect &rc, const QRect &wrapRect) { + QVector result; + + if (wrapRect.contains(rc)) { + result.append(rc.topLeft()); + } else { + int x = xToWrappedX(rc.x(), wrapRect); + int y = yToWrappedY(rc.y(), wrapRect); + int w = qMin(rc.width(), wrapRect.width()); + int h = qMin(rc.height(), wrapRect.height()); + + // we ensure that the topleft of the rect belongs to the + // visible rectangle + Q_ASSERT(x >= 0 && x < wrapRect.width()); + Q_ASSERT(y >= 0 && y < wrapRect.height()); + + QRect newRect(x, y, w, h); + + if (!(newRect & wrapRect).isEmpty()) { // tl + result.append(QPoint(x, y)); + } + + if (!(newRect.translated(-wrapRect.width(), 0) & wrapRect).isEmpty()) { // tr + result.append(QPoint(x - wrapRect.width(), y)); + } + + if (!(newRect.translated(0, -wrapRect.height()) & wrapRect).isEmpty()) { // bl + result.append(QPoint(x, y - wrapRect.height())); + } + + if (!(newRect.translated(-wrapRect.width(), -wrapRect.height()) & wrapRect).isEmpty()) { // br + result.append(QPoint(x - wrapRect.width(), y - wrapRect.height())); + } + } + + return result; + } + +public: + enum { TOPLEFT = 0, TOPRIGHT, BOTTOMLEFT, BOTTOMRIGHT }; KisWrappedRect(const QRect &rc, const QRect &wrapRect) : m_wrapRect(wrapRect), m_originalRect(rc) { if (wrapRect.contains(rc)) { append(rc); } else { int x = xToWrappedX(rc.x(), wrapRect); int y = yToWrappedY(rc.y(), wrapRect); int w = qMin(rc.width(), wrapRect.width()); int h = qMin(rc.height(), wrapRect.height()); // we ensure that the topleft of the rect belongs to the // visible rectangle Q_ASSERT(x >= 0 && x < wrapRect.width()); Q_ASSERT(y >= 0 && y < wrapRect.height()); QRect newRect(x, y, w, h); append(newRect & wrapRect); // tl append(newRect.translated(-wrapRect.width(), 0) & wrapRect); // tr append(newRect.translated(0, -wrapRect.height()) & wrapRect); // bl append(newRect.translated(-wrapRect.width(), -wrapRect.height()) & wrapRect); // br } } bool isSplit() const { int size = this->size(); // we can either split or not split only Q_ASSERT(size == 1 || size == 4); return size > 1; } QRect topLeft() const { return this->at(TOPLEFT); } QRect topRight() const { return this->at(TOPRIGHT); } QRect bottomLeft() const { return this->at(BOTTOMLEFT); } QRect bottomRight() const { return this->at(BOTTOMRIGHT); } QRect wrapRect() const { return m_wrapRect; } QRect originalRect() const { return m_originalRect; } private: QRect m_wrapRect; QRect m_originalRect; }; #endif /* __KIS_WRAPPED_RECT_H */ diff --git a/libs/image/tests/kis_painter_test.cpp b/libs/image/tests/kis_painter_test.cpp index 7ae36d03a7..68f963cf96 100644 --- a/libs/image/tests/kis_painter_test.cpp +++ b/libs/image/tests/kis_painter_test.cpp @@ -1,800 +1,806 @@ /* * Copyright (c) 2007 Sven Langkamp * * 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_test.h" #include #include #include #include #include #include #include #include #include #include "kis_datamanager.h" #include "kis_types.h" #include "kis_paint_device.h" #include "kis_painter.h" #include "kis_pixel_selection.h" #include "kis_fill_painter.h" #include #include "testutil.h" #include void KisPainterTest::allCsApplicator(void (KisPainterTest::* funcPtr)(const KoColorSpace*cs)) { QList colorsapces = KoColorSpaceRegistry::instance()->allColorSpaces(KoColorSpaceRegistry::AllColorSpaces, KoColorSpaceRegistry::OnlyDefaultProfile); Q_FOREACH (const KoColorSpace* cs, colorsapces) { QString csId = cs->id(); // ALL THESE COLORSPACES ARE BROKEN: WE NEED UNITTESTS FOR COLORSPACES! if (csId.startsWith("KS")) continue; if (csId.startsWith("Xyz")) continue; if (csId.startsWith('Y')) continue; if (csId.contains("AF")) continue; if (csId == "GRAYU16") continue; // No point in testing bounds with a cs without alpha if (csId == "GRAYU8") continue; // No point in testing bounds with a cs without alpha dbgKrita << "Testing with cs" << csId; if (cs && cs->compositeOp(COMPOSITE_OVER) != 0) { (this->*funcPtr)(cs); } else { dbgKrita << "Cannot bitBlt for cs" << csId; } } } void KisPainterTest::testSimpleBlt(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c(Qt::red, cs); c.setOpacity(quint8(128)); src->fill(20, 20, 20, 20, c.data()); QCOMPARE(src->exactBounds(), QRect(20, 20, 20, 20)); const KoCompositeOp* op; { op = cs->compositeOp(COMPOSITE_OVER); KisPainter painter(dst); painter.setCompositeOp(op); painter.bitBlt(50, 50, src, 20, 20, 20, 20); painter.end(); QCOMPARE(dst->exactBounds(), QRect(50,50,20,20)); } dst->clear(); { op = cs->compositeOp(COMPOSITE_COPY); KisPainter painter(dst); painter.setCompositeOp(op); painter.bitBlt(50, 50, src, 20, 20, 20, 20); painter.end(); QCOMPARE(dst->exactBounds(), QRect(50,50,20,20)); } } void KisPainterTest::testSimpleBlt() { allCsApplicator(&KisPainterTest::testSimpleBlt); } /* Note: the bltSelection tests assume the following geometry: 0,0 0,30 +---------+------+ | 10,10 | | | +----+ | | |####| | | |####| | +----+----+ | | 20,20 | | | | | +----------------+ 30,30 */ void KisPainterTest::testPaintDeviceBltSelection(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c(Qt::red, cs); c.setOpacity(quint8(128)); src->fill(0, 0, 20, 20, c.data()); QCOMPARE(src->exactBounds(), QRect(0, 0, 20, 20)); KisSelectionSP selection = new KisSelection(); selection->pixelSelection()->select(QRect(10, 10, 20, 20)); selection->updateProjection(); QCOMPARE(selection->selectedExactRect(), QRect(10, 10, 20, 20)); KisPainter painter(dst); painter.setSelection(selection); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QImage image = dst->convertToQImage(0); image.save("blt_Selection_" + cs->name() + ".png"); QCOMPARE(dst->exactBounds(), QRect(10, 10, 10, 10)); const KoCompositeOp* op = cs->compositeOp(COMPOSITE_SUBTRACT); if (op->id() == COMPOSITE_SUBTRACT) { KisPaintDeviceSP dst2 = new KisPaintDevice(cs); KisPainter painter2(dst2); painter2.setSelection(selection); painter2.setCompositeOp(op); painter2.bitBlt(0, 0, src, 0, 0, 30, 30); painter2.end(); QCOMPARE(dst2->exactBounds(), QRect(10, 10, 10, 10)); } } void KisPainterTest::testPaintDeviceBltSelection() { allCsApplicator(&KisPainterTest::testPaintDeviceBltSelection); } void KisPainterTest::testPaintDeviceBltSelectionIrregular(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KisFillPainter gc(src); gc.fillRect(0, 0, 20, 20, KoColor(Qt::red, cs)); gc.end(); QCOMPARE(src->exactBounds(), QRect(0, 0, 20, 20)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP psel = sel->pixelSelection(); psel->select(QRect(10, 15, 20, 15)); psel->select(QRect(15, 10, 15, 5)); QCOMPARE(psel->selectedExactRect(), QRect(10, 10, 20, 20)); QCOMPARE(TestUtil::alphaDevicePixel(psel, 13, 13), MIN_SELECTED); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QImage image = dst->convertToQImage(0); image.save("blt_Selection_irregular" + cs->name() + ".png"); QCOMPARE(dst->exactBounds(), QRect(10, 10, 10, 10)); Q_FOREACH (KoChannelInfo * channel, cs->channels()) { // Only compare alpha if there actually is an alpha channel in // this colorspace if (channel->channelType() == KoChannelInfo::ALPHA) { QColor c; dst->pixel(13, 13, &c); QCOMPARE((int) c.alpha(), (int) OPACITY_TRANSPARENT_U8); } } } void KisPainterTest::testPaintDeviceBltSelectionIrregular() { allCsApplicator(&KisPainterTest::testPaintDeviceBltSelectionIrregular); } void KisPainterTest::testPaintDeviceBltSelectionInverted(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KisFillPainter gc(src); gc.fillRect(0, 0, 30, 30, KoColor(Qt::red, cs)); gc.end(); QCOMPARE(src->exactBounds(), QRect(0, 0, 30, 30)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP psel = sel->pixelSelection(); psel->select(QRect(10, 10, 20, 20)); psel->invert(); sel->updateProjection(); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QCOMPARE(dst->exactBounds(), QRect(0, 0, 30, 30)); } void KisPainterTest::testPaintDeviceBltSelectionInverted() { allCsApplicator(&KisPainterTest::testPaintDeviceBltSelectionInverted); } void KisPainterTest::testSelectionBltSelection() { KisPixelSelectionSP src = new KisPixelSelection(); src->select(QRect(0, 0, 20, 20)); QCOMPARE(src->selectedExactRect(), QRect(0, 0, 20, 20)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP Selection = sel->pixelSelection(); Selection->select(QRect(10, 10, 20, 20)); QCOMPARE(Selection->selectedExactRect(), QRect(10, 10, 20, 20)); sel->updateProjection(); KisPixelSelectionSP dst = new KisPixelSelection(); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QCOMPARE(dst->selectedExactRect(), QRect(10, 10, 10, 10)); KisSequentialConstIterator it(dst, QRect(10, 10, 10, 10)); do { // These are selections, so only one channel and it should // be totally selected QCOMPARE(it.oldRawData()[0], MAX_SELECTED); } while (it.nextPixel()); } /* Test with non-square selection 0,0 0,30 +-----------+------+ | 13,13 | | | x +--+ | | +--+##| | | |#####| | +-----+-----+ | | 20,20 | | | | | +------------------+ 30,30 */ void KisPainterTest::testSelectionBltSelectionIrregular() { KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); KisPixelSelectionSP src = new KisPixelSelection(); src->select(QRect(0, 0, 20, 20)); QCOMPARE(src->selectedExactRect(), QRect(0, 0, 20, 20)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP Selection = sel->pixelSelection(); Selection->select(QRect(10, 15, 20, 15)); Selection->select(QRect(15, 10, 15, 5)); QCOMPARE(Selection->selectedExactRect(), QRect(10, 10, 20, 20)); QCOMPARE(TestUtil::alphaDevicePixel(Selection, 13, 13), MIN_SELECTED); sel->updateProjection(); KisPixelSelectionSP dst = new KisPixelSelection(); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QCOMPARE(dst->selectedExactRect(), QRect(10, 10, 10, 10)); QCOMPARE(TestUtil::alphaDevicePixel(dst, 13, 13), MIN_SELECTED); } void KisPainterTest::testSelectionBitBltFixedSelection() { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c(Qt::red, cs); c.setOpacity(quint8(128)); src->fill(0, 0, 20, 20, c.data()); QCOMPARE(src->exactBounds(), QRect(0, 0, 20, 20)); KisFixedPaintDeviceSP fixedSelection = new KisFixedPaintDevice(cs); fixedSelection->setRect(QRect(0, 0, 20, 20)); fixedSelection->initialize(); KoColor fill(Qt::white, cs); fixedSelection->fill(5, 5, 10, 10, fill.data()); fixedSelection->convertTo(KoColorSpaceRegistry::instance()->alpha8()); KisPainter painter(dst); painter.bitBltWithFixedSelection(0, 0, src, fixedSelection, 20, 20); painter.end(); QCOMPARE(dst->exactBounds(), QRect(5, 5, 10, 10)); /* dbgKrita << "canary1.5"; dst->clear(); painter.begin(dst); painter.bitBltWithFixedSelection(0, 0, src, fixedSelection, 10, 20); painter.end(); dbgKrita << "canary2"; QCOMPARE(dst->exactBounds(), QRect(5, 5, 5, 10)); dst->clear(); painter.begin(dst); painter.bitBltWithFixedSelection(0, 0, src, fixedSelection, 5, 5, 5, 5, 10, 20); painter.end(); dbgKrita << "canary3"; QCOMPARE(dst->exactBounds(), QRect(5, 5, 5, 10)); dst->clear(); painter.begin(dst); painter.bitBltWithFixedSelection(5, 5, src, fixedSelection, 10, 20); painter.end(); dbgKrita << "canary4"; QCOMPARE(dst->exactBounds(), QRect(10, 10, 5, 10)); */ } void KisPainterTest::testSelectionBitBltEraseCompositeOp() { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintDeviceSP dst = new KisPaintDevice(cs); KoColor c(Qt::red, cs); dst->fill(0, 0, 150, 150, c.data()); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c2(Qt::black, cs); src->fill(50, 50, 50, 50, c2.data()); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP selection = sel->pixelSelection(); selection->select(QRect(25, 25, 100, 100)); sel->updateProjection(); const KoCompositeOp* op = cs->compositeOp(COMPOSITE_ERASE); KisPainter painter(dst); painter.setSelection(sel); painter.setCompositeOp(op); painter.bitBlt(0, 0, src, 0, 0, 150, 150); painter.end(); //dst->convertToQImage(0).save("result.png"); QRect erasedRect(50, 50, 50, 50); KisSequentialConstIterator it(dst, QRect(0, 0, 150, 150)); do { if(!erasedRect.contains(it.x(), it.y())) { QVERIFY(memcmp(it.oldRawData(), c.data(), cs->pixelSize()) == 0); } } while (it.nextPixel()); } void KisPainterTest::testSimpleAlphaCopy() { KisPaintDeviceSP src = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); KisPaintDeviceSP dst = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); quint8 p = 128; src->fill(0, 0, 100, 100, &p); QVERIFY(src->exactBounds() == QRect(0, 0, 100, 100)); KisPainter gc(dst); gc.setCompositeOp(KoColorSpaceRegistry::instance()->alpha8()->compositeOp(COMPOSITE_COPY)); gc.bitBlt(QPoint(0, 0), src, src->exactBounds()); gc.end(); QCOMPARE(dst->exactBounds(), QRect(0, 0, 100, 100)); } void KisPainterTest::checkPerformance() { KisPaintDeviceSP src = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); KisPaintDeviceSP dst = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); quint8 p = 128; src->fill(0, 0, 10000, 5000, &p); KisSelectionSP sel = new KisSelection(); sel->pixelSelection()->select(QRect(0, 0, 10000, 5000), 128); sel->updateProjection(); QTime t; t.start(); for (int i = 0; i < 10; ++i) { KisPainter gc(dst); gc.bitBlt(0, 0, src, 0, 0, 10000, 5000); } t.restart(); for (int i = 0; i < 10; ++i) { KisPainter gc(dst, sel); gc.bitBlt(0, 0, src, 0, 0, 10000, 5000); } } void KisPainterTest::testBitBltOldData() { const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); KisPaintDeviceSP src = new KisPaintDevice(cs); KisPaintDeviceSP dst = new KisPaintDevice(cs); quint8 defaultPixel = 0; quint8 p1 = 128; quint8 p2 = 129; quint8 p3 = 130; KoColor defaultColor(&defaultPixel, cs); KoColor color1(&p1, cs); KoColor color2(&p2, cs); KoColor color3(&p3, cs); QRect fillRect(0,0,5000,5000); src->fill(fillRect, color1); KisPainter srcGc(src); srcGc.beginTransaction(); src->fill(fillRect, color2); KisPainter dstGc(dst); dstGc.bitBltOldData(QPoint(), src, fillRect); QVERIFY(TestUtil::checkAlphaDeviceFilledWithPixel(dst, fillRect, p1)); dstGc.end(); srcGc.deleteTransaction(); } void KisPainterTest::benchmarkBitBlt() { quint8 p = 128; const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); KisPaintDeviceSP src = new KisPaintDevice(cs); KisPaintDeviceSP dst = new KisPaintDevice(cs); KoColor color(&p, cs); QRect fillRect(0,0,5000,5000); src->fill(fillRect, color); QBENCHMARK { KisPainter gc(dst); gc.bitBlt(QPoint(), src, fillRect); } } void KisPainterTest::benchmarkBitBltOldData() { quint8 p = 128; const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); KisPaintDeviceSP src = new KisPaintDevice(cs); KisPaintDeviceSP dst = new KisPaintDevice(cs); KoColor color(&p, cs); QRect fillRect(0,0,5000,5000); src->fill(fillRect, color); QBENCHMARK { KisPainter gc(dst); gc.bitBltOldData(QPoint(), src, fillRect); } } #include "kis_paint_device_debug_utils.h" #include "KisRenderedDab.h" void testMassiveBltFixedImpl(int numRects, bool varyOpacity = false, bool useSelection = false) { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); 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(); } + + QVector dabRects; + Q_FOREACH (const KisRenderedDab &dab, devices) { + dabRects.append(dab.realBounds()); + } + 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); + QVector rects = KisPaintOpUtils::splitDabsIntoRects(dabRects, 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/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp b/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp index def6285d4f..f7c3798d88 100644 --- a/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp @@ -1,379 +1,426 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004-2008 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * * 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_brushop.h" #include #include #include #include #include #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" +#include "kis_wrapped_rect.h" + KisBrushOp::KisBrushOp(const KisPaintOpSettingsSP settings, KisPainter *painter, KisNodeSP node, KisImageSP image) : KisBrushBasedPaintOp(settings, painter) , m_opacityOption(node) , m_avgSpacing(50) , m_avgNumDabs(50) , m_avgUpdateTimePerDab(50) , m_idealNumRects(KisImageConfig().maxNumberOfThreads()) , m_minUpdatePeriod(10) , m_maxUpdatePeriod(100) { Q_UNUSED(image); Q_ASSERT(settings); /** * 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); m_opacityOption.readOptionSetting(settings); m_flowOption.readOptionSetting(settings); m_sizeOption.readOptionSetting(settings); m_ratioOption.readOptionSetting(settings); m_spacingOption.readOptionSetting(settings); m_rateOption.readOptionSetting(settings); m_softnessOption.readOptionSetting(settings); m_rotationOption.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_rotationOption.resetAllSensors(); m_scatterOption.resetAllSensors(); m_sharpnessOption.resetAllSensors(); 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() { } KisSpacingInformation KisBrushOp::paintAt(const KisPaintInformation& info) { if (!painter()->device()) return KisSpacingInformation(1.0); KisBrushSP brush = m_brush; Q_ASSERT(brush); if (!brush) return KisSpacingInformation(1.0); if (!brush->canPaintFor(info)) return KisSpacingInformation(1.0); qreal scale = m_sizeOption.apply(info); scale *= KisLodTransform::lodToScale(painter()->device()); if (checkSizeTooSmall(scale)) return KisSpacingInformation(); qreal rotation = m_rotationOption.apply(info); qreal ratio = m_ratioOption.apply(info); KisDabShape shape(scale, ratio, rotation); QPointF cursorPos = m_scatterOption.apply(info, brush->maskWidth(shape, 0, 0, info), brush->maskHeight(shape, 0, 0, info)); m_opacityOption.setFlow(m_flowOption.apply(info)); 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)); } 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)); } state->allDirtyRects.append(rects); } std::pair KisBrushOp::doAsyncronousUpdate(QVector &jobs) { bool someDabsAreStillInQueue = false; const bool hasPreparedDabsAtStart = m_dabExecutor->hasPreparedDabs(); if (!m_updateSharedState && hasPreparedDabsAtStart) { m_updateSharedState = toQShared(new UpdateSharedState()); UpdateSharedStateSP state = m_updateSharedState; state->painter = painter(); { const qreal dabRenderingTime = m_dabExecutor->averageDabRenderingTime(); const qreal totalRenderingTimePerDab = dabRenderingTime + m_avgUpdateTimePerDab.rollingMeanSafe(); // we limit the number of fetched dabs to fit the maximum update period and not // make visual hiccups const int dabsLimit = totalRenderingTimePerDab > 0 ? qMax(10, int(m_maxUpdatePeriod / totalRenderingTimePerDab * m_idealNumRects)) : -1; state->dabsQueue = m_dabExecutor->takeReadyDabs(painter()->hasMirroring(), dabsLimit, &someDabsAreStillInQueue); } KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!state->dabsQueue.isEmpty(), std::make_pair(m_currentUpdatePeriod, false)); const int diameter = m_dabExecutor->averageDabSize(); const qreal spacing = m_avgSpacing.rollingMean(); const int idealNumRects = m_idealNumRects; - QVector rects = - KisPaintOpUtils::splitDabsIntoRects(state->dabsQueue, + + QVector rects; + + // wrap the dabs if needed + if (painter()->device()->defaultBounds()->wrapAroundMode()) { + /** + * In WA mode we do two things: + * + * 1) We ensure that the parallel threads do not access the same are on + * the image. For normal updates that is ensured by the code in KisImage + * and the scheduler. Here we should do that manually by adjusting 'rects' + * so that they would not intersect in the wrapped space. + * + * 2) We duplicate dabs, to ensure that all the pieces of dabs are painted + * inside the wraped rect. No pieces are dabs are painted twice, because + * we paint only the parts intersecting the wrap rect. + */ + + const QRect wrapRect = painter()->device()->defaultBounds()->bounds(); + + QList wrappedDabs; + + Q_FOREACH (const KisRenderedDab &dab, state->dabsQueue) { + const QVector normalizationOrigins = + KisWrappedRect::normalizationOriginsForRect(dab.realBounds(), wrapRect); + + Q_FOREACH(const QPoint &pt, normalizationOrigins) { + KisRenderedDab newDab = dab; + + newDab.offset = pt; + + rects.append(newDab.realBounds() & wrapRect); + wrappedDabs.append(newDab); + } + } + + state->dabsQueue = wrappedDabs; + + } else { + // just get all rects + Q_FOREACH (const KisRenderedDab &dab, state->dabsQueue) { + rects.append(dab.realBounds()); + } + } + + // split/merge rects into non-overlapping areas + rects = KisPaintOpUtils::splitDabsIntoRects(rects, 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)); } /** * After the dab has been rendered once, we should mirror it either one * (h __or__ v) or three (h __and__ v) times. This sequence of 'if's achives * the goal without any extra copying. Please note that it has __no__ 'else' * branches, which is done intentionally! */ 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, someDabsAreStillInQueue] () { 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 qreal dabRenderingTime = m_dabExecutor->averageDabRenderingTime(); m_avgNumDabs(state->dabsQueue.size()); const qreal currentUpdateTimePerDab = qreal(updateRenderingTime) / state->dabsQueue.size(); m_avgUpdateTimePerDab(currentUpdateTimePerDab); /** * NOTE: using currentUpdateTimePerDab in the calculation for the next update time instead * of the average one makes rendering speed about 40% faster. It happens because the * adaptation period is shorter than if it used */ const qreal totalRenderingTimePerDab = dabRenderingTime + currentUpdateTimePerDab; const int approxDabRenderingTime = qreal(totalRenderingTimePerDab) * m_avgNumDabs.rollingMean() / m_idealNumRects; m_currentUpdatePeriod = someDabsAreStillInQueue ? m_minUpdatePeriod : qBound(m_minUpdatePeriod, int(1.5 * approxDabRenderingTime), m_maxUpdatePeriod); { // debug chunk // ENTER_FUNCTION() << ppVar(state->allDirtyRects.size()) << ppVar(state->dabsQueue.size()) << ppVar(dabRenderingTime) << ppVar(updateRenderingTime); // ENTER_FUNCTION() << ppVar(m_currentUpdatePeriod) << ppVar(someDabsAreStillInQueue); } // release all the dab devices state->dabsQueue.clear(); m_updateSharedState.clear(); }, KisStrokeJobData::SEQUENTIAL)); } else if (m_updateSharedState && hasPreparedDabsAtStart) { someDabsAreStillInQueue = true; } return std::make_pair(m_currentUpdatePeriod, someDabsAreStillInQueue); } KisSpacingInformation KisBrushOp::updateSpacingImpl(const KisPaintInformation &info) const { const qreal scale = m_sizeOption.apply(info) * KisLodTransform::lodToScale(painter()->device()); qreal rotation = m_rotationOption.apply(info); return effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); } KisTimingInformation KisBrushOp::updateTimingImpl(const KisPaintInformation &info) const { return KisPaintOpPluginUtils::effectiveTiming(&m_airbrushOption, &m_rateOption, info); } void KisBrushOp::paintLine(const KisPaintInformation& pi1, const KisPaintInformation& pi2, KisDistanceInformation *currentDistance) { if (m_sharpnessOption.isChecked() && m_brush && (m_brush->width() == 1) && (m_brush->height() == 1)) { if (!m_lineCacheDevice) { m_lineCacheDevice = source()->createCompositionSourceDevice(); } else { m_lineCacheDevice->clear(); } KisPainter p(m_lineCacheDevice); p.setPaintColor(painter()->paintColor()); p.drawDDALine(pi1.pos(), pi2.pos()); QRect rc = m_lineCacheDevice->extent(); painter()->bitBlt(rc.x(), rc.y(), m_lineCacheDevice, rc.x(), rc.y(), rc.width(), rc.height()); //fixes Bug 338011 painter()->renderMirrorMask(rc, m_lineCacheDevice); } else { KisPaintOp::paintLine(pi1, pi2, currentDistance); } }