Commit fda4a0d5 authored by Dmitry Kazakov's avatar Dmitry Kazakov

[NEED TESTING FROM USERS!] A HUGE optimization for the merger framework

Now the merger deals much more carefully with the layer's extent and
doesn't write data if the area in question is empty! That is really
important when working with transformation and transparency masks.

What needs to be tested really thoroughly:

1) Using Move Tool

* on usual layers
* on group layers
* on layers with transparency masks
* on layers with transformation masks

2) Updates when using Transformation Masks

3) Updates when using Transparency Masks


CCMAIL:kimageshop@kde.org
parent 716d55e5
......@@ -134,10 +134,7 @@ void KisFilter::process(const KisPaintDeviceSP src,
if(transaction) {
delete transaction;
KisPainter p(dst);
p.setCompositeOp(COMPOSITE_COPY);
p.setSelection(selection);
p.bitBlt(applyRect.topLeft(), temporary, applyRect);
KisPainter::copyAreaOptimized(applyRect.topLeft(), temporary, dst, applyRect, selection);
}
}
......
......@@ -103,9 +103,7 @@ public:
* filter inside. Then the layer has work as a pass-through
* node. Just copy the merged data to the layer's original.
*/
KisPainter gc(originalDevice);
gc.setCompositeOp(COMPOSITE_COPY);
gc.bitBlt(applyRect.topLeft(), m_projection, applyRect);
KisPainter::copyAreaOptimized(applyRect.topLeft(), m_projection, originalDevice, applyRect);
return true;
}
......@@ -327,9 +325,7 @@ void KisAsyncMerger::writeProjection(KisNodeSP topmostNode, bool useTempProjecti
if (!m_currentProjection) return;
if(m_currentProjection != m_finalProjection) {
KisPainter gc(m_finalProjection);
gc.setCompositeOp(m_finalProjection->colorSpace()->compositeOp(COMPOSITE_COPY));
gc.bitBlt(rect.topLeft(), m_currentProjection, rect);
KisPainter::copyAreaOptimized(rect.topLeft(), m_currentProjection, m_finalProjection, rect);
}
DEBUG_NODE_ACTION("Writing projection", "", topmostNode->parent(), rect);
}
......@@ -342,7 +338,7 @@ bool KisAsyncMerger::compositeWithProjection(KisLayerSP layer, const QRect &rect
KisPainter gc(m_currentProjection);
layer->projectionPlane()->apply(&gc, rect);
DEBUG_NODE_ACTION("Compositing projection", "", layer, needRect);
DEBUG_NODE_ACTION("Compositing projection", "", layer, rect);
return true;
}
......
......@@ -167,15 +167,15 @@ public:
return m_cloneNotifications;
}
inline const QRect& accessRect() const {
inline QRect accessRect() const {
return m_resultAccessRect;
}
inline const QRect& changeRect() const {
inline QRect changeRect() const {
return m_resultChangeRect;
}
inline const QRect& uncroppedChangeRect() const {
inline QRect uncroppedChangeRect() const {
return m_resultUncroppedChangeRect;
}
......@@ -191,7 +191,7 @@ public:
return m_startNode;
}
inline const QRect& requestedRect() const {
inline QRect requestedRect() const {
return m_requestedRect;
}
......
......@@ -136,9 +136,7 @@ void KisCloneLayer::copyOriginalToProjection(const KisPaintDeviceSP original,
QRect copyRect = rect;
copyRect.translate(-m_d->x, -m_d->y);
KisPainter gc(projection);
gc.setCompositeOp(colorSpace()->compositeOp(COMPOSITE_COPY));
gc.bitBlt(rect.topLeft(), original, copyRect);
KisPainter::copyAreaOptimized(rect.topLeft(), original, projection, copyRect);
}
void KisCloneLayer::setDirtyOriginal(const QRect &rect)
......
......@@ -27,7 +27,7 @@ class KisFullRefreshWalker : public KisRefreshSubtreeWalker, public KisMergeWalk
{
public:
KisFullRefreshWalker(QRect cropRect)
: m_firstRun(true)
: KisMergeWalker(NO_FILTHY), m_firstRun(true)
{
setCropRect(cropRect);
}
......@@ -69,14 +69,12 @@ public:
* true in case of full refresh walker, because all the
* children of the dirty node are dirty as well, that is
* why we shouldn't rely on usual registerChangeRect()
* mechanism for this node. Actually, node->changeRect()
* may not be valid in case its masks have been changes.
* That is why we just unite the changeRects of all its
* children here.
* mechanism for this node. That is why we just unite the
* changeRects of all its children here.
*/
if(node == startNode()) {
KisRefreshSubtreeWalker::calculateChangeRect(node, changeRect());
if(node == startNode() && node->parent()) {
KisRefreshSubtreeWalker::calculateChangeRect(node, requestedRect());
}
else {
KisMergeWalker::registerChangeRect(node, position);
......
......@@ -513,7 +513,7 @@ KisNode::PositionToFilthy calculatePositionToFilthy(KisNodeSP nodeInQuestion,
}
QRect KisLayer::applyMasks(const KisPaintDeviceSP source,
const KisPaintDeviceSP destination,
KisPaintDeviceSP destination,
const QRect &requestedRect,
KisNodeSP filthyNode,
KisNodeSP lastNode) const
......@@ -594,9 +594,7 @@ QRect KisLayer::applyMasks(const KisPaintDeviceSP source,
}
Q_ASSERT(applyRects.isEmpty());
KisPainter gc2(destination);
gc2.setCompositeOp(colorSpace()->compositeOp(COMPOSITE_COPY));
gc2.bitBlt(changeRect.topLeft(), tempDevice, changeRect);
KisPainter::copyAreaOptimized(changeRect.topLeft(), tempDevice, destination, changeRect);
}
}
......@@ -663,9 +661,7 @@ void KisLayer::copyOriginalToProjection(const KisPaintDeviceSP original,
KisPaintDeviceSP projection,
const QRect& rect) const
{
KisPainter gc(projection);
gc.setCompositeOp(colorSpace()->compositeOp(COMPOSITE_COPY));
gc.bitBlt(rect.topLeft(), original, rect);
KisPainter::copyAreaOptimized(rect.topLeft(), original, projection, rect);
}
KisAbstractProjectionPlaneSP KisLayer::projectionPlane() const
......
......@@ -362,7 +362,7 @@ protected:
bool &rectVariesFlag) const;
QRect applyMasks(const KisPaintDeviceSP source,
const KisPaintDeviceSP destination,
KisPaintDeviceSP destination,
const QRect &requestedRect,
KisNodeSP filthyNode, KisNodeSP lastNode) const;
private:
......
......@@ -21,6 +21,7 @@
#include <QBitArray>
#include <KoColorSpace.h>
#include <KoChannelInfo.h>
#include <KoCompositeOpRegistry.h>
#include "kis_painter.h"
......@@ -50,7 +51,12 @@ void KisLayerProjectionPlane::apply(KisPainter *painter, const QRect &rect)
KisPaintDeviceSP device = m_d->layer->projection();
if (!device) return;
QRect needRect = rect & device->extent();
QRect needRect = rect;
if (m_d->layer->compositeOpId() != COMPOSITE_COPY) {
needRect &= device->extent();
}
if(needRect.isEmpty()) return;
QBitArray channelFlags = m_d->layer->channelFlags();
......@@ -84,7 +90,6 @@ void KisLayerProjectionPlane::apply(KisPainter *painter, const QRect &rect)
}
painter->setChannelFlags(channelFlags);
painter->setCompositeOp(m_d->layer->compositeOp());
painter->setOpacity(m_d->layer->opacity());
painter->bitBlt(needRect.topLeft(), device, needRect);
......
......@@ -155,10 +155,8 @@ void KisMask::Private::initSelectionImpl(KisSelectionSP copyFrom, KisLayerSP par
} else if (copyFromDevice) {
selection = new KisSelection(new KisSelectionDefaultBounds(parentPaintDevice, parentLayer->image()));
KisPainter gc(selection->pixelSelection());
gc.setCompositeOp(COMPOSITE_COPY);
QRect rc(copyFromDevice->extent());
gc.bitBlt(rc.topLeft(), copyFromDevice, rc);
KisPainter::copyAreaOptimized(rc.topLeft(), copyFromDevice, selection->pixelSelection(), rc);
selection->pixelSelection()->invalidateOutlineCache();
} else {
......@@ -246,12 +244,8 @@ void KisMask::apply(KisPaintDeviceSP projection, const QRect &applyRect, const Q
QRect updatedRect = decorateRect(projection, cacheDevice, applyRect, maskPos);
KisPainter gc(projection);
// masks don't have any compositioning
gc.setCompositeOp(COMPOSITE_COPY);
gc.setSelection(m_d->selection);
gc.bitBlt(updatedRect.topLeft(), cacheDevice, updatedRect);
KisPainter::copyAreaOptimized(updatedRect.topLeft(), cacheDevice, projection, updatedRect, m_d->selection);
m_d->paintDeviceCache.putDevice(cacheDevice);
} else {
......
......@@ -50,6 +50,7 @@ public:
protected:
KisMergeWalker() : m_flags(DEFAULT) {}
KisMergeWalker(Flags flags) : m_flags(flags) {}
/**
* Begins visiting nodes starting with @startWith.
......
......@@ -323,13 +323,23 @@ void KisPaintDevice::prepareClone(KisPaintDeviceSP src)
void KisPaintDevice::makeCloneFrom(KisPaintDeviceSP src, const QRect &rect)
{
prepareClone(src);
fastBitBlt(src, rect);
// we guarantee that *this is totally empty, so copy pixels that
// are areally present on the source image only
const QRect optimizedRect = rect & src->extent();
fastBitBlt(src, optimizedRect);
}
void KisPaintDevice::makeCloneFromRough(KisPaintDeviceSP src, const QRect &minimalRect)
{
prepareClone(src);
fastBitBltRough(src, minimalRect);
// we guarantee that *this is totally empty, so copy pixels that
// are areally present on the source image only
const QRect optimizedRect = minimalRect & src->extent();
fastBitBltRough(src, optimizedRect);
}
void KisPaintDevice::setDirty()
......
......@@ -115,11 +115,10 @@ void KisPaintLayer::copyOriginalToProjection(const KisPaintDeviceSP original,
{
lockTemporaryTarget();
KisPainter gc(projection);
gc.setCompositeOp(projection->colorSpace()->compositeOp(COMPOSITE_COPY));
gc.bitBlt(rect.topLeft(), original, rect);
KisPainter::copyAreaOptimized(rect.topLeft(), original, projection, rect);
if (hasTemporaryTarget()) {
KisPainter gc(projection);
setupTemporaryPainter(&gc);
gc.bitBlt(rect.topLeft(), temporaryTarget(), rect);
}
......
......@@ -186,6 +186,77 @@ KisPainter::~KisPainter()
delete d;
}
template <bool useOldData>
void copyAreaOptimizedImpl(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &srcRect)
{
const QRect dstRect(dstPt, srcRect.size());
const bool srcEmpty = (src->extent() & srcRect).isEmpty();
const bool dstEmpty = (dst->extent() & dstRect).isEmpty();
if (!srcEmpty || !dstEmpty) {
if (srcEmpty) {
dst->clear(dstRect);
} else {
KisPainter gc(dst);
gc.setCompositeOp(dst->colorSpace()->compositeOp(COMPOSITE_COPY));
if (useOldData) {
gc.bitBltOldData(dstRect.topLeft(), src, srcRect);
} else {
gc.bitBlt(dstRect.topLeft(), src, srcRect);
}
}
}
}
void KisPainter::copyAreaOptimized(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &srcRect)
{
copyAreaOptimizedImpl<false>(dstPt, src, dst, srcRect);
}
void KisPainter::copyAreaOptimizedOldData(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &srcRect)
{
copyAreaOptimizedImpl<true>(dstPt, src, dst, srcRect);
}
void KisPainter::copyAreaOptimized(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &originalSrcRect,
KisSelectionSP selection)
{
const QRect selectionRect = selection->selectedRect();
const QRect srcRect = originalSrcRect & selectionRect;
const QPoint dstOffset = srcRect.topLeft() - originalSrcRect.topLeft();
const QRect dstRect = QRect(dstPt + dstOffset, srcRect.size());
const bool srcEmpty = (src->extent() & srcRect).isEmpty();
const bool dstEmpty = (dst->extent() & dstRect).isEmpty();
if (!srcEmpty || !dstEmpty) {
//if (srcEmpty) {
// doesn't support dstRect
// dst->clearSelection(selection);
// } else */
{
KisPainter gc(dst);
gc.setSelection(selection);
gc.setCompositeOp(dst->colorSpace()->compositeOp(COMPOSITE_COPY));
gc.bitBlt(dstRect.topLeft(), src, srcRect);
}
}
}
void KisPainter::begin(KisPaintDeviceSP device)
{
begin(device, d->selection);
......
......@@ -87,6 +87,22 @@ public:
virtual ~KisPainter();
public:
static void copyAreaOptimized(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &originalSrcRect);
static void copyAreaOptimizedOldData(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &originalSrcRect);
static void copyAreaOptimized(const QPoint &dstPt,
KisPaintDeviceSP src,
KisPaintDeviceSP dst,
const QRect &originalSrcRect,
KisSelectionSP selection);
/**
* Start painting on the specified device. Not undoable.
*/
......
......@@ -150,18 +150,17 @@ void KisPerspectiveTransformWorker::runPartialDst(KisPaintDeviceSP srcDev,
const QRect &dstRect)
{
if (m_isIdentity) {
KisPainter gc(dstDev);
gc.setCompositeOp(COMPOSITE_COPY);
gc.bitBltOldData(dstRect.topLeft(), srcDev, dstRect);
KisPainter::copyAreaOptimizedOldData(dstRect.topLeft(), srcDev, dstDev, dstRect);
return;
}
QRectF srcClipRect = srcDev->exactBounds();
if (srcClipRect.isEmpty()) return;
KisProgressUpdateHelper progressHelper(m_progressUpdater, 100, dstRect.height());
KisRandomSubAccessorSP srcAcc = srcDev->createRandomSubAccessor();
KisRandomAccessorSP accessor = dstDev->createRandomAccessorNG(0, 0);
KisRandomAccessorSP accessor = dstDev->createRandomAccessorNG(dstRect.x(), dstRect.y());
for (int y = dstRect.y(); y < dstRect.y() + dstRect.height(); ++y) {
for (int x = dstRect.x(); x < dstRect.x() + dstRect.width(); ++x) {
......
......@@ -525,9 +525,6 @@ void KisPixelSelection::renderToProjection(KisPaintDeviceSP projection, const QR
QRect updateRect = rc & selectedExactRect();
if (updateRect.isValid()) {
KisPainter painter(projection);
painter.setCompositeOp(COMPOSITE_COPY);
painter.bitBlt(updateRect.topLeft(), KisPaintDeviceSP(this), updateRect);
painter.end();
KisPainter::copyAreaOptimized(updateRect.topLeft(), KisPaintDeviceSP(this), projection, updateRect);
}
}
......@@ -73,7 +73,7 @@ protected:
nextNode = currentNode->nextSibling();
if(isLayer(currentNode)) {
tempRect = calculateChangeRect(currentNode, tempRect);
tempRect |= calculateChangeRect(currentNode, requestedRect);
if(!changeRectVaries)
changeRectVaries = tempRect != requestedRect;
......@@ -96,7 +96,7 @@ protected:
}
void startTrip(KisNodeSP startWith) {
calculateChangeRect(startWith, requestedRect());
setExplicitChangeRect(startWith, requestedRect(), false);
if(startWith == startNode()) {
NodePosition pos = N_EXTRA | calculateNodePosition(startWith);
......
......@@ -140,9 +140,9 @@ void KisSelectionBasedLayer::copyOriginalToProjection(const KisPaintDeviceSP ori
projection->clear(rect);
gc.setCompositeOp(colorSpace()->compositeOp(COMPOSITE_OVER));
gc.setSelection(tempSelection);
} else
} else {
gc.setCompositeOp(colorSpace()->compositeOp(COMPOSITE_COPY));
}
gc.bitBlt(rect.topLeft(), original, rect);
......
......@@ -249,15 +249,13 @@ QRect KisTransformMask::decorateRect(KisPaintDeviceSP &src,
m_d->worker.runPartialDst(src, dst, rc);
#ifdef DEBUG_RENDERING
qDebug() << "Partial" << name() << ppVar(src->exactBounds()) << ppVar(dst->exactBounds()) << ppVar(rc);
qDebug() << "Partial" << name() << ppVar(src->exactBounds()) << ppVar(src->extent()) << ppVar(dst->exactBounds()) << ppVar(dst->extent()) << ppVar(rc);
KIS_DUMP_DEVICE_2(src, DUMP_RECT, "partial_src", "dd");
KIS_DUMP_DEVICE_2(dst, DUMP_RECT, "partial_dst", "dd");
#endif /* DEBUG_RENDERING */
} else {
KisPainter gc(dst);
gc.setCompositeOp(COMPOSITE_COPY);
gc.bitBlt(rc.topLeft(), m_d->staticCacheDevice, rc);
KisPainter::copyAreaOptimized(rc.topLeft(), m_d->staticCacheDevice, dst, rc);
#ifdef DEBUG_RENDERING
qDebug() << "Fetch" << name() << ppVar(src->exactBounds()) << ppVar(dst->exactBounds()) << ppVar(rc);
......
......@@ -95,9 +95,7 @@ void KisDumbTransformMaskParams::transformDevice(KisNodeSP node, KisPaintDeviceS
qWarning() << ppVar(t);
}
KisPainter gc(dst);
gc.setCompositeOp(COMPOSITE_COPY);
gc.bitBlt(dstTopLeft, src, rc);
KisPainter::copyAreaOptimized(dstTopLeft, src, dst, rc);
}
QString KisDumbTransformMaskParams::id() const
......
......@@ -635,8 +635,6 @@ void KisTransformWorker::offset(KisPaintDeviceSP device, const QPoint& offsetPos
}
KisPaintDeviceSP offsetDevice = new KisPaintDevice(device->colorSpace());
KisPainter gc(offsetDevice);
gc.setCompositeOp(COMPOSITE_COPY);
int srcX = 0;
int srcY = 0;
......@@ -647,10 +645,9 @@ void KisTransformWorker::offset(KisPaintDeviceSP device, const QPoint& offsetPos
width = qBound<int>(0, width - offsetX, width);
height = qBound<int>(0, height - offsetY, height);
if ((width != 0) && (height != 0))
{
if ((width != 0) && (height != 0)) {
// convert back to paint device space
gc.bitBlt(destX + sx, destY + sy, device, srcX + sx, srcY + sy, width, height);
KisPainter::copyAreaOptimized(QPoint(destX + sx, destY + sy), device, offsetDevice, QRect(srcX + sx, srcY + sy, width, height));
}
srcX = wrapRect.width() - offsetX;
......@@ -659,28 +656,21 @@ void KisTransformWorker::offset(KisPaintDeviceSP device, const QPoint& offsetPos
destX = (srcX + offsetX) % wrapRect.width();
destY = (srcY + offsetY) % wrapRect.height();
if (offsetX != 0 && offsetY != 0)
{
gc.bitBlt(destX + sx, destY + sy, device, srcX + sx, srcY + sy, offsetX, offsetY);
if (offsetX != 0 && offsetY != 0) {
KisPainter::copyAreaOptimized(QPoint(destX + sx, destY + sy), device, offsetDevice, QRect(srcX + sx, srcY + sy, offsetX, offsetY));
}
if (offsetX != 0)
{
gc.bitBlt(destX + sx, (destY + offsetY) + sy, device, srcX + sx, 0 + sy, offsetX, wrapRect.height() - offsetY);
if (offsetX != 0) {
KisPainter::copyAreaOptimized(QPoint(destX + sx, (destY + offsetY) + sy), device, offsetDevice, QRect(srcX + sx, 0 + sy, offsetX, wrapRect.height() - offsetY));
}
if (offsetY != 0)
{
gc.bitBlt((destX + offsetX) + sx, destY + sy, device, 0 + sx, srcY + sy, wrapRect.width() - offsetX, offsetY);
if (offsetY != 0) {
KisPainter::copyAreaOptimized(QPoint((destX + offsetX) + sx, destY + sy), device, offsetDevice, QRect(0 + sx, srcY + sy, wrapRect.width() - offsetX, offsetY));
}
gc.end();
// bitblt the result back
KisPainter gc2(device);
gc2.setCompositeOp(COMPOSITE_COPY);
gc2.bitBlt(sx,sy,offsetDevice, sx, sy, wrapRect.width(), wrapRect.height());
gc2.end();
QRect resultRect(sx, sy, wrapRect.width(), wrapRect.height());
KisPainter::copyAreaOptimized(resultRect.topLeft(), offsetDevice, device, resultRect);
}
......@@ -52,9 +52,7 @@ QRect KisTransparencyMask::decorateRect(KisPaintDeviceSP &src,
Q_UNUSED(maskPos);
if (src != dst) {
KisPainter gc(dst);
gc.setCompositeOp(src->colorSpace()->compositeOp(COMPOSITE_COPY));
gc.bitBlt(rc.topLeft(), src, rc);
KisPainter::copyAreaOptimized(rc.topLeft(), src, dst, rc);
src->fill(rc, KoColor(Qt::transparent, src->colorSpace()));
}
......
......@@ -810,4 +810,128 @@ void KisTransformMaskTest::testMaskWithOffset()
QVERIFY(chk.testPassed());
}
void KisTransformMaskTest::testWeirdFullUpdates()
{
//TestUtil::ExternalImageChecker chk("mask_with_offset", "transform_mask_updates");
QRect imageRect(0,0,512,512);
QRect fillRect(10, 10, 236, 236);
TestUtil::MaskParent p(imageRect);
p.layer->paintDevice()->fill(fillRect, KoColor(Qt::red, p.layer->colorSpace()));
KisPaintLayerSP player1 = new KisPaintLayer(p.image, "pl1", OPACITY_OPAQUE_U8, p.image->colorSpace());
player1->paintDevice()->fill(fillRect, KoColor(Qt::red, p.layer->colorSpace()));
p.image->addNode(player1, p.image->root());
KisTransformMaskSP mask1 = new KisTransformMask();
mask1->setName("mask1");
QTransform transform1 =
QTransform::fromTranslate(256, 0);
mask1->setTransformParams(KisTransformMaskParamsInterfaceSP(
new KisDumbTransformMaskParams(transform1)));
p.image->addNode(mask1, player1);
KisPaintLayerSP player2 = new KisPaintLayer(p.image, "pl2", OPACITY_OPAQUE_U8, p.image->colorSpace());
player2->paintDevice()->fill(fillRect, KoColor(Qt::red, p.layer->colorSpace()));
p.image->addNode(player2, p.image->root());
KisTransformMaskSP mask2 = new KisTransformMask();
mask2->setName("mask2");
QTransform transform2 =
QTransform::fromTranslate(0, 256);
mask2->setTransformParams(KisTransformMaskParamsInterfaceSP(
new KisDumbTransformMaskParams(transform2)));
p.image->addNode(mask2, player2);
KisPaintLayerSP player3 = new KisPaintLayer(p.image, "pl3", OPACITY_OPAQUE_U8, p.image->colorSpace());
player3->paintDevice()->fill(fillRect, KoColor(Qt::red, p.layer->colorSpace()));
p.image->addNode(player3, p.image->root());
KisTransformMaskSP mask3 = new KisTransformMask();
mask3->setName("mask3");
QTransform transform3 =
QTransform::fromTranslate(256, 256);
mask3->setTransformParams(KisTransformMaskParamsInterfaceSP(
new KisDumbTransformMaskParams(transform3)));
p.image->addNode(mask3, player3);
//p.image->initialRefreshGraph();
p.image->refreshGraphAsync(0, QRect(0,0,256,256), QRect());
p.image->waitForDone();
QVERIFY(player1->projection()->extent().isEmpty());
QVERIFY(player1->projection()->exactBounds().isEmpty());
QVERIFY(player2->projection()->extent().isEmpty());
QVERIFY(player2->projection()->exactBounds().isEmpty());
QVERIFY(player3->projection()->extent().isEmpty());
QVERIFY(player3->projection()->exactBounds().isEmpty());
QCOMPARE(p.image->projection()->exactBounds(), QRect(QRect(10,10,236,236)));
p.image->refreshGraphAsync(0, QRect(0,256,256,256), QRect());
p.image->waitForDone();
QVERIFY(player1->projection()->extent().isEmpty());
QVERIFY(player1->projection()->exactBounds().isEmpty());
QVERIFY(!player2->projection()->extent().isEmpty());
QVERIFY(!player2->projection()->exactBounds().isEmpty());
QVERIFY(player3->projection()->extent().isEmpty());
QVERIFY(player3->projection()->exactBounds().isEmpty());
QCOMPARE(p.image->projection()->exactBounds(), QRect(QRect(10,10,236,492)));
p.image->refreshGraphAsync(0, QRect(256,0,256,256), QRect());
p.image->waitForDone();
QVERIFY(!player1->projection()->extent().isEmpty());
QVERIFY(!player1->projection()->exactBounds().isEmpty());
QVERIFY(!player2->projection()->extent().isEmpty());
QVERIFY(!player2->projection()->exactBounds().isEmpty());
QVERIFY(player3->projection()->extent().isEmpty());
QVERIFY(player3->projection()->exactBounds().isEmpty());
QCOMPARE(p.image->projection()->exactBounds(), QRect(QRect(10,10,492,492)));
QVERIFY((p.image->projection()->region() & QRect(256,256,256,256)).isEmpty());
p.image->refreshGraphAsync(0, QRect(256,256,256,256), QRect());
p.image->waitForDone();
QVERIFY(!player1->projection()->extent().isEmpty());
QVERIFY(!player1->projection()->exactBounds().isEmpty());
QVERIFY(!player2->projection()->extent().isEmpty());
QVERIFY(!player2->projection()->exactBounds().isEmpty());
QVERIFY(!player3->projection()->extent().isEmpty());
QVERIFY(!player3->projection()->exactBounds().isEmpty());
QCOMPARE(p.image->projection()->exactBounds(), QRect(QRect(10,10,492,492)));