Commit e03e9533 authored by Dmitry Kazakov's avatar Dmitry Kazakov

Implement the first version of the multithreaded Pixel Brush

All the presets that use Pixel Brush (KisBrushOp) are now
multithreaded and rendered asynchronously. Basically, it means
that if the brush is too slow, Krita will lower down FPS rate
for the sake of faster rendering of the stroke.


Short summary:

1) It doesn't use strokes system's threading, just QtConcurrent. It
   is not good, but works for now. I hope it is only a temporary
   solution.

2) Updates are coming asynchronously with the period of 20...80ms,
   which is 50...12fps. I didn't manage to implement a correct control
   loop for auto-adjusting the FPS value, because it needs porting the
   threading part into strokes system and a bit of refactoring of
   the strokes system itself. Therefore, the FPS adjustment is controlled
   by an open-loop system, based on one-dab-rendering-time. Basically,
   FPS is proportional to the time spent on rendering a single tile.

3) The patch adds two new API functions: KisPaintOpSettings::
   needsAsynchronousUpdates() tells if the paintop uses threading and
   needs asynchronous updates. When this function returns true, the
   freehand stroke does additional calls to
   KisPaintOp::doAsyncronousUpdate(), which does the rendering itself.

4) Still to be implemented:
     * color source options
     * postprocessing: sharpness and texturing
     * selection handling (works only in Wash mode)
     * mirroring mode
     * pipe brushes

CC:kimageshop@kde.org
parent 82dcd6ef
/*
* Copyright (c) 2017 Dmitry Kazakov <dimula73@gmail.com>
*
* 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())
{
}
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
......@@ -103,6 +103,11 @@ void KisPaintOp::splitCoordinate(qreal coordinate, qint32 *whole, qreal *fractio
*fraction = f;
}
int KisPaintOp::doAsyncronousUpdate()
{
return 40;
}
static void paintBezierCurve(KisPaintOp *paintOp,
const KisPaintInformation &pi1,
const KisVector2D &control1,
......
......@@ -111,6 +111,14 @@ public:
*/
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.
*
* @return the desired FPS rate (period of updates)
*/
virtual int doAsyncronousUpdate();
protected:
friend class KisPaintInformation;
/**
......
......@@ -332,6 +332,11 @@ bool KisPaintOpSettings::useSpacingUpdates() const
return getBool(SPACING_USE_UPDATES, false);
}
bool KisPaintOpSettings::needsAsynchronousUpdates() const
{
return false;
}
QPainterPath KisPaintOpSettings::brushOutline(const KisPaintInformation &info, OutlineMode mode)
{
QPainterPath path;
......
......@@ -151,6 +151,12 @@ public:
*/
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.
*/
......
......@@ -2542,6 +2542,20 @@ void KisPainter::setOpacityUpdateAverage(quint8 opacity)
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;
......
......@@ -53,6 +53,7 @@ class KoPattern;
class KisPaintInformation;
class KisPaintOp;
class KisDistanceInformation;
class KisRenderedDab;
/**
* KisPainter contains the graphics primitives necessary to draw on a
......@@ -301,7 +302,7 @@ public:
* 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<KisFixedPaintDeviceSP> allSrcDevices);
void bltFixed(const QRect &rc, const QList<KisRenderedDab> allSrcDevices);
/**
* Convenience method that uses QPoint and QRect.
......@@ -714,6 +715,16 @@ public:
*/
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);
......
......@@ -22,17 +22,18 @@
#include "kis_paint_device.h"
#include "kis_fixed_paint_device.h"
#include "kis_random_accessor_ng.h"
#include "KisRenderedDab.h"
void KisPainter::Private::collectRowDevices(int x1, int x2, int y, int y2,
const QList<KisFixedPaintDeviceSP> allDevices,
QList<KisFixedPaintDeviceSP> *rowDevices,
const QList<KisRenderedDab> allDevices,
QList<KisRenderedDab> *rowDevices,
int *numContiguousRows)
{
*numContiguousRows = y2 - y + 1;
rowDevices->clear();
for (auto it = allDevices.cbegin(); it != allDevices.cend(); ++it) {
const QRect rc = (*it)->bounds();
const QRect rc = it->realBounds();
if (rc.left() > x2 || rc.right() < x1 ||
rc.top() > y || rc.bottom() < y) {
......@@ -53,18 +54,22 @@ void KisPainter::Private::collectRowDevices(int x1, int x2, int y, int y2,
struct KisPainter::Private::ChunkDescriptor {
quint8 *ptr;
int rowStride;
float opacity = 1.0;
float averageOpacity = 1.0;
float flow = 1.0;
};
void KisPainter::Private::collectChunks(int x, int x2, int y,
QList<KisFixedPaintDeviceSP> rowDevices,
QList<KisRenderedDab> rowDevices,
QList<ChunkDescriptor> *chunks,
int *numContiguosColumns)
{
*numContiguosColumns = x2 - x + 1;
chunks->clear();
chunks->erase(chunks->begin(), chunks->end());
for (auto it = rowDevices.cbegin(); it != rowDevices.cend(); ++it) {
const QRect rc = (*it)->bounds();
const QRect rc = it->realBounds();
if (rc.left() > x || rc.right() < x) {
const int distance = rc.left() - x;
......@@ -78,12 +83,15 @@ void KisPainter::Private::collectChunks(int x, int x2, int y,
*numContiguosColumns = qMin(*numContiguosColumns,
rc.right() - x + 1);
const int pixelSize = (*it)->pixelSize();
const int pixelSize = it->device->pixelSize();
ChunkDescriptor chunk;
chunk.opacity = it->opacity;
chunk.averageOpacity = it->averageOpacity;
chunk.flow = it->flow;
chunk.rowStride = rc.width() * pixelSize;
chunk.ptr =
(*it)->data() +
it->device->data() +
chunk.rowStride * (y - rc.top()) +
pixelSize * (x - rc.left());
......@@ -94,7 +102,8 @@ void KisPainter::Private::collectChunks(int x, int x2, int y,
void KisPainter::Private::applyChunks(int x, int y, int width, int height,
KisRandomAccessorSP dstIt,
const QList<ChunkDescriptor> &chunks,
const KoColorSpace *srcColorSpace)
const KoColorSpace *srcColorSpace,
KoCompositeOp::ParameterInfo &localParamInfo)
{
const int srcPixelSize = srcColorSpace->pixelSize();
QList<ChunkDescriptor> savedChunks = chunks;
......@@ -102,7 +111,6 @@ void KisPainter::Private::applyChunks(int x, int y, int width, int height,
qint32 dstY = y;
qint32 rowsRemaining = height;
while (rowsRemaining > 0) {
qint32 dstX = x;
......@@ -121,19 +129,21 @@ void KisPainter::Private::applyChunks(int x, int y, int width, int height,
qint32 dstRowStride = dstIt->rowStride(dstX, dstY);
dstIt->moveTo(dstX, dstY);
paramInfo.dstRowStart = dstIt->rawData();
paramInfo.dstRowStride = dstRowStride;
paramInfo.maskRowStart = 0;
paramInfo.maskRowStride = 0;
paramInfo.rows = rows;
paramInfo.cols = columns;
localParamInfo.dstRowStart = dstIt->rawData();
localParamInfo.dstRowStride = dstRowStride;
localParamInfo.maskRowStart = 0;
localParamInfo.maskRowStride = 0;
localParamInfo.rows = rows;
localParamInfo.cols = columns;
const int srcColumnStep = srcPixelSize * columns;
for (auto it = rowChunks.begin(); it != rowChunks.end(); ++it) {
paramInfo.srcRowStart = it->ptr;
paramInfo.srcRowStride = it->rowStride;
colorSpace->bitBlt(srcColorSpace, paramInfo, compositeOp, renderingIntent, conversionFlags);
localParamInfo.srcRowStart = it->ptr;
localParamInfo.srcRowStride = it->rowStride;
localParamInfo.setOpacityAndAverage(it->opacity, it->averageOpacity);
localParamInfo.flow = it->flow;
colorSpace->bitBlt(srcColorSpace, localParamInfo, compositeOp, renderingIntent, conversionFlags);
it->ptr += srcColumnStep;
}
......@@ -153,31 +163,32 @@ void KisPainter::Private::applyChunks(int x, int y, int width, int height,
}
void KisPainter::bltFixed(const QRect &rc, const QList<KisFixedPaintDeviceSP> allSrcDevices)
void KisPainter::bltFixed(const QRect &rc, const QList<KisRenderedDab> allSrcDevices)
{
const KoColorSpace *srcColorSpace = 0;
QList<KisFixedPaintDeviceSP> devices;
QList<KisRenderedDab> devices;
QRect totalDevicesRect;
Q_FOREACH (KisFixedPaintDeviceSP dev, allSrcDevices) {
if (rc.intersects(dev->bounds())) {
devices.append(dev);
totalDevicesRect |= dev->bounds();
Q_FOREACH (const KisRenderedDab &dab, allSrcDevices) {
if (rc.intersects(dab.realBounds())) {
devices.append(dab);
totalDevicesRect |= dab.realBounds();
}
if (!srcColorSpace) {
srcColorSpace = dev->colorSpace();
srcColorSpace = dab.device->colorSpace();
} else {
KIS_SAFE_ASSERT_RECOVER_RETURN(*srcColorSpace == *dev->colorSpace());
KIS_SAFE_ASSERT_RECOVER_RETURN(*srcColorSpace == *dab.device->colorSpace());
}
}
if (devices.isEmpty() || !totalDevicesRect.intersects(rc)) return;
KoCompositeOp::ParameterInfo localParamInfo = d->paramInfo;
KisRandomAccessorSP dstIt = d->device->createRandomAccessorNG(rc.left(), rc.top());
int row = rc.top();
QList<KisFixedPaintDeviceSP> rowDevices;
QList<KisRenderedDab> rowDevices;
int numContiguousRowsInDevices = 0;
while (row <= rc.bottom()) {
......@@ -199,7 +210,8 @@ void KisPainter::bltFixed(const QRect &rc, const QList<KisFixedPaintDeviceSP> al
numContiguousColumnsInDevices, numContiguousRowsInDevices,
dstIt,
chunks,
srcColorSpace);
srcColorSpace,
localParamInfo);
}
column += numContiguousColumnsInDevices;
......
......@@ -85,20 +85,20 @@ struct Q_DECL_HIDDEN KisPainter::Private {
void fillPainterPathImpl(const QPainterPath& path, const QRect &requestedRect);
static void collectRowDevices(int x1, int x2, int y, int y2,
const QList<KisFixedPaintDeviceSP> allDevices,
QList<KisFixedPaintDeviceSP> *rowDevices,
const QList<KisRenderedDab> allDevices,
QList<KisRenderedDab> *rowDevices,
int *numContiguousRows);
struct ChunkDescriptor;
static void collectChunks(int x, int x2, int y,
QList<KisFixedPaintDeviceSP> rowDevices,
QList<KisRenderedDab> rowDevices,
QList<ChunkDescriptor> *chunks,
int *numContiguosColumns);
void applyChunks(int x, int y, int width, int height,
KisRandomAccessorSP dstIt,
const QList<ChunkDescriptor> &chunks,
const KoColorSpace *srcColorSpace);
const KoColorSpace *srcColorSpace, KoCompositeOp::ParameterInfo &localParamInfo);
};
......
......@@ -517,8 +517,9 @@ void KisPainterTest::benchmarkBitBltOldData()
}
}
#include "kis_paint_device_debug_utils.h"
#include "KisRenderedDab.h"
void testMassiveBltFixedImpl(int numRects)
void testMassiveBltFixedImpl(int numRects, bool varyOpacity = false)
{
const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8();
KisPaintDeviceSP dst = new KisPaintDevice(cs);
......@@ -529,7 +530,7 @@ void testMassiveBltFixedImpl(int numRects)
colors << Qt::blue;
QRect devicesRect;
QList<KisFixedPaintDeviceSP> devices;
QList<KisRenderedDab> devices;
for (int i = 0; i < numRects; i++) {
const QRect rc(10 + i * 10, 10 + i * 10, 30, 30);
......@@ -538,10 +539,18 @@ void testMassiveBltFixedImpl(int numRects)
dev->initialize();
dev->fill(rc, KoColor(colors[i % 3], cs));
dev->fill(kisGrowRect(rc, -5), KoColor(Qt::white, cs));
devices << dev;
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;
}
const QString opacityPostfix = varyOpacity ? "_varyop" : "";
const QRect fullRect = kisGrowRect(devicesRect, 10);
{
......@@ -551,7 +560,7 @@ void testMassiveBltFixedImpl(int numRects)
QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect),
"kispainter_test",
"massive_bitblt",
QString("full_update_%1").arg(numRects)));
QString("full_update_%1%2").arg(numRects).arg(opacityPostfix)));
}
dst->clear();
......@@ -568,7 +577,7 @@ void testMassiveBltFixedImpl(int numRects)
QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect),
"kispainter_test",
"massive_bitblt",
QString("partial_update_%1").arg(numRects)));
QString("partial_update_%1%2").arg(numRects).arg(opacityPostfix)));
}
}
......@@ -583,12 +592,17 @@ void KisPainterTest::testMassiveBltFixedMultiTile()
testMassiveBltFixedImpl(6);
}
void KisPainterTest::testMassiveBltFixedMultiTileWithOpacity()
{
testMassiveBltFixedImpl(6, true);
}
void KisPainterTest::testMassiveBltFixedCornerCases()
{
const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8();
KisPaintDeviceSP dst = new KisPaintDevice(cs);
QList<KisFixedPaintDeviceSP> devices;
QList<KisRenderedDab> devices;
QVERIFY(dst->extent().isEmpty());
......@@ -607,6 +621,8 @@ void KisPainterTest::testMassiveBltFixedCornerCases()
dev->initialize();
dev->fill(rc, KoColor(Qt::white, cs));
devices.append(KisRenderedDab(dev));
{
// rect outside the devices bounds, shouldn't crash
KisPainter painter(dst);
......
......@@ -57,8 +57,9 @@ private Q_SLOTS:
void testMassiveBltFixedSingleTile();
void testMassiveBltFixedMultiTile();
void testMassiveBltFixedCornerCases();
void testMassiveBltFixedMultiTileWithOpacity();
void testMassiveBltFixedCornerCases();
};
#endif
......
......@@ -60,6 +60,18 @@ KoCompositeOp::ParameterInfo& KoCompositeOp::ParameterInfo::operator=(const Para
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;
......
......@@ -25,6 +25,8 @@
#include <QMultiMap>
#include <QBitArray>
#include <boost/optional.hpp>
#include "kritapigment_export.h"
class KoColorSpace;
......@@ -70,6 +72,8 @@ public:
float* lastOpacity;
QBitArray channelFlags;
void setOpacityAndAverage(float _opacity, float _averageOpacity);
void updateOpacityAndAverage(float value);
private:
inline void copy(const ParameterInfo &rhs);
......
......@@ -373,6 +373,11 @@ bool KisResourcesSnapshot::presetAllowsLod() const
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;
......
......@@ -89,6 +89,7 @@ public:
qreal effectiveZoom() const;
bool presetAllowsLod() const;
bool presetNeedsAsynchronousUpdates() const;
void setFGColorOverride(const KoColor &color);
void setBGColorOverride(const KoColor &color);
......
......@@ -101,6 +101,8 @@ struct KisToolFreehandHelper::Private
KisStabilizedEventsSampler stabilizedSampler;
KisStabilizerDelayedPaintHelper stabilizerDelayedPaintHelper;
QTimer asynchronousUpdatesThresholdTimer;
int canvasRotation;
bool canvasMirroredH;
......@@ -124,6 +126,7 @@ KisToolFreehandHelper::KisToolFreehandHelper(KisPaintingInformationBuilder *info
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(
......@@ -306,9 +309,12 @@ void KisToolFreehandHelper::initPaintImpl(qreal startAngle,
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) {
......@@ -612,6 +618,10 @@ void KisToolFreehandHelper::endPaint()
m_d->airbrushingTimer.stop();
}
if (m_d->asynchronousUpdatesThresholdTimer.isActive()) {
m_d->asynchronousUpdatesThresholdTimer.stop();
}
if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) {
stabilizerEnd();
}
......@@ -642,6 +652,10 @@ void KisToolFreehandHelper::cancelPaint()
m_d->airbrushingTimer.stop();
}
if (m_d->asynchronousUpdatesThresholdTimer.isActive()) {
m_d->asynchronousUpdatesThresholdTimer.stop();
}
if (m_d->stabilizerPollTimer.isActive()) {
m_d->stabilizerPollTimer.stop();
}
......@@ -862,12 +876,24 @@ void KisToolFreehandHelper::doAirbrushing()
}
}
void KisToolFreehandHelper::doAsynchronousUpdate()
{
asyncUpdate();
}
int KisToolFreehandHelper::computeAirbrushTimerInterval() const
{
qreal realInterval = m_d->resources->airbrushingInterval() * AIRBRUSH_INTERVAL_FACTOR;
return qMax(1, qFloor(realInterval));
}
void KisToolFreehandHelper::asyncUpdate(int painterInfoId)
{
m_d->strokesFacade->addJob(m_d->strokeId,
new FreehandStrokeStrategy::UpdateData(m_d->resources->currentNode(),
painterInfoId));
}
void KisToolFreehandHelper::paintAt(int painterInfoId,
const KisPaintInformation &pi)
{
......@@ -942,6 +968,11 @@ void KisToolFreehandHelper::createPainters(QVector<PainterInfo*> &painterInfos,
painterInfos << new PainterInfo(startDist);
}
void KisToolFreehandHelper::asyncUpdate()
{
asyncUpdate(0);
}
void KisToolFreehandHelper::paintAt(const KisPaintInformation &pi)
{
paintAt(0, pi);
......
......@@ -113,6 +113,8 @@ protected:
// lo-level methods for painting primitives
void asyncUpdate(int painterInfoId);
void paintAt(int painterInfoId, const KisPaintInformation &pi);
void paintLine(int painterInfoId,
......@@ -127,6 +129,8 @@ protected:
// hi-level methods for painting primitives
virtual void asyncUpdate();
virtual void paintAt(const KisPaintInformation &pi);
virtual void paintLine(const KisPaintInformation &pi1,
......@@ -152,6 +156,7 @@ private Q_SLOTS:
void finishStroke();
void doAirbrushing();
void doAsynchronousUpdate();
void stabilizerPollAndPaint();
private:
......
......@@ -54,6 +54,13 @@ void KisToolMultihandHelper::createPainters(QVector<PainterInfo*> &painterInfos,
}
}
void KisToolMultihandHelper::asyncUpdate()